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

Configure OpenAI models in the app host #6577

Open
wants to merge 17 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
29 changes: 23 additions & 6 deletions playground/OpenAIEndToEnd/OpenAIEndToEnd.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Azure.Provisioning.CognitiveServices;

var builder = DistributedApplication.CreateBuilder(args);

var deploymentAndModelName = "gpt-4o";
var openai = builder.AddAzureOpenAI("openai").AddDeployment(
new(deploymentAndModelName, deploymentAndModelName, "2024-05-13")
);
var openaiA = builder.AddAzureOpenAI("openaiA")
.ConfigureInfrastructure(infra =>
{
var cognitiveAccount = infra.GetProvisionableResources().OfType<CognitiveServicesAccount>().Single();
cognitiveAccount.Properties.DisableLocalAuth = false;
})
.AddDeployment(new("modelA1", "gpt-4o", "2024-05-13"))
;

//var openaiB = builder.AddAzureOpenAI("openaiB")
// .ConfigureInfrastructure(infra =>
// {
// var cognitiveAccount = infra.GetProvisionableResources().OfType<CognitiveServicesAccount>().Single();
// cognitiveAccount.Properties.DisableLocalAuth = false;
// })
// .AddDeployment(new("modelB1", "gpt-4o", "2024-05-13"))
// .AddDeployment(new("modelB2", "gpt-4o", "2024-05-13"));

builder.AddProject<Projects.OpenAIEndToEnd_WebStory>("webstory")
.WithExternalHttpEndpoints()
.WithReference(openai)
.WithEnvironment("OpenAI__DeploymentName", deploymentAndModelName);
.WithEnvironment("OpenAI__DeploymentName", "modelA1") // Used by the Pages/Home.razor component
.WithReference(openaiA)
//.WithReference(openaiB)
;

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
Expand Down
9 changes: 9 additions & 0 deletions playground/OpenAIEndToEnd/OpenAIEndToEnd.WebStory/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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;
using Microsoft.Extensions.AI;
using OpenAIEndToEnd.WebStory.Components;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -18,6 +20,13 @@

var app = builder.Build();

ArgumentNullException.ThrowIfNull(app.Services.GetService<AzureOpenAIClient>());
ArgumentNullException.ThrowIfNull(app.Services.GetService<IChatClient>());

//ArgumentNullException.ThrowIfNull(app.Services.GetKeyedService<AzureOpenAIClient>("openaiB"));
//ArgumentNullException.ThrowIfNull(app.Services.GetKeyedService<IChatClient>("modelB1"));
//ArgumentNullException.ThrowIfNull(app.Services.GetKeyedService<IChatClient>("modelB2"));

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace Aspire.Hosting;
/// </summary>
public static class AzureOpenAIExtensions
{
internal const string DefaultConfigSectionName = "Aspire:Azure:AI:OpenAI";

/// <summary>
/// Adds an Azure OpenAI resource to the application model.
/// </summary>
Expand Down Expand Up @@ -117,7 +119,7 @@ public static IResourceBuilder<AzureOpenAIResource> AddAzureOpenAI(this IDistrib
}

/// <summary>
/// Adds an Azure OpenAI Deployment resource to the application model. This resource requires an <see cref="AzureOpenAIResource"/> to be added to the application model.
/// Adds an Azure OpenAI Deployment to the <see cref="AzureOpenAIResource"/> resource. This resource requires an <see cref="AzureOpenAIResource"/> to be added to the application model.
/// </summary>
/// <param name="builder">The Azure OpenAI resource builder.</param>
/// <param name="deployment">The deployment to add.</param>
Expand All @@ -128,9 +130,41 @@ public static IResourceBuilder<AzureOpenAIResource> AddDeployment(this IResource
ArgumentNullException.ThrowIfNull(deployment);

builder.Resource.AddDeployment(deployment);

return builder;
}

/// <summary>
/// Injects the environment variables from the source <see cref="AzureOpenAIResource" /> into the destination resource, using the source resource's name as the connection string name (if not overridden).
/// The format of the connection environment variable will be "ConnectionStrings__{sourceResourceName}={connectionString}".
/// Each deployment will be injected using the format "Aspire__Azure__AI__OpenAI__{sourceResourceName}__Models__{deploymentName}={modelName}".
/// </summary>
/// <typeparam name="TDestination">The destination resource.</typeparam>
/// <param name="builder">The resource where connection string will be injected.</param>
/// <param name="source">The resource from which to extract the connection string.</param>
/// <param name="resourceName">An override of the source resource's name for the connection string. The resulting connection string will be "ConnectionStrings__connectionName" if this is not null.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<TDestination> WithReference<TDestination>(this IResourceBuilder<TDestination> builder, IResourceBuilder<AzureOpenAIResource> source, string? resourceName = null)
where TDestination : IResourceWithEnvironment
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(source);

var resource = source.Resource;
resourceName ??= resource.Name;

builder.WithReference((IResourceBuilder<IResourceWithConnectionString>)source, resourceName);

return builder.WithEnvironment(context =>
{
foreach (var deployment in resource.Deployments)
{
var variableName = $"ASPIRE__AZURE__AI__OPENAI__{resourceName}__MODELS__{deployment.Name}";
context.EnvironmentVariables[variableName] = deployment.ModelName;
}
});
}

