Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.ContainerRegistry", "src\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj", "{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.AI.Inference", "src\Components\Aspire.Azure.AI.Inference\Aspire.Azure.AI.Inference.csproj", "{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -3891,6 +3893,18 @@ Global
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x64.Build.0 = Release|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.ActiveCfg = Release|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.Build.0 = Release|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Debug|x64.ActiveCfg = Debug|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Debug|x64.Build.0 = Debug|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Debug|x86.ActiveCfg = Debug|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Debug|x86.Build.0 = Debug|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Release|Any CPU.Build.0 = Release|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Release|x64.ActiveCfg = Release|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Release|x64.Build.0 = Release|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Release|x86.ActiveCfg = Release|Any CPU
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -4209,6 +4223,7 @@ Global
{8FCA0CFA-7823-6A2F-342A-107A994915B0} = {C424395C-1235-41A4-BF55-07880A04368C}
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7} = {C424395C-1235-41A4-BF55-07880A04368C}
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {47DCFECF-5631-4BDE-A1EC-BE41E90F60C4}
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Azure SDK for .NET dependencies -->
<PackageVersion Include="Azure.AI.Inference" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
<PackageVersion Include="Azure.Data.Tables" Version="12.10.0" />
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" />
Expand Down Expand Up @@ -153,6 +154,7 @@
<!-- dotnet/extensions dependencies ** Common between net8 and net9 ** -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="$(MicrosoftExtensionsDiagnosticsTestingVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(MicrosoftExtensionsHttpResilienceVersion)" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="$(MicrosoftExtensionsTimeProviderTestingVersion)" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<IsPackable>true</IsPackable>
<PackageTags>$(ComponentAzurePackageTags) ai</PackageTags>
<Description>A client for Azure AI Inference SDK that integrates with Aspire, including logging and telemetry.</Description>
<PackageIconFullPath>$(SharedDir)Azure_256x.png</PackageIconFullPath>
<NoWarn>$(NoWarn);SYSLIB1100;SYSLIB1101</NoWarn>
<!-- In preview until the public API is validated and the Microsoft.Extensions.AI integration is designed.. -->
<SuppressFinalPackageVersion>true</SuppressFinalPackageVersion>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Common\AzureComponent.cs" Link="AzureComponent.cs" />
<Compile Include="..\Common\ConfigurationSchemaAttributes.cs" Link="ConfigurationSchemaAttributes.cs" />
<Compile Include="..\Common\HealthChecksExtensions.cs" Link="HealthChecksExtensions.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.Inference" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.AzureAIInference" />
<PackageReference Include="Microsoft.Extensions.Azure" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Azure.Common;
using Azure;
using Azure.AI.Inference;
using Azure.Core;
using Azure.Core.Extensions;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Microsoft.Extensions.Hosting;

/// <summary>
/// Extension methods for adding Azure AI Inference services to an Aspire application.
/// </summary>
public static class AspireAzureAIInferenceExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:AI:Inference";

/// <summary>
/// Adds a <see cref="ChatCompletionsClient"/> to the application and configures it with the specified settings.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to add the client to.</param>
/// <param name="connectionName">The name of the client. This is used to retrieve the connection string from configuration.</param>
/// <param name="configureClient">An optional callback to configure the <see cref="ChatCompletionsClientSettings"/>.</param>
/// <param name="configureClientBuilder">An optional callback to configure the <see cref="IAzureClientBuilder{TClient, TOptions}"/> for the client.</param>
/// <returns>An <see cref="AspireChatCompletionsClientBuilder"/> that can be used to further configure the client.</returns>
/// <exception cref="InvalidOperationException">Thrown when endpoint is missing from settings.</exception>
/// <remarks>
/// <para>
/// The client is registered as a singleton with a keyed service.
/// </para>
/// <para>
/// Configuration is loaded from the "Aspire:Azure:AI:Inference" section, and can be supplemented with a connection string named after the <paramref name="connectionName"/> parameter.
/// </para>
/// </remarks>
public static AspireChatCompletionsClientBuilder AddChatCompletionsClient(
this IHostApplicationBuilder builder,
string connectionName,
Action<ChatCompletionsClientSettings>? configureClient = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configureClient => configureSettings

Action<IAzureClientBuilder<ChatCompletionsClient, AzureAIInferenceClientOptions>>? configureClientBuilder = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(connectionName);

var settings = new ChatCompletionsClientServiceComponent().AddClient(
builder,
DefaultConfigSectionName,
configureClient,
configureClientBuilder,
connectionName,
serviceKey: null);

return new AspireChatCompletionsClientBuilder(builder, serviceKey: null, settings.ModelId, settings.DisableTracing);
}

/// <summary>
/// Adds a <see cref="ChatCompletionsClient"/> to the application and configures it with the specified settings.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to add the client to.</param>
/// <param name="name">The name of the component, which is used as the <see cref="ServiceDescriptor.ServiceKey"/> of the service and also to retrieve the connection string from the ConnectionStrings configuration section.</param>
/// <param name="configureClient">An optional callback to configure the <see cref="ChatCompletionsClientSettings"/>.</param>
/// <param name="configureClientBuilder">An optional callback to configure the <see cref="IAzureClientBuilder{TClient, TOptions}"/> for the client.</param>
/// <returns>An <see cref="AspireChatCompletionsClientBuilder"/> that can be used to further configure the client.</returns>
/// <exception cref="InvalidOperationException">Thrown when endpoint is missing from settings.</exception>
/// <remarks>
/// <para>
/// The client is registered as a singleton with a keyed service.
/// </para>
/// <para>
/// Configuration is loaded from the "Aspire:Azure:AI:Inference" section, and can be supplemented with a connection string named after the <paramref name="name"/> parameter.
/// </para>
/// </remarks>
public static AspireChatCompletionsClientBuilder AddKeyedChatCompletionsClient(
this IHostApplicationBuilder builder,
string name,
Action<ChatCompletionsClientSettings>? configureClient = null,
Action<IAzureClientBuilder<ChatCompletionsClient, AzureAIInferenceClientOptions>>? configureClientBuilder = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);

var settings = new ChatCompletionsClientServiceComponent().AddClient(
builder,
DefaultConfigSectionName,
configureClient,
configureClientBuilder,
name,
serviceKey: name);

return new AspireChatCompletionsClientBuilder(builder, serviceKey: name, settings.ModelId, settings.DisableTracing);
}

