Skip to content

Commit b3c4ccf

Browse files
committed
Address feedback from PR review
1 parent a3a3e6f commit b3c4ccf

File tree

11 files changed

+161
-79
lines changed

11 files changed

+161
-79
lines changed

src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/.template.config/template.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@
6060
"ChatWithCustomData-CSharp.AppHost/**",
6161
"ChatWithCustomData-CSharp.ServiceDefaults/**",
6262
"ChatWithCustomData-CSharp.Web/Program.Aspire.cs",
63-
"ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs",
63+
"ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs",
6464
"README.Aspire.md",
6565
"*.sln"
6666
]
6767
},
6868
{
6969
"condition": "(IsAspire && !IsOpenAI && !IsAzureOpenAI)",
7070
"exclude": [
71-
"ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs"
71+
"ChatWithCustomData-CSharp.Web/AspireOpenAIClientBuilderResponsesChatClientExtensions.cs"
7272
]
7373
},
7474
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using Microsoft.Extensions.AI;
2+
using Aspire.OpenAI;
3+
using OpenAI;
4+
using System.Data.Common;
5+
6+
namespace ChatWithCustomData_CSharp.Web.Services;
7+
8+
public static class AspireOpenAIClientBuilderResponsesChatClientExtensions
9+
{
10+
private const string DeploymentKey = "Deployment";
11+
private const string ModelKey = "Model";
12+
13+
public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName)
14+
{
15+
ArgumentNullException.ThrowIfNull(builder, "builder");
16+
17+
return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName));
18+
}
19+
private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName)
20+
{
21+
OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService<OpenAIClient>() : services.GetRequiredKeyedService<OpenAIClient>(builder.ServiceKey);
22+
23+
deploymentName ??= GetRequiredDeploymentName(builder);
24+
25+
IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient();
26+
27+
if (builder.DisableTracing)
28+
{
29+
return chatClient;
30+
}
31+
32+
var loggerFactory = services.GetService<ILoggerFactory>();
33+
return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient)));
34+
}
35+
36+
private static string GetRequiredDeploymentName(this AspireOpenAIClientBuilder builder)
37+
{
38+
string? deploymentName = null;
39+
40+
var configuration = builder.HostBuilder.Configuration;
41+
if (configuration.GetConnectionString(builder.ConnectionName) is string connectionString)
42+
{
43+
// The reason we accept either 'Deployment' or 'Model' as the key is because some hosting solutions
44+
// require specific named deployments (Azure Foundry AI) while others may use a generic model name (OpenAI, GitHub Models).
45+
var connectionBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString };
46+
var deploymentValue = ConnectionStringValue(connectionBuilder, DeploymentKey);
47+
var modelValue = ConnectionStringValue(connectionBuilder, ModelKey);
48+
if (deploymentValue is not null && modelValue is not null)
49+
{
50+
throw new InvalidOperationException(
51+
$"The connection string '{builder.ConnectionName}' contains both '{DeploymentKey}' and '{ModelKey}' keys. Either of these may be specified, but not both.");
52+
}
53+
54+
deploymentName = deploymentValue ?? modelValue;
55+
}
56+
57+
if (string.IsNullOrEmpty(deploymentName))
58+
{
59+
var configSection = configuration.GetSection(builder.ConfigurationSectionName);
60+
deploymentName = configSection[DeploymentKey];
61+
}
62+
63+
if (string.IsNullOrEmpty(deploymentName))
64+
{
65+
throw new InvalidOperationException($"The deployment could not be determined. Ensure a '{DeploymentKey}' or '{ModelKey}' value is provided in 'ConnectionStrings:{builder.ConnectionName}', or specify a '{DeploymentKey}' in the '{builder.ConfigurationSectionName}' configuration section, or specify a '{nameof(deploymentName)}' in the call.");
66+
}
67+
68+
return deploymentName;
69+
}
70+
71+
private static string? ConnectionStringValue(DbConnectionStringBuilder connectionString, string key)
72+
=> connectionString.TryGetValue(key, out var value) ? value as string : null;
73+
74+
}