/// <summary>
/// Assigns the specified roles to the given resource, granting it the necessary permissions
/// on the target Azure OpenAI resource. This replaces the default role assignments for the resource.
Expand Down
15 changes: 10 additions & 5 deletions src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ namespace Aspire.Hosting.ApplicationModel;
/// Represents an Azure OpenAI resource.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="configureInfrastructure">Configures the underlying Azure resource using Azure.Provisioning.</param>
public class AzureOpenAIResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
: AzureProvisioningResource(name, configureInfrastructure),
IResourceWithConnectionString
/// <param name="configureInfrastructure">Configures the underlying Azure resource using the CDK.</param>
public class AzureOpenAIResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
AzureProvisioningResource(name, configureInfrastructure),
IResourceWithConnectionString,
IResourceWithEnvironment
{
private readonly List<AzureOpenAIDeployment> _deployments = [];

Expand All @@ -35,7 +36,11 @@ public class AzureOpenAIResource(string name, Action<AzureResourceInfrastructure
/// </summary>
public IReadOnlyList<AzureOpenAIDeployment> Deployments => _deployments;

internal void AddDeployment(AzureOpenAIDeployment deployment)
/// <summary>
/// Adds an <see cref="AzureOpenAIDeployment"/> instance to the list of deployments.
/// </summary>
/// <param name="deployment">The <see cref="AzureOpenAIDeployment"/> instance to add.</param>
public void AddDeployment(AzureOpenAIDeployment deployment)
{
ArgumentNullException.ThrowIfNull(deployment);

Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ private static Action<EnvironmentCallbackContext> CreateEndpointReferenceEnviron

/// <summary>
/// Injects a connection string as an environment variable from the source resource into the destination resource, using the source resource's name as the connection string name (if not overridden).
/// The format of the environment variable will be "ConnectionStrings__{sourceResourceName}={connectionString}."
/// The format of the environment variable will be "ConnectionStrings__{sourceResourceName}={connectionString}".
/// <para>
/// Each resource defines the format of the connection string value. The
/// underlying connection string value can be retrieved using <see cref="IResourceWithConnectionString.GetConnectionStringAsync(CancellationToken)"/>.
Expand Down Expand Up @@ -363,7 +363,7 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(this IR

/// <summary>
/// Injects service discovery information as environment variables from the project resource into the destination resource, using the source resource's name as the service name.
/// Each endpoint defined on the project resource will be injected using the format "services__{sourceResourceName}__{endpointName}__{endpointIndex}={uriString}."
/// Each endpoint defined on the project resource will be injected using the format "services__{sourceResourceName}__{endpointName}__{endpointIndex}={uriString}".
/// </summary>
/// <typeparam name="TDestination">The destination resource.</typeparam>
/// <param name="builder">The resource where the service discovery information will be injected.</param>
Expand Down Expand Up @@ -410,7 +410,7 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(this IR

/// <summary>
/// Injects service discovery information from the specified endpoint into the project resource using the source resource's name as the service name.
/// Each endpoint will be injected using the format "services__{sourceResourceName}__{endpointName}__{endpointIndex}={uriString}."
/// Each endpoint will be injected using the format "services__{sourceResourceName}__{endpointName}__{endpointIndex}={uriString}".
/// </summary>
/// <typeparam name="TDestination">The destination resource.</typeparam>
/// <param name="builder">The resource where the service discovery information will be injected.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

/// <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>
Expand All @@ -20,4 +24,4 @@
{
/// <inheritdoc />
public override string ConfigurationSectionName => AspireAzureOpenAIExtensions.DefaultConfigSectionName;
}

Check failure on line 27 in src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIClientBuilder.cs

View check run for this annotation

Azure Pipelines / dotnet.aspire (Build Linux)

src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIClientBuilder.cs#L27

src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIClientBuilder.cs(27,2): error CS1513: (NETCORE_ENGINEERING_TELEMETRY=Build) } expected

Check failure on line 27 in src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIClientBuilder.cs

View check run for this annotation

Azure Pipelines / dotnet.aspire

src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIClientBuilder.cs#L27

src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIClientBuilder.cs(27,2): error CS1513: (NETCORE_ENGINEERING_TELEMETRY=Build) } expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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.AI.OpenAI;
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
{
/// <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 AspireAzureOpenAIClientBuilder AddChatClient(
this AspireAzureOpenAIClientBuilder builder,
string? deploymentName = null,
Func<ChatClientBuilder, ChatClientBuilder>? configurePipeline = null)
{
builder.HostBuilder.Services.AddSingleton(
services => CreateChatClient(services, builder, deploymentName, configurePipeline));

return builder;
}

/// <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="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 AspireAzureOpenAIClientBuilder AddKeyedChatClient(
this AspireAzureOpenAIClientBuilder builder,
string serviceKey,
Func<ChatClientBuilder, ChatClientBuilder>? configurePipeline = null)
{
builder.HostBuilder.Services.TryAddKeyedSingleton(
serviceKey,
(services, _) => CreateChatClient(services, builder, serviceKey, configurePipeline));

return builder;
}

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);

var deploymentSettings = GetDeployments(builder.HostBuilder.Configuration, builder.ConnectionName);

// If no deployment name is provided, we search for the first one (and maybe only one) in configuration
if (deploymentName is null)
{
deploymentName = deploymentSettings.Models.Keys.FirstOrDefault();

if (string.IsNullOrEmpty(deploymentName))
{
throw new InvalidOperationException($"An {nameof(IChatClient)} could not be configured. Ensure a deployment was defined .");
}
}

if (!deploymentSettings.Models.TryGetValue(deploymentName, out var _))
{
throw new InvalidOperationException($"An {nameof(IChatClient)} could not be configured. Ensure the deployment name '{deploymentName}' was defined .");
}

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

private static DeploymentModelSettings GetDeployments(IConfiguration configuration, string connectionName)
{
var configurationSectionName = $"{AspireAzureOpenAIExtensions.DefaultConfigSectionName}:{connectionName}";
var configSection = configuration.GetSection(configurationSectionName);

var settings = new DeploymentModelSettings();
configSection.Bind(settings);

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

namespace Aspire.Azure.AI.OpenAI;

/// <summary>
/// Helper class to bind the deployment models from configuration (deployment names and model names).
/// More specifically, it binds the "Aspire:Azure:AI:OpenAI:{resourceName}:Models" section.
/// </summary>
internal sealed class DeploymentModelSettings
{
/// <summary>
/// Gets or sets the dictionary of deployment names and model names.
/// </summary>
/// <remarks>
/// For instance <code>{ ["chat"] = "gpt-4o" }</code>.
/// </remarks>
public Dictionary<string, string> Models { get; set; } = [];
}
Loading