Skip to content

Commit d576d5f

Browse files
committed
Add Aspire, client-side agent selection and chat client
This completes the end-to-end by showcasing how the client can inspect available agents exposed via the /agents endpoint from the registered agent catalog, and how the client can subsequently chat to that agent using the standard OpenAI chat client API mapped via responses API compatible endpoint.
1 parent 693f3ae commit d576d5f

29 files changed

+613
-192
lines changed

AI.slnx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
<Platform Name="x64" />
55
<Platform Name="x86" />
66
</Configurations>
7+
<Folder Name="/Sample/">
8+
<Project Path="sample/Aspire/Aspire.csproj" Id="6166be22-a13f-4074-91a9-b0f3b3a6c4fe" />
9+
<Project Path="sample/Client/Client.csproj" />
10+
<Project Path="sample/Server/Server.csproj" Id="34619937-085f-453d-bc12-9ab2d4abccb7" />
11+
</Folder>
712
<Project Path="src/Agents/Agents.csproj" Id="90827430-b415-47d6-aac9-2dbe4911b348" />
813
<Project Path="src/Extensions.CodeAnalysis/Extensions.CodeAnalysis.csproj" />
914
<Project Path="src/Extensions/Extensions.csproj" />
10-
<Project Path="src/SampleChat/SampleChat.csproj" Id="63ca9077-db60-473a-813d-d3bb5befdf35" />
1115
<Project Path="src/Tests/Tests.csproj" />
1216
</Solution>

sample/Aspire/AppHost.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Projects;
2+
3+
var builder = DistributedApplication.CreateBuilder(args);
4+
5+
var server = builder.AddProject<Server>("server");
6+
7+
// For now, we can't really launch a console project and have its terminal shown.
8+
// See https://github.com/dotnet/aspire/issues/8440
9+
//builder.AddProject<Client>("client")
10+
// .WithReference(server)
11+
// // Flow the resolved Server HTTP endpoint to the client config
12+
// .WithEnvironment("ai__clients__chat__endpoint", server.GetEndpoint("http"))
13+
// .WithExternalConsole();
14+
15+
builder.Build().Run();

sample/Aspire/Aspire.csproj

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" />
4+
5+
<PropertyGroup>
6+
<OutputType>Exe</OutputType>
7+
<TargetFramework>net10.0</TargetFramework>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\Client\Client.csproj" />
16+
<ProjectReference Include="..\Server\Server.csproj" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"https": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "https://localhost:17198;http://localhost:15055",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development",
11+
"DOTNET_ENVIRONMENT": "Development",
12+
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21263",
13+
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22169"
14+
}
15+
},
16+
"http": {
17+
"commandName": "Project",
18+
"dotnetRunMessages": true,
19+
"launchBrowser": true,
20+
"applicationUrl": "http://localhost:15055",
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development",
23+
"DOTNET_ENVIRONMENT": "Development",
24+
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19208",
25+
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20046"
26+
}
27+
}
28+
}
29+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}

sample/Aspire/appsettings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning",
6+
"Aspire.Hosting.Dcp": "Warning"
7+
}
8+
}
9+
}

sample/Client/Client.csproj

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
10+
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
11+
<PackageReference Include="Smith" Version="0.2.5" />
12+
<PackageReference Include="Spectre.Console" Version="0.52.0" />
13+
<PackageReference Include="Spectre.Console.Json" Version="0.52.0" />
14+
<PackageReference Include="DotNetEnv" Version="3.1.1" />
15+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
16+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
17+
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
18+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
19+
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.1" />
20+
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.6" />
21+
<PackageReference Include="ThisAssembly.Project" Version="2.1.2" PrivateAssets="all" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\..\src\Extensions\Extensions.csproj" />
26+
<ProjectReference Include="..\..\src\Extensions.CodeAnalysis\Extensions.CodeAnalysis.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
27+
</ItemGroup>
28+
29+
</Project>

sample/Client/Program.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Net.Http.Json;
2+
using System.Text.Json.Serialization;
3+
using Devlooped.Extensions.AI.OpenAI;
4+
using OpenTelemetry.Metrics;
5+
using OpenTelemetry.Trace;
6+
using Spectre.Console;
7+
8+
var builder = App.CreateBuilder(args);
9+
#if DEBUG
10+
builder.Environment.EnvironmentName = Environments.Development;
11+
#endif
12+
13+
builder.AddServiceDefaults();
14+
builder.Services.AddHttpClient();
15+
16+
var app = builder.Build(async (IServiceProvider services, CancellationToken cancellation) =>
17+
{
18+
var baseUrl = Environment.GetEnvironmentVariable("applicationUrl") ?? "http://localhost:5117";
19+
var http = services.GetRequiredService<IHttpClientFactory>().CreateClient();
20+
var agents = await http.GetFromJsonAsync<AgentCard[]>($"{baseUrl}/agents", cancellation) ?? [];
21+
22+
if (agents.Length == 0)
23+
{
24+
AnsiConsole.MarkupLine(":warning: No agents available");
25+
return;
26+
}
27+
28+
var selectedAgent = AnsiConsole.Prompt(new SelectionPrompt<AgentCard>()
29+
.Title("Select agent:")
30+
.UseConverter(a => $"{a.Name}: {a.Description ?? ""}")
31+
.AddChoices(agents));
32+
33+
var chat = new OpenAIChatClient("none", "default", new OpenAI.OpenAIClientOptions
34+
{
35+
Endpoint = new Uri($"{baseUrl}/{selectedAgent.Name}/v1")
36+
}).AsBuilder().UseOpenTelemetry().UseJsonConsoleLogging().Build(services);
37+
38+
var history = new List<ChatMessage>();
39+
40+
AnsiConsole.MarkupLine($":robot: Ready");
41+
AnsiConsole.Markup($":person_beard: ");
42+
while (!cancellation.IsCancellationRequested)
43+
{
44+
var input = Console.ReadLine()?.Trim();
45+
if (string.IsNullOrEmpty(input))
46+
continue;
47+
48+
history.Add(new ChatMessage(ChatRole.User, input));
49+
try
50+
{
51+
var response = await AnsiConsole.Status().StartAsync(":robot: Thinking...", ctx => chat.GetResponseAsync(input));
52+
history.AddRange(response.Messages);
53+
try
54+
{
55+
// Try rendering as formatted markup
56+
if (response.Text is { Length: > 0 })
57+
AnsiConsole.MarkupLine($":robot: {response.Text}");
58+
}
59+
catch (Exception)
60+
{
61+
// Fallback to escaped markup text if rendering fails
62+
AnsiConsole.MarkupLineInterpolated($":robot: {response.Text}");
63+
}
64+
AnsiConsole.Markup($":person_beard: ");
65+
}
66+
catch (Exception e)
67+
{
68+
AnsiConsole.WriteException(e);
69+
}
70+
}
71+
72+
AnsiConsole.MarkupLine($":robot: Shutting down...");
73+
});
74+
75+
Console.WriteLine("Powered by Smith");
76+
77+
await app.RunAsync();
78+
79+
record AgentCard(string Name, string? Description);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"profiles": {
3+
"Client": {
4+
"commandName": "Project",
5+
"environmentVariables": {
6+
"applicationUrl": "http://localhost:5117"
7+
}
8+
}
9+
}
10+
}

sample/Client/appsettings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"AI": {
3+
"Clients": {
4+
"Chat": {
5+
"ApiKey": "dev",
6+
"ModelId": "default",
7+
"Endpoint": "http://localhost:5117/notes/v1"
8+
}
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)