src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Components/Pages/Chat/Chat.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);
6868
currentResponseCancellation = new();
6969
@*#if (IsAzureOpenAI || IsOpenAI)
70-
await foreach (var update in ChatClient.GetStreamingResponseAsync([userMessage], chatOptions, currentResponseCancellation.Token))
70+
await foreach (var update in ChatClient.GetStreamingResponseAsync(userMessage, chatOptions, currentResponseCancellation.Token))
7171
{
7272
messages.AddMessages(update, filter: c => c is not TextContent);
7373
responseText.Text += update.Text;

src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.Aspire.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,17 @@
2727
#else
2828
var openai = builder.AddAzureOpenAIClient("openai");
2929
#endif
30+
#if (IsGHModels)
31+
openai.AddChatClient("gpt-4o-mini")
32+
.UseFunctionInvocation()
33+
.UseOpenTelemetry(configure: c =>
34+
c.EnableSensitiveData = builder.Environment.IsDevelopment());
35+
#else // (IsOpenAI || IsAzureOpenAI)
3036
openai.AddResponsesChatClient("gpt-4o-mini")
3137
.UseFunctionInvocation()
3238
.UseOpenTelemetry(configure: c =>
3339
c.EnableSensitiveData = builder.Environment.IsDevelopment());
40+
#endif
3441
openai.AddEmbeddingGenerator("text-embedding-3-small");
3542
#endif
3643

src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/ResponseClientHelper.cs

Lines changed: 0 additions & 36 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using Microsoft.Extensions.AI;
2+
using Aspire.OpenAI;
3+
using OpenAI;
4+
using System.Data.Common;
5+
6+
namespace aichatweb.Web.Services;
7+
8+
public static class AspireOpenAIClientBuilderResponsesChatClientExtensions
9+
{
10+
private const string DeploymentKey = "Deployment";
11+
private const string ModelKey = "Model";
12+
13+
public static ChatClientBuilder AddResponsesChatClient(this AspireOpenAIClientBuilder builder, string? deploymentName)
14+
{
15+
ArgumentNullException.ThrowIfNull(builder, "builder");
16+
17+
return builder.HostBuilder.Services.AddChatClient((IServiceProvider services) => CreateInnerChatClient(services, builder, deploymentName));
18+
}
19+
private static IChatClient CreateInnerChatClient(IServiceProvider services, AspireOpenAIClientBuilder builder, string? deploymentName)
20+
{
21+
OpenAIClient openAIClient = builder.ServiceKey is null ? services.GetRequiredService<OpenAIClient>() : services.GetRequiredKeyedService<OpenAIClient>(builder.ServiceKey);
22+
23+
deploymentName ??= GetRequiredDeploymentName(builder);
24+
25+
IChatClient chatClient = openAIClient.GetOpenAIResponseClient(deploymentName).AsIChatClient();
26+
27+
if (builder.DisableTracing)
28+
{
29+
return chatClient;
30+
}
31+
32+
var loggerFactory = services.GetService<ILoggerFactory>();
33+
return new OpenTelemetryChatClient(chatClient, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient)));
34+
}
35+
36+
private static string GetRequiredDeploymentName(this AspireOpenAIClientBuilder builder)
37+
{
38+
string? deploymentName = null;
39+
40+
var configuration = builder.HostBuilder.Configuration;
41+
if (configuration.GetConnectionString(builder.ConnectionName) is string connectionString)
42+
{
43+
// The reason we accept either 'Deployment' or 'Model' as the key is because some hosting solutions
44+
// require specific named deployments (Azure Foundry AI) while others may use a generic model name (OpenAI, GitHub Models).
45+
var connectionBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString };
46+
var deploymentValue = ConnectionStringValue(connectionBuilder, DeploymentKey);
47+
var modelValue = ConnectionStringValue(connectionBuilder, ModelKey);
48+
if (deploymentValue is not null && modelValue is not null)
49+
{
50+
throw new InvalidOperationException(
51+
$"The connection string '{builder.ConnectionName}' contains both '{DeploymentKey}' and '{ModelKey}' keys. Either of these may be specified, but not both.");
52+
}
53+
54+
deploymentName = deploymentValue ?? modelValue;
55+
}
56+
57+
if (string.IsNullOrEmpty(deploymentName))
58+
{
59+
var configSection = configuration.GetSection(builder.ConfigurationSectionName);
60+
deploymentName = configSection[DeploymentKey];
61+
}
62+
63+
if (string.IsNullOrEmpty(deploymentName))
64+
{
65+
throw new InvalidOperationException($"The deployment could not be determined. Ensure a '{DeploymentKey}' or '{ModelKey}' value is provided in 'ConnectionStrings:{builder.ConnectionName}', or specify a '{DeploymentKey}' in the '{builder.ConfigurationSectionName}' configuration section, or specify a '{nameof(deploymentName)}' in the call.");
66+
}
67+
68+
return deploymentName;
69+
}
70+
71+
private static string? ConnectionStringValue(DbConnectionStringBuilder connectionString, string key)
72+
=> connectionString.TryGetValue(key, out var value) ? value as string : null;
73+
74+
}

test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
var responseText = new TextContent("");
6767
currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);
6868
currentResponseCancellation = new();
69-
await foreach (var update in ChatClient.GetStreamingResponseAsync([userMessage], chatOptions, currentResponseCancellation.Token))
69+
await foreach (var update in ChatClient.GetStreamingResponseAsync(userMessage, chatOptions, currentResponseCancellation.Token))
7070
{
7171
messages.AddMessages(update, filter: c => c is not TextContent);
7272
responseText.Text += update.Text;

test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/Program.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using aichatweb.Web.Services.Ingestion;
55

66
var builder = WebApplication.CreateBuilder(args);
7-
87
builder.AddServiceDefaults();
98
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
109

test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/ResponseClientHelper.cs

Lines changed: 0 additions & 36 deletions
This file was deleted.

test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
1010

1111
var openai = builder.AddAzureOpenAIClient("openai");
12-
openai.AddResponsesChatClient("gpt-4o-mini")
12+
openai.AddChatClient("gpt-4o-mini")
1313
.UseFunctionInvocation()
1414
.UseOpenTelemetry(configure: c =>
1515
c.EnableSensitiveData = builder.Environment.IsDevelopment());

0 commit comments

Comments
 (0)