Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
30 changes: 30 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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.AppService", "src\Aspire.Hosting.Azure.AppService\Aspire.Hosting.Azure.AppService.csproj", "{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzureAppService", "AzureAppService", "{2D9974C2-3AB2-FBFD-5156-080508BB7449}"
Expand All @@ -673,6 +675,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAppService.ApiService"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAppService.AppHost", "playground\AzureAppService\AzureAppService.AppHost\AzureAppService.AppHost.csproj", "{2C879943-DF34-44FA-B2C3-29D97F24DD76}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.AI.Inference.Tests", "tests\Aspire.Azure.AI.Inference.Tests\Aspire.Azure.AI.Inference.Tests.csproj", "{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -3899,6 +3903,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
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -3935,6 +3951,18 @@ Global
{2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x64.Build.0 = Release|Any CPU
{2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x86.ActiveCfg = Release|Any CPU
{2C879943-DF34-44FA-B2C3-29D97F24DD76}.Release|x86.Build.0 = Release|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Debug|x64.ActiveCfg = Debug|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Debug|x64.Build.0 = Debug|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Debug|x86.ActiveCfg = Debug|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Debug|x86.Build.0 = Debug|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Release|Any CPU.Build.0 = Release|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Release|x64.ActiveCfg = Release|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Release|x64.Build.0 = Release|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Release|x86.ActiveCfg = Release|Any CPU
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -4253,10 +4281,12 @@ 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} = {77CFE74A-32EE-400C-8930-5025E8555256}
{F53BA5F3-31ED-E71D-DABD-DF4C9DF59079} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941} = {77CFE74A-32EE-400C-8930-5025E8555256}
{2D9974C2-3AB2-FBFD-5156-080508BB7449} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{A617DC84-65DA-41B5-B378-6C2F569CEE48} = {2D9974C2-3AB2-FBFD-5156-080508BB7449}
{2C879943-DF34-44FA-B2C3-29D97F24DD76} = {2D9974C2-3AB2-FBFD-5156-080508BB7449}
{58A5A310-BDB7-494F-8BC7-BEC66430F4A8} = {C424395C-1235-41A4-BF55-07880A04368C}
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 @@ -154,6 +155,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 Azure.AI.Inference is shipped stable. -->
<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,178 @@
// 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.DeploymentId, 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.DeploymentId, 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;
}

return new ChatClientBuilder(result)
.UseOpenTelemetry()
.Build();
});
}
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?

}
15 changes: 15 additions & 0 deletions src/Components/Aspire.Azure.AI.Inference/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire;
using Azure.AI.Inference;
using Microsoft.Extensions.Hosting;

[assembly: ConfigurationSchema("Aspire:Azure:AI:Inference", typeof(ChatCompletionsClientSettings))]
[assembly: ConfigurationSchema("Aspire:Azure:AI:Inference:ClientOptions", typeof(AzureAIInferenceClientOptions))]

[assembly: LoggingCategories(
"Azure",
"Azure.Core",
"Azure.Identity"
)]
Loading
Loading