private sealed class ChatCompletionsClientServiceComponent : AzureComponent<ChatCompletionsClientSettings, ChatCompletionsClient, AzureAIInferenceClientOptions>
{
protected override IAzureClientBuilder<ChatCompletionsClient, AzureAIInferenceClientOptions> AddClient(
AzureClientFactoryBuilder azureFactoryBuilder,
ChatCompletionsClientSettings settings,
string connectionName, string
configurationSectionName)
{
return azureFactoryBuilder.AddClient<ChatCompletionsClient, AzureAIInferenceClientOptions>((options, _, _) =>
{
if (settings.Endpoint is null)
{
throw new InvalidOperationException($"An ChatCompletionsClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or specify a '{nameof(ChatCompletionsClientSettings.Endpoint)}' or '{nameof(ChatCompletionsClientSettings.Key)}' in the '{configurationSectionName}' configuration section.");
}
else
{
// Connect to Azure AI Foundry using key auth
if (!string.IsNullOrEmpty(settings.Key))
{
var credential = new AzureKeyCredential(settings.Key);
return new ChatCompletionsClient(settings.Endpoint, credential, options);
}
else
{
return new ChatCompletionsClient(settings.Endpoint, settings.TokenCredential ?? new DefaultAzureCredential(), options);
}
}
});
}

protected override void BindClientOptionsToConfiguration(IAzureClientBuilder<ChatCompletionsClient, AzureAIInferenceClientOptions> clientBuilder, IConfiguration configuration)
{
#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works
clientBuilder.ConfigureOptions(options => configuration.Bind(options));
#pragma warning restore IDE0200
}

protected override void BindSettingsToConfiguration(ChatCompletionsClientSettings settings, IConfiguration configuration)
=> configuration.Bind(settings);

protected override IHealthCheck CreateHealthCheck(ChatCompletionsClient client, ChatCompletionsClientSettings settings)
=> throw new NotImplementedException();

protected override bool GetHealthCheckEnabled(ChatCompletionsClientSettings settings)
=> false;

protected override bool GetMetricsEnabled(ChatCompletionsClientSettings settings)
=> !settings.DisableMetrics;

protected override TokenCredential? GetTokenCredential(ChatCompletionsClientSettings settings)
=> settings.TokenCredential;

protected override bool GetTracingEnabled(ChatCompletionsClientSettings settings)
=> !settings.DisableTracing;
}

