Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AddAzureOpenAIChatClient #6225

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="$(MicrosoftEntityFrameworkCoreToolsPackageVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="$(MicrosoftEntityFrameworkCoreDesignPackageVersion)" />
<!-- runtime dependencies-->
<PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsAIPackageVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="$(MicrosoftExtensionsAIPackageVersion)" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(MicrosoftExtensionsConfigurationAbstractionsPackageVersion)" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsConfigurationBinderPackageVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)" />
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<MicrosoftDotNetXUnitExtensionsPackageVersion>9.0.0-beta.24516.2</MicrosoftDotNetXUnitExtensionsPackageVersion>
<MicrosoftDotNetBuildTasksInstallersPackageVersion>9.0.0-beta.24516.2</MicrosoftDotNetBuildTasksInstallersPackageVersion>
<MicrosoftDotNetBuildTasksWorkloadsPackageVersion>9.0.0-beta.24516.2</MicrosoftDotNetBuildTasksWorkloadsPackageVersion>
<MicrosoftExtensionsAIPackageVersion>9.0.0-preview.9.24507.7</MicrosoftExtensionsAIPackageVersion>
<MicrosoftExtensionsHttpResiliencePackageVersion>9.0.0-preview.9.24524.6</MicrosoftExtensionsHttpResiliencePackageVersion>
<MicrosoftExtensionsDiagnosticsTestingPackageVersion>9.0.0-preview.9.24524.6</MicrosoftExtensionsDiagnosticsTestingPackageVersion>
<MicrosoftExtensionsConfigurationAbstractionsPackageVersion>8.0.0</MicrosoftExtensionsConfigurationAbstractionsPackageVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
<base href="/" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="OpenAIEndToEnd.WebStory.styles.css" />
<HeadOutlet @rendermode="InteractiveServer" />
<HeadOutlet @rendermode="@renderMode" />
</head>

<body>
<Routes @rendermode="InteractiveServer" />
<Routes @rendermode="@renderMode" />
<script src="_framework/blazor.web.js"></script>
</body>

</html>

