Skip to content

Commit

Permalink
Integrate OpenAI/AzureOpenAI support with Microsoft.Extensions.AI (#6225
Browse files Browse the repository at this point in the history
)
  • Loading branch information
SteveSandersonMS authored Nov 27, 2024
1 parent 111d4e8 commit 04cf1fe
Show file tree
Hide file tree
Showing 25 changed files with 1,272 additions and 23 deletions.
6 changes: 4 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Azure SDK for .NET dependencies -->
<PackageVersion Include="Azure.AI.OpenAI" Version="2.0.0" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.1.0-beta.2" />
<PackageVersion Include="Azure.Data.Tables" Version="12.9.1" />
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.2" />
<PackageVersion Include="Azure.Messaging.EventHubs" Version="5.11.5" />
Expand Down Expand Up @@ -80,6 +80,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 Expand Up @@ -116,7 +118,7 @@
<PackageVersion Include="NATS.Net" Version="2.5.3" />
<PackageVersion Include="Npgsql.DependencyInjection" Version="8.0.6" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(NpgsqlEntityFrameworkCorePostgreSQLPackageVersion)" />
<PackageVersion Include="OpenAI" Version="2.0.0" />
<PackageVersion Include="OpenAI" Version="2.1.0-beta.2" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="8.23.60" />
<PackageVersion Include="Oracle.ManagedDataAccess.OpenTelemetry" Version="23.6.0" />
<PackageVersion Include="Polly.Core" Version="8.5.0" />
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<MicrosoftDotNetXUnitExtensionsPackageVersion>9.0.0-beta.24572.2</MicrosoftDotNetXUnitExtensionsPackageVersion>
<MicrosoftDotNetBuildTasksInstallersPackageVersion>9.0.0-beta.24516.2</MicrosoftDotNetBuildTasksInstallersPackageVersion>
<MicrosoftDotNetBuildTasksWorkloadsPackageVersion>9.0.0-beta.24516.2</MicrosoftDotNetBuildTasksWorkloadsPackageVersion>
<MicrosoftExtensionsAIPackageVersion>9.0.1-preview.1.24570.5</MicrosoftExtensionsAIPackageVersion>
<MicrosoftExtensionsHttpResiliencePackageVersion>9.0.0</MicrosoftExtensionsHttpResiliencePackageVersion>
<MicrosoftExtensionsDiagnosticsTestingPackageVersion>9.0.0</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>
5 changes: 4 additions & 1 deletion playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

builder.AddServiceDefaults();

builder.AddAzureOpenAIClient("openai");
// Instead of passing this manually, it can also be read from the connection string
var openAiDeploymentName = builder.Configuration["OpenAI:DeploymentName"];

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

// 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,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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

namespace Aspire.Azure.AI.OpenAI;

/// <summary>
/// A builder for configuring an <see cref="AzureOpenAIClient"/> service registration.
/// </summary>
public class AspireAzureOpenAIClientBuilder : AspireOpenAIClientBuilder
{
/// <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>
/// <param name="disableTracing">A flag to indicate whether tracing should be disabled.</param>
public AspireAzureOpenAIClientBuilder(IHostApplicationBuilder hostBuilder, string connectionName, string? serviceKey, bool disableTracing)
: base(hostBuilder, connectionName, serviceKey, disableTracing)
{
}

/// <inheritdoc />
public override string ConfigurationSectionName => AspireAzureOpenAIExtensions.DefaultConfigSectionName;
}
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 @@ -33,17 +33,20 @@ public static class AspireAzureOpenAIExtensions
/// <param name="connectionName">A name used to retrieve the connection string from the ConnectionStrings configuration section.</param>
/// <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>
/// <returns>An <see cref="AspireAzureOpenAIClientBuilder"/> that can be used to register additional services.</returns>
/// <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,
Action<IAzureClientBuilder<AzureOpenAIClient, AzureOpenAIClientOptions>>? configureClientBuilder = null)
{
new OpenAIComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
var settings = new OpenAIComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);

// 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, disableTracing: settings.DisableTracing);
}