/// <summary>
/// Creates a <see cref="IChatClient"/> from the <see cref="ChatCompletionsClient"/> registered in the service collection.
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static ChatClientBuilder AddChatClient(this AspireChatCompletionsClientBuilder builder) =>
builder.HostBuilder.Services.AddChatClient(services =>
{
var chatCompletionsClient = !string.IsNullOrEmpty(builder.ServiceKey) ?
services.GetRequiredService<ChatCompletionsClient>() :
services.GetRequiredKeyedService<ChatCompletionsClient>(builder.ServiceKey);

var result = chatCompletionsClient.AsIChatClient();

if (builder.DisableTracing)
{
return result;
}

var loggerFactory = services.GetService<ILoggerFactory>();

Check failure on line 174 in src/Components/Aspire.Azure.AI.Inference/AspireAzureAIInferenceExtensions.cs

View check run for this annotation

Azure Pipelines / dotnet.aspire (Build Linux)

src/Components/Aspire.Azure.AI.Inference/AspireAzureAIInferenceExtensions.cs#L174

src/Components/Aspire.Azure.AI.Inference/AspireAzureAIInferenceExtensions.cs(174,53): error CS0246: (NETCORE_ENGINEERING_TELEMETRY=Build) The type or namespace name 'ILoggerFactory' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 174 in src/Components/Aspire.Azure.AI.Inference/AspireAzureAIInferenceExtensions.cs

View check run for this annotation

Azure Pipelines / dotnet.aspire

src/Components/Aspire.Azure.AI.Inference/AspireAzureAIInferenceExtensions.cs#L174

src/Components/Aspire.Azure.AI.Inference/AspireAzureAIInferenceExtensions.cs(174,53): error CS0246: (NETCORE_ENGINEERING_TELEMETRY=Build) The type or namespace name 'ILoggerFactory' could not be found (are you missing a using directive or an assembly reference?)
return new OpenTelemetryChatClient(result, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it do this instead of using the UseOpenTelemetry helper method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know it existed. I copied that from the OpenAI integration

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me as the author of the OpenAI integration: 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Azure.AI.Inference;

namespace Microsoft.Extensions.Hosting;

/// <summary>
/// Provides a builder for configuring and integrating an Aspire Chat Completions client into a host application.
/// </summary>
/// <remarks>This class is used to configure the necessary parameters for creating an Aspire Chat Completions
/// client, such as the host application builder, service key, and optional model ID. It is intended for internal use
/// within the application setup process.</remarks>
/// <param name="hostBuilder">The <see cref="IHostApplicationBuilder"/> with which services are being registered.</param>
/// <param name="serviceKey">The service key used to register the <see cref="ChatCompletionsClient"/> service, if any.</param>
/// <param name="deploymentId">The id of the deployment in Azure AI Foundry.</param>
/// <param name="disableTracing">A flag to indicate whether tracing should be disabled.</param>
public class AspireChatCompletionsClientBuilder(
IHostApplicationBuilder hostBuilder,
string? serviceKey,
string? deploymentId,
bool disableTracing)
{
/// <summary>
/// Gets a flag indicating whether tracing should be disabled.
/// </summary>
public bool DisableTracing { get; } = disableTracing;

/// <summary>
/// Gets the <see cref="IHostApplicationBuilder"/> with which services are being registered.
/// </summary>
public IHostApplicationBuilder HostBuilder { get; } = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));

/// <summary>
/// Gets the service key used to register the <see cref="ChatCompletionsClient"/> service, if any.
/// </summary>
public string? ServiceKey { get; } = serviceKey;

/// <summary>
/// The ID of the deployment in Azure AI Foundry.
/// </summary>
public string? DeploymentId { get; } = deploymentId;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this get used?

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Azure.Core;
using System.Data.Common;

namespace Microsoft.Extensions.Hosting;

/// <summary>
/// Represents configuration settings for Azure AI Chat Completions client.
/// </summary>
public sealed class ChatCompletionsClientSettings
{
/// <summary>
/// Gets or sets the name of the AI model to use for chat completions.
/// </summary>
public string? ModelName { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly isn't, I think it was from an older implementation


/// <summary>
/// Gets or sets the ID of the AI model to use for chat completions.
/// </summary>
public string? ModelId { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Azure.AI.Inference library calls this "deploymentId", right? We also use the term DeploymentId in AspireChatCompletionsClientBuilder. We should consolidate on a single term here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed that in my refactor, will fix.


/// <summary>
/// Gets or sets the endpoint URI for the Azure AI service.
/// </summary>
public Uri? Endpoint { get; set; }

/// <summary>
/// Gets or sets the token credential used for Azure authentication.
/// </summary>
public TokenCredential? TokenCredential { get; set; }

/// <summary>
/// Gets or sets the API key used for authentication with the Azure AI service.
/// </summary>
public string? Key { get; set; }

/// <summary>
/// Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.
/// </summary>
/// <remarks>
/// /// Azure AI Inference telemetry follows the pattern of Azure SDKs Diagnostics.
/// </remarks>
public bool DisableMetrics { get; set; }

/// <summary>
/// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
/// </summary>
/// <remarks>
/// Azure AI Inference telemetry follows the pattern of Azure SDKs Diagnostics.
/// </remarks>
public bool DisableTracing { get; set; }

/// <summary>
/// Parses a connection string and populates the settings properties.
/// </summary>
/// <param name="connectionString">The connection string containing configuration values.</param>
/// <remarks>
/// The connection string can contain the following keys:
/// - ModelName: The name of the AI model
/// - ModelId: The ID of the AI model
/// - Endpoint: The service endpoint URI
/// - Key: The API key for authentication
/// </remarks>
internal void ParseConnectionString(string connectionString)
{
var connectionBuilder = new DbConnectionStringBuilder
{
ConnectionString = connectionString
};

if (connectionBuilder.TryGetValue(nameof(ModelName), out var model))
{
ModelName = model.ToString();
}

if (connectionBuilder.TryGetValue(nameof(ModelId), out var modelId))
{
ModelId = modelId.ToString();
}

if (connectionBuilder.TryGetValue(nameof(Endpoint), out var endpoint) && Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out var serviceUri))
{
Endpoint = serviceUri;
}

if (connectionBuilder.TryGetValue(nameof(Key), out var key) && !string.IsNullOrWhiteSpace(key.ToString()))
{
Key = key.ToString();
}
}
}
Loading