@code {
IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@page "/"
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@using OpenAI
@using OpenAI.Chat
@inject OpenAIClient aiClient
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@page "/useichatclient"
@using Microsoft.Extensions.AI
@inject IChatClient aiClient
@inject ILogger<Home> logger
@inject IConfiguration configuration

<div class="storybox" style="margin: 25%">
@foreach (var message in chatMessages.Where(m => m.Role == ChatRole.Assistant))
{
<p style="font-size: 3em;">@message.Text</p>
}

<button @onclick="GenerateNextParagraph" autofocus>Generate</button>
</div>

@code {
private List<ChatMessage> chatMessages = new List<ChatMessage>
{
new(ChatRole.System, "Pick a random topic and write a sentence of a fictional story about it.")
};

private async Task GenerateNextParagraph()
{
if (chatMessages.Count > 1)
{
chatMessages.Add(new (ChatRole.User, "Write the next sentence in the story."));
}

var response = await aiClient.CompleteAsync(chatMessages);
chatMessages.Add(response.Message);
}

protected override async Task OnInitializedAsync()
{
await GenerateNextParagraph();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@
<ProjectReference Include="..\..\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj" />
</ItemGroup>

<Import Project="$(RepoRoot)\src\Components\Aspire.OpenAI\MEAIPackageOverrides.targets" />

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

builder.AddServiceDefaults();

builder.AddAzureOpenAIClient("openai");
builder.AddAzureOpenAIClient("openai").AddChatClient();

// Add services to the container.
builder.Services.AddRazorComponents()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@
<ProjectReference Include="..\Aspire.OpenAI\Aspire.OpenAI.csproj" />
</ItemGroup>

<Import Project="..\Aspire.OpenAI\MEAIPackageOverrides.targets" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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.OpenAI;

namespace Microsoft.Extensions.Hosting;

/// <summary>
/// A builder for configuring an <see cref="AzureOpenAIClient"/> service registration.
/// </summary>
public class AspireAzureOpenAIClientBuilder
{
/// <summary>
/// Constructs a new instance of <see cref="AspireAzureOpenAIClientBuilder"/>.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostApplicationBuilder"/> with which services are being registered.</param>
/// <param name="connectionName">The name used to retrieve the connection string from the ConnectionStrings configuration section.</param>
/// <param name="serviceKey">The service key used to register the <see cref="AzureOpenAIClient"/> service, if any.</param>
public AspireAzureOpenAIClientBuilder(IHostApplicationBuilder hostBuilder, string connectionName, string? serviceKey)
{
HostBuilder = hostBuilder;
ConnectionName = connectionName;
ServiceKey = serviceKey;
}

/// <summary>
/// Gets the <see cref="IHostApplicationBuilder"/> with which services are being registered.
/// </summary>
public IHostApplicationBuilder HostBuilder { get; }

/// <summary>
/// Gets the name used to retrieve the connection string from the ConnectionStrings configuration section.
/// </summary>
public string ConnectionName { get; }

/// <summary>
/// Gets the service key used to register the <see cref="AzureOpenAIClient"/> service, if any.
/// </summary>
public string? ServiceKey { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data.Common;
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.Hosting;

/// <summary>
/// Provides extension methods for registering <see cref="IChatClient"/> as a singleton in the services provided by the <see cref="IHostApplicationBuilder"/>.
/// </summary>
public static class AspireAzureOpenAIClientBuilderChatClientExtensions
{
private const string DeploymentKey = "Deployment";
private const string ModelKey = "Model";

/// <summary>
/// Registers a singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/>.
/// </summary>
/// <param name="builder">An <see cref="AspireAzureOpenAIClientBuilder" />.</param>
/// <param name="deploymentName">Optionally specifies which model deployment to use. If not specified, a value will be taken from the connection string.</param>
/// <param name="configurePipeline">An optional method that can be used for customizing the <see cref="IChatClient"/> pipeline.</param>
/// <remarks>Reads the configuration from "Aspire.Azure.AI.OpenAI" section.</remarks>
public static void AddChatClient(
this AspireAzureOpenAIClientBuilder builder,
string? deploymentName = null,
Func<ChatClientBuilder, ChatClientBuilder>? configurePipeline = null)
{
builder.HostBuilder.Services.AddSingleton(
services => CreateChatClient(services, builder, deploymentName, configurePipeline));
}

/// <summary>
/// Registers a keyed singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/>.
/// </summary>
/// <param name="builder">An <see cref="AspireAzureOpenAIClientBuilder" />.</param>
/// <param name="serviceKey">The service key with which the <see cref="IChatClient"/> will be registered.</param>
/// <param name="deploymentName">Optionally specifies which model deployment to use. If not specified, a value will be taken from the connection string.</param>
/// <param name="configurePipeline">An optional method that can be used for customizing the <see cref="IChatClient"/> pipeline.</param>
/// <remarks>Reads the configuration from "Aspire.Azure.AI.OpenAI" section.</remarks>
public static void AddKeyedChatClient(
this AspireAzureOpenAIClientBuilder builder,
string serviceKey,
string? deploymentName = null,
Func<ChatClientBuilder, ChatClientBuilder>? configurePipeline = null)
{
builder.HostBuilder.Services.TryAddKeyedSingleton(
serviceKey,
(services, _) => CreateChatClient(services, builder, deploymentName, configurePipeline));
}

private static IChatClient CreateChatClient(
IServiceProvider services,
AspireAzureOpenAIClientBuilder builder,
string? deploymentName,
Func<ChatClientBuilder, ChatClientBuilder>? configurePipeline)
{
var openAiClient = builder.ServiceKey is null
? services.GetRequiredService<AzureOpenAIClient>()
: services.GetRequiredKeyedService<AzureOpenAIClient>(builder.ServiceKey);

var chatClientBuilder = new ChatClientBuilder(services);
configurePipeline?.Invoke(chatClientBuilder);

deploymentName ??= GetRequiredDeploymentName(builder.HostBuilder.Configuration, builder.ConnectionName);

return chatClientBuilder.Use(openAiClient.AsChatClient(deploymentName));
}

private static string GetRequiredDeploymentName(IConfiguration configuration, string connectionName)
{
string? deploymentName = null;

if (configuration.GetConnectionString(connectionName) is string connectionString)
{
var connectionBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString };
var deploymentValue = ConnectionStringValue(connectionBuilder, DeploymentKey);
var modelValue = ConnectionStringValue(connectionBuilder, ModelKey);
if (deploymentValue is not null && modelValue is not null)
{
throw new InvalidOperationException(
$"The connection string '{connectionName}' contains both '{DeploymentKey}' and '{ModelKey}' keys. Either of these may be specified, but not both.");
}

deploymentName = deploymentValue ?? modelValue;
}

var configurationSectionName = AspireAzureOpenAIExtensions.DefaultConfigSectionName;
if (string.IsNullOrEmpty(deploymentName))
{
var configSection = configuration.GetSection(configurationSectionName);
deploymentName = configSection[DeploymentKey];
}

if (string.IsNullOrEmpty(deploymentName))
{
throw new InvalidOperationException($"An {nameof(IChatClient)} could not be configured. Ensure a '{DeploymentKey}' or '{ModelKey}' value is provided in 'ConnectionStrings:{connectionName}', or specify a '{DeploymentKey}' in the '{configurationSectionName}' configuration section, or specify a '{nameof(deploymentName)}' in the call to {nameof(AddChatClient)}.");
}

return deploymentName;
}

private static string? ConnectionStringValue(DbConnectionStringBuilder connectionString, string key)
=> connectionString.TryGetValue(key, out var value) ? value as string : null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Microsoft.Extensions.Hosting;
/// </summary>
public static class AspireAzureOpenAIExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:AI:OpenAI";
internal const string DefaultConfigSectionName = "Aspire:Azure:AI:OpenAI";

/// <summary>
/// Registers <see cref="AzureOpenAIClient"/> as a singleton in the services provided by the <paramref name="builder"/>.
Expand All @@ -34,7 +34,7 @@ public static class AspireAzureOpenAIExtensions
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="AzureOpenAISettings"/>. It's invoked after the settings are read from the configuration.</param>
/// <param name="configureClientBuilder">An optional method that can be used for customizing the <see cref="IAzureClientBuilder{AzureOpenAIClient, AzureOpenAIClientOptions}"/>.</param>
/// <remarks>Reads the configuration from "Aspire.Azure.AI.OpenAI" section.</remarks>
public static void AddAzureOpenAIClient(
public static AspireAzureOpenAIClientBuilder AddAzureOpenAIClient(
this IHostApplicationBuilder builder,
string connectionName,
Action<AzureOpenAISettings>? configureSettings = null,
Expand All @@ -44,6 +44,8 @@ public static void AddAzureOpenAIClient(

// Add the AzureOpenAIClient service as OpenAIClient. That way the service can be resolved by both service Types.
builder.Services.TryAddSingleton(typeof(OpenAIClient), static provider => provider.GetRequiredService<AzureOpenAIClient>());

return new AspireAzureOpenAIClientBuilder(builder, connectionName, serviceKey: null);
}

/// <summary>
Expand All @@ -56,7 +58,7 @@ public static void AddAzureOpenAIClient(
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="AzureOpenAISettings"/>. It's invoked after the settings are read from the configuration.</param>
/// <param name="configureClientBuilder">An optional method that can be used for customizing the <see cref="IAzureClientBuilder{AzureOpenAIClient, OpenAIClientOptions}"/>.</param>
/// <remarks>Reads the configuration from "Aspire.Azure.AI.OpenAI:{name}" section.</remarks>
public static void AddKeyedAzureOpenAIClient(
public static AspireAzureOpenAIClientBuilder AddKeyedAzureOpenAIClient(
this IHostApplicationBuilder builder,
string name,
Action<AzureOpenAISettings>? configureSettings = null,
Expand All @@ -68,6 +70,8 @@ public static void AddKeyedAzureOpenAIClient(

// Add the AzureOpenAIClient service as OpenAIClient. That way the service can be resolved by both service Types.
builder.Services.TryAddKeyedSingleton(typeof(OpenAIClient), serviceKey: name, static (provider, key) => provider.GetRequiredKeyedService<AzureOpenAIClient>(key));

return new AspireAzureOpenAIClientBuilder(builder, name, name);
}

private sealed class OpenAIComponent : AzureComponent<AzureOpenAISettings, AzureOpenAIClient, AzureOpenAIClientOptions>
Expand Down
12 changes: 10 additions & 2 deletions src/Components/Aspire.Azure.AI.OpenAI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ Aspire.Azure.AI.OpenAI.AzureOpenAISettings.Endpoint.get -> System.Uri?
Aspire.Azure.AI.OpenAI.AzureOpenAISettings.Endpoint.set -> void
Aspire.Azure.AI.OpenAI.AzureOpenAISettings.Key.get -> string?
Aspire.Azure.AI.OpenAI.AzureOpenAISettings.Key.set -> void
Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder
Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder.AspireAzureOpenAIClientBuilder(Microsoft.Extensions.Hosting.IHostApplicationBuilder! hostBuilder, string! connectionName, string? serviceKey) -> void
Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder.ConnectionName.get -> string!
Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder.HostBuilder.get -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder.ServiceKey.get -> string?
Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilderChatClientExtensions
Microsoft.Extensions.Hosting.AspireAzureOpenAIExtensions
Microsoft.Extensions.Hosting.AspireConfigurableOpenAIExtensions
static Microsoft.Extensions.Hosting.AspireAzureOpenAIExtensions.AddAzureOpenAIClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! connectionName, System.Action<Aspire.Azure.AI.OpenAI.AzureOpenAISettings!>? configureSettings = null, System.Action<Azure.Core.Extensions.IAzureClientBuilder<Azure.AI.OpenAI.AzureOpenAIClient!, Azure.AI.OpenAI.AzureOpenAIClientOptions!>!>? configureClientBuilder = null) -> void
static Microsoft.Extensions.Hosting.AspireAzureOpenAIExtensions.AddKeyedAzureOpenAIClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! name, System.Action<Aspire.Azure.AI.OpenAI.AzureOpenAISettings!>? configureSettings = null, System.Action<Azure.Core.Extensions.IAzureClientBuilder<Azure.AI.OpenAI.AzureOpenAIClient!, Azure.AI.OpenAI.AzureOpenAIClientOptions!>!>? configureClientBuilder = null) -> void
static Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilderChatClientExtensions.AddChatClient(this Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder! builder, string? deploymentName = null, System.Func<Microsoft.Extensions.AI.ChatClientBuilder!, Microsoft.Extensions.AI.ChatClientBuilder!>? configurePipeline = null) -> void
static Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilderChatClientExtensions.AddKeyedChatClient(this Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder! builder, string! serviceKey, string? deploymentName = null, System.Func<Microsoft.Extensions.AI.ChatClientBuilder!, Microsoft.Extensions.AI.ChatClientBuilder!>? configurePipeline = null) -> void
static Microsoft.Extensions.Hosting.AspireAzureOpenAIExtensions.AddAzureOpenAIClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! connectionName, System.Action<Aspire.Azure.AI.OpenAI.AzureOpenAISettings!>? configureSettings = null, System.Action<Azure.Core.Extensions.IAzureClientBuilder<Azure.AI.OpenAI.AzureOpenAIClient!, Azure.AI.OpenAI.AzureOpenAIClientOptions!>!>? configureClientBuilder = null) -> Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder!
static Microsoft.Extensions.Hosting.AspireAzureOpenAIExtensions.AddKeyedAzureOpenAIClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! name, System.Action<Aspire.Azure.AI.OpenAI.AzureOpenAISettings!>? configureSettings = null, System.Action<Azure.Core.Extensions.IAzureClientBuilder<Azure.AI.OpenAI.AzureOpenAIClient!, Azure.AI.OpenAI.AzureOpenAIClientOptions!>!>? configureClientBuilder = null) -> Microsoft.Extensions.Hosting.AspireAzureOpenAIClientBuilder!
static Microsoft.Extensions.Hosting.AspireConfigurableOpenAIExtensions.AddKeyedOpenAIClientFromConfiguration(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! name) -> void
static Microsoft.Extensions.Hosting.AspireConfigurableOpenAIExtensions.AddOpenAIClientFromConfiguration(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! connectionName) -> void
5 changes: 4 additions & 1 deletion src/Components/Aspire.OpenAI/Aspire.OpenAI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="OpenAI" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
</ItemGroup>

<Import Project="MEAIPackageOverrides.targets" />

</Project>
13 changes: 13 additions & 0 deletions src/Components/Aspire.OpenAI/MEAIPackageOverrides.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project>
<ItemGroup>
<!--
Microsoft.Extensions.AI depends on 9.x packages, even on net8.0, so we have to override central package management
to avoid "package downgrade" build errors. This is only used when referencing Aspire.OpenAI and doesn't break
compatibility with net8.0.
-->
<PackageReference Include="Microsoft.Extensions.Primitives" VersionOverride="9.0.0-rc.2.24473.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="9.0.0-rc.2.24473.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" VersionOverride="9.0.0-rc.2.24473.5" />
<PackageReference Include="System.Text.Json" VersionOverride="9.0.0-rc.2.24473.5" />
</ItemGroup>
</Project>
Loading