/// <summary>
Expand All @@ -55,19 +58,22 @@ public static void AddAzureOpenAIClient(
/// <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="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>
/// <returns>An <see cref="AspireAzureOpenAIClientBuilder"/> that can be used to register additional services.</returns>
/// <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,
Action<IAzureClientBuilder<AzureOpenAIClient, AzureOpenAIClientOptions>>? configureClientBuilder = null)
{
ArgumentException.ThrowIfNullOrEmpty(name);

new OpenAIComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name);
var settings = new OpenAIComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name);

// 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, settings.DisableTracing);
}

private sealed class OpenAIComponent : AzureComponent<AzureOpenAISettings, AzureOpenAIClient, AzureOpenAIClientOptions>
Expand Down
7 changes: 5 additions & 2 deletions src/Components/Aspire.Azure.AI.OpenAI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
Aspire.Azure.AI.OpenAI.AspireAzureOpenAIClientBuilder
Aspire.Azure.AI.OpenAI.AspireAzureOpenAIClientBuilder.AspireAzureOpenAIClientBuilder(Microsoft.Extensions.Hosting.IHostApplicationBuilder! hostBuilder, string! connectionName, string? serviceKey, bool disableTracing) -> void
Aspire.Azure.AI.OpenAI.AzureOpenAISettings
Aspire.Azure.AI.OpenAI.AzureOpenAISettings.AzureOpenAISettings() -> void
Aspire.Azure.AI.OpenAI.AzureOpenAISettings.Credential.get -> Azure.Core.TokenCredential?
Expand All @@ -13,7 +15,8 @@ Aspire.Azure.AI.OpenAI.AzureOpenAISettings.Key.get -> string?
Aspire.Azure.AI.OpenAI.AzureOpenAISettings.Key.set -> void
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
override Aspire.Azure.AI.OpenAI.AspireAzureOpenAIClientBuilder.ConfigurationSectionName.get -> string!
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) -> Aspire.Azure.AI.OpenAI.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) -> Aspire.Azure.AI.OpenAI.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>
98 changes: 98 additions & 0 deletions src/Components/Aspire.OpenAI/AspireOpenAIClientBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.Configuration;
using System.Data.Common;
using Microsoft.Extensions.Hosting;
using OpenAI;

namespace Aspire.OpenAI;

/// <summary>
/// A builder for configuring an <see cref="OpenAIClient"/> service registration.
/// </summary>
public class AspireOpenAIClientBuilder
{
private const string DeploymentKey = "Deployment";
private const string ModelKey = "Model";

/// <summary>
/// Constructs a new instance of <see cref="AspireOpenAIClientBuilder"/>.
/// </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="OpenAIClient"/> service, if any.</param>
/// <param name="disableTracing">A flag to indicate whether tracing should be disabled.</param>
public AspireOpenAIClientBuilder(IHostApplicationBuilder hostBuilder, string connectionName, string? serviceKey, bool disableTracing)
{
HostBuilder = hostBuilder;
ConnectionName = connectionName;
ServiceKey = serviceKey;
DisableTracing = disableTracing;
}

/// <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="OpenAIClient"/> service, if any.
/// </summary>
public string? ServiceKey { get; }

/// <summary>
/// Gets a flag indicating whether tracing should be disabled.
/// </summary>
public bool DisableTracing { get; }

/// <summary>
/// Gets the name of the configuration section for this component type.
/// </summary>
public virtual string ConfigurationSectionName => AspireOpenAIExtensions.DefaultConfigSectionName;

internal string GetRequiredDeploymentName()
{
string? deploymentName = null;

var configuration = HostBuilder.Configuration;
if (configuration.GetConnectionString(ConnectionName) is string connectionString)
{
// The reason we accept either 'Deployment' or 'Model' as the key is because OpenAI's terminology
// is 'Model' and Azure OpenAI's terminology is 'Deployment'. It may seem awkward if we picked just
// one of these, as it might not match the usage scenario. We could restrict it based on which backend
// you're using, but that adds an unnecessary failure case for no clear benefit.
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;
}

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

if (string.IsNullOrEmpty(deploymentName))
{
throw new InvalidOperationException($"The deployment could not be determined. 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.");
}

return deploymentName;
}

private static string? ConnectionStringValue(DbConnectionStringBuilder connectionString, string key)
=> connectionString.TryGetValue(key, out var value) ? value as string : null;
}
Loading

0 comments on commit 04cf1fe

Please sign in to comment.