test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace AgentWebChat.AgentHost;
|
||||
|
||||
internal static class ActorFrameworkWebApplicationExtensions
|
||||
{
|
||||
public static void MapAgentDiscovery(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string path)
|
||||
{
|
||||
var registeredAIAgents = endpoints.ServiceProvider.GetKeyedServices<AIAgent>(KeyedService.AnyKey);
|
||||
|
||||
var routeGroup = endpoints.MapGroup(path);
|
||||
routeGroup.MapGet("/", async (CancellationToken cancellationToken) =>
|
||||
{
|
||||
var results = new List<AgentDiscoveryCard>();
|
||||
foreach (var result in registeredAIAgents)
|
||||
{
|
||||
results.Add(new AgentDiscoveryCard
|
||||
{
|
||||
Name = result.Name!,
|
||||
Description = result.Description,
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(results);
|
||||
})
|
||||
.WithName("GetAgents");
|
||||
}
|
||||
|
||||
internal sealed class AgentDiscoveryCard
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.DevUI\Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.OpenAI\Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\AgentWebChat.ServiceDefaults\AgentWebChat.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Aspire.Hosting.Azure.CognitiveServices" />
|
||||
<PackageReference Include="Aspire.Microsoft.Azure.Cosmos" />
|
||||
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenAPI" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentWebChat.AgentHost.Custom;
|
||||
|
||||
public class CustomAITool : AITool;
|
||||
|
||||
public class CustomFunctionTool : AIFunction
|
||||
{
|
||||
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<object?>(arguments.Context?.Count ?? 0);
|
||||
}
|
||||
}
|
||||
181
dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
Normal file
181
dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using A2A.AspNetCore;
|
||||
using AgentWebChat.AgentHost;
|
||||
using AgentWebChat.AgentHost.Custom;
|
||||
using AgentWebChat.AgentHost.Utilities;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DevUI;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add service defaults & Aspire client integrations.
|
||||
builder.AddServiceDefaults();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddProblemDetails();
|
||||
|
||||
// Configure the chat model and our agent.
|
||||
builder.AddKeyedChatClient("chat-model");
|
||||
|
||||
// Add DevUI services
|
||||
builder.AddDevUI();
|
||||
|
||||
// Add OpenAI services
|
||||
builder.AddOpenAIChatCompletions();
|
||||
builder.AddOpenAIResponses();
|
||||
|
||||
var pirateAgentBuilder = builder.AddAIAgent(
|
||||
"pirate",
|
||||
instructions: "You are a pirate. Speak like a pirate",
|
||||
description: "An agent that speaks like a pirate.",
|
||||
chatClientServiceKey: "chat-model")
|
||||
.WithAITool(new CustomAITool())
|
||||
.WithAITool(new CustomFunctionTool())
|
||||
.WithInMemoryThreadStore();
|
||||
|
||||
var knightsKnavesAgentBuilder = builder.AddAIAgent("knights-and-knaves", (sp, key) =>
|
||||
{
|
||||
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
|
||||
|
||||
ChatClientAgent knight = new(
|
||||
chatClient,
|
||||
"""
|
||||
You are a knight. This means that you must always tell the truth. Your name is Alice.
|
||||
Bob is standing next to you. Bob is a knave, which means he always lies.
|
||||
When replying, always start with your name (Alice). Eg, "Alice: I am a knight."
|
||||
""", "Alice");
|
||||
|
||||
ChatClientAgent knave = new(
|
||||
chatClient,
|
||||
"""
|
||||
You are a knave. This means that you must always lie. Your name is Bob.
|
||||
Alice is standing next to you. Alice is a knight, which means she always tells the truth.
|
||||
When replying, always include your name (Bob). Eg, "Bob: I am a knight."
|
||||
""", "Bob");
|
||||
|
||||
ChatClientAgent narrator = new(
|
||||
chatClient,
|
||||
"""
|
||||
You are are the narrator of a puzzle involving knights (who always tell the truth) and knaves (who always lie).
|
||||
The user is going to ask questions and guess whether Alice or Bob is the knight or knave.
|
||||
Alice is standing to one side of you. Alice is a knight, which means she always tells the truth.
|
||||
Bob is standing to the other side of you. Bob is a knave, which means he always lies.
|
||||
When replying, always include your name (Narrator).
|
||||
Once the user has deduced what type (knight or knave) both Alice and Bob are, tell them whether they are right or wrong.
|
||||
If the user asks a general question about their surrounding, make something up which is consistent with the scenario.
|
||||
""", "Narrator");
|
||||
|
||||
return AgentWorkflowBuilder.BuildConcurrent([knight, knave, narrator]).AsAgent(name: key);
|
||||
});
|
||||
|
||||
// Workflow consisting of multiple specialized agents
|
||||
var chemistryAgent = builder.AddAIAgent("chemist",
|
||||
instructions: "You are a chemistry expert. Answer thinking from the chemistry perspective",
|
||||
description: "An agent that helps with chemistry.",
|
||||
chatClientServiceKey: "chat-model");
|
||||
|
||||
var mathsAgent = builder.AddAIAgent("mathematician",
|
||||
instructions: "You are a mathematics expert. Answer thinking from the maths perspective",
|
||||
description: "An agent that helps with mathematics.",
|
||||
chatClientServiceKey: "chat-model");
|
||||
|
||||
var literatureAgent = builder.AddAIAgent("literator",
|
||||
instructions: "You are a literature expert. Answer thinking from the literature perspective",
|
||||
description: "An agent that helps with literature.",
|
||||
chatClientServiceKey: "chat-model");
|
||||
|
||||
var scienceSequentialWorkflow = builder.AddWorkflow("science-sequential-workflow", (sp, key) =>
|
||||
{
|
||||
List<IHostedAgentBuilder> usedAgents = [chemistryAgent, mathsAgent, literatureAgent];
|
||||
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
|
||||
return AgentWorkflowBuilder.BuildSequential(workflowName: key, agents: agents);
|
||||
}).AddAsAIAgent();
|
||||
|
||||
var scienceConcurrentWorkflow = builder.AddWorkflow("science-concurrent-workflow", (sp, key) =>
|
||||
{
|
||||
List<IHostedAgentBuilder> usedAgents = [chemistryAgent, mathsAgent, literatureAgent];
|
||||
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
|
||||
return AgentWorkflowBuilder.BuildConcurrent(workflowName: key, agents: agents);
|
||||
}).AddAsAIAgent();
|
||||
|
||||
builder.AddWorkflow("nonAgentWorkflow", (sp, key) =>
|
||||
{
|
||||
List<IHostedAgentBuilder> usedAgents = [pirateAgentBuilder, chemistryAgent];
|
||||
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
|
||||
return AgentWorkflowBuilder.BuildSequential(workflowName: key, agents: agents);
|
||||
});
|
||||
|
||||
builder.Services.AddKeyedSingleton("NonAgentAndNonmatchingDINameWorkflow", (sp, key) =>
|
||||
{
|
||||
List<IHostedAgentBuilder> usedAgents = [pirateAgentBuilder, chemistryAgent];
|
||||
var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService<AIAgent>(ab.Name));
|
||||
return AgentWorkflowBuilder.BuildSequential(workflowName: "random-name", agents: agents);
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<AIAgent>(sp =>
|
||||
{
|
||||
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
|
||||
return new ChatClientAgent(chatClient, name: "default-agent", instructions: "you are a default agent.");
|
||||
});
|
||||
|
||||
builder.Services.AddKeyedSingleton<AIAgent>("my-di-nonmatching-agent", (sp, name) =>
|
||||
{
|
||||
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
|
||||
return new ChatClientAgent(
|
||||
chatClient,
|
||||
name: "some-random-name", // demonstrating registration can be different for DI and actual agent
|
||||
instructions: "you are a dependency inject agent. Tell me all about dependency injection.");
|
||||
});
|
||||
|
||||
builder.Services.AddKeyedSingleton<AIAgent>("my-di-matchingname-agent", (sp, name) =>
|
||||
{
|
||||
if (name is not string nameStr)
|
||||
{
|
||||
throw new NotSupportedException("Name should be passed as a key");
|
||||
}
|
||||
|
||||
var chatClient = sp.GetRequiredKeyedService<IChatClient>("chat-model");
|
||||
return new ChatClientAgent(
|
||||
chatClient,
|
||||
name: nameStr, // demonstrating registration with the same name
|
||||
instructions: "you are a dependency inject agent. Tell me all about dependency injection.");
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapOpenApi();
|
||||
app.UseSwaggerUI(options => options.SwaggerEndpoint("/openapi/v1.json", "Agents API"));
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
app.UseExceptionHandler();
|
||||
|
||||
// attach a2a with simple message communication
|
||||
app.MapA2A(pirateAgentBuilder, path: "/a2a/pirate");
|
||||
app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard: new()
|
||||
{
|
||||
Name = "Knights and Knaves",
|
||||
Description = "An agent that helps you solve the knights and knaves puzzle.",
|
||||
Version = "1.0",
|
||||
|
||||
// Url can be not set, and SDK will help assign it.
|
||||
// Url = "http://localhost:5390/a2a/knights-and-knaves"
|
||||
});
|
||||
|
||||
app.MapDevUI();
|
||||
|
||||
app.MapOpenAIResponses();
|
||||
app.MapOpenAIConversations();
|
||||
|
||||
app.MapOpenAIChatCompletions(pirateAgentBuilder);
|
||||
app.MapOpenAIChatCompletions(knightsKnavesAgentBuilder);
|
||||
|
||||
// Map the agents HTTP endpoints
|
||||
app.MapAgentDiscovery("/agents");
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
app.Run();
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5390",
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7373;http://localhost:5390",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace AgentWebChat.AgentHost.Utilities;
|
||||
|
||||
public class ChatClientConnectionInfo
|
||||
{
|
||||
public Uri? Endpoint { get; init; }
|
||||
public required string SelectedModel { get; init; }
|
||||
|
||||
public ClientChatProvider Provider { get; init; }
|
||||
public string? AccessKey { get; init; }
|
||||
|
||||
// Example connection string:
|
||||
// Endpoint=https://localhost:4523;Model=phi3.5;AccessKey=1234;Provider=ollama;
|
||||
public static bool TryParse(string? connectionString, [NotNullWhen(true)] out ChatClientConnectionInfo? settings)
|
||||
{
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var connectionBuilder = new DbConnectionStringBuilder
|
||||
{
|
||||
ConnectionString = connectionString
|
||||
};
|
||||
|
||||
Uri? endpoint = null;
|
||||
if (connectionBuilder.ContainsKey("Endpoint") && Uri.TryCreate(connectionBuilder["Endpoint"].ToString(), UriKind.Absolute, out endpoint))
|
||||
{
|
||||
}
|
||||
|
||||
string? model = null;
|
||||
if (connectionBuilder.ContainsKey("Model"))
|
||||
{
|
||||
model = (string)connectionBuilder["Model"];
|
||||
}
|
||||
|
||||
string? accessKey = null;
|
||||
if (connectionBuilder.ContainsKey("AccessKey"))
|
||||
{
|
||||
accessKey = (string)connectionBuilder["AccessKey"];
|
||||
}
|
||||
|
||||
var provider = ClientChatProvider.Unknown;
|
||||
if (connectionBuilder.ContainsKey("Provider"))
|
||||
{
|
||||
var providerValue = (string)connectionBuilder["Provider"];
|
||||
Enum.TryParse(providerValue, ignoreCase: true, out provider);
|
||||
}
|
||||
|
||||
if ((endpoint is null && provider != ClientChatProvider.OpenAI) || model is null || provider is ClientChatProvider.Unknown)
|
||||
{
|
||||
settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
settings = new ChatClientConnectionInfo
|
||||
{
|
||||
Endpoint = endpoint,
|
||||
SelectedModel = model,
|
||||
AccessKey = accessKey,
|
||||
Provider = provider
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ClientChatProvider
|
||||
{
|
||||
Unknown,
|
||||
Ollama,
|
||||
OpenAI,
|
||||
AzureOpenAI,
|
||||
AzureAIInference,
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AgentWebChat.AgentHost.Utilities;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OllamaSharp;
|
||||
|
||||
namespace AgentWebChat.AgentHost.Utilities;
|
||||
|
||||
public static class ChatClientExtensions
|
||||
{
|
||||
public static ChatClientBuilder AddChatClient(this IHostApplicationBuilder builder, string connectionName)
|
||||
{
|
||||
var cs = builder.Configuration.GetConnectionString(connectionName);
|
||||
|
||||
if (!ChatClientConnectionInfo.TryParse(cs, out var connectionInfo))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid connection string: {cs}. Expected format: 'Endpoint=endpoint;AccessKey=your_access_key;Model=model_name;Provider=ollama/openai/azureopenai;'.");
|
||||
}
|
||||
|
||||
var chatClientBuilder = connectionInfo.Provider switch
|
||||
{
|
||||
ClientChatProvider.Ollama => builder.AddOllamaClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.OpenAI => builder.AddOpenAIClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.AzureOpenAI => builder.AddAzureOpenAIClient(connectionName).AddChatClient(connectionInfo.SelectedModel),
|
||||
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
|
||||
};
|
||||
|
||||
// Add OpenTelemetry tracing for the ChatClient activity source
|
||||
chatClientBuilder.UseOpenTelemetry().UseLogging();
|
||||
|
||||
builder.Services.AddOpenTelemetry().WithTracing(t => t.AddSource("Experimental.Microsoft.Extensions.AI"));
|
||||
|
||||
return chatClientBuilder;
|
||||
}
|
||||
|
||||
private static ChatClientBuilder AddOpenAIClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
|
||||
builder.AddOpenAIClient(connectionName, settings =>
|
||||
{
|
||||
settings.Endpoint = connectionInfo.Endpoint;
|
||||
settings.Key = connectionInfo.AccessKey;
|
||||
})
|
||||
.AddChatClient(connectionInfo.SelectedModel);
|
||||
|
||||
private static ChatClientBuilder AddOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
var httpKey = $"{connectionName}_http";
|
||||
|
||||
builder.Services.AddHttpClient(httpKey, c => c.BaseAddress = connectionInfo.Endpoint);
|
||||
|
||||
return builder.Services.AddChatClient(sp =>
|
||||
{
|
||||
// Create a client for the Ollama API using the http client factory
|
||||
var client = sp.GetRequiredService<IHttpClientFactory>().CreateClient(httpKey);
|
||||
|
||||
return new OllamaApiClient(client, connectionInfo.SelectedModel);
|
||||
});
|
||||
}
|
||||
|
||||
public static ChatClientBuilder AddKeyedChatClient(this IHostApplicationBuilder builder, string connectionName)
|
||||
{
|
||||
var cs = builder.Configuration.GetConnectionString(connectionName);
|
||||
|
||||
if (!ChatClientConnectionInfo.TryParse(cs, out var connectionInfo))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid connection string: {cs}. Expected format: 'Endpoint=endpoint;AccessKey=your_access_key;Model=model_name;Provider=ollama/openai/azureopenai;'.");
|
||||
}
|
||||
|
||||
var chatClientBuilder = connectionInfo.Provider switch
|
||||
{
|
||||
ClientChatProvider.Ollama => builder.AddKeyedOllamaClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.OpenAI => builder.AddKeyedOpenAIClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.AzureOpenAI => builder.AddKeyedAzureOpenAIClient(connectionName).AddKeyedChatClient(connectionName, connectionInfo.SelectedModel),
|
||||
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
|
||||
};
|
||||
|
||||
// Add OpenTelemetry tracing for the ChatClient activity source
|
||||
chatClientBuilder.UseOpenTelemetry().UseLogging();
|
||||
|
||||
builder.Services.AddOpenTelemetry().WithTracing(t => t.AddSource("Experimental.Microsoft.Extensions.AI"));
|
||||
|
||||
return chatClientBuilder;
|
||||
}
|
||||
|
||||
private static ChatClientBuilder AddKeyedOpenAIClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
|
||||
builder.AddKeyedOpenAIClient(connectionName, settings =>
|
||||
{
|
||||
settings.Endpoint = connectionInfo.Endpoint;
|
||||
settings.Key = connectionInfo.AccessKey;
|
||||
})
|
||||
.AddKeyedChatClient(connectionName, connectionInfo.SelectedModel);
|
||||
|
||||
private static ChatClientBuilder AddKeyedOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
var httpKey = $"{connectionName}_http";
|
||||
|
||||
builder.Services.AddHttpClient(httpKey, c => c.BaseAddress = connectionInfo.Endpoint);
|
||||
|
||||
return builder.Services.AddKeyedChatClient(connectionName, sp =>
|
||||
{
|
||||
// Create a client for the Ollama API using the http client factory
|
||||
var client = sp.GetRequiredService<IHttpClientFactory>().CreateClient(httpKey);
|
||||
|
||||
return new OllamaApiClient(client, connectionInfo.SelectedModel);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspireAppHostSdkVersion)" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireHost>true</IsAspireHost>
|
||||
<UserSecretsId>2969a84d-8ee6-4304-8737-6e469a315aa8</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" />
|
||||
<PackageReference Include="Aspire.Hosting.Azure.CognitiveServices" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AgentWebChat.AgentHost\AgentWebChat.AgentHost.csproj" />
|
||||
<ProjectReference Include="..\AgentWebChat.Web\AgentWebChat.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AgentWebChat.AppHost;
|
||||
|
||||
public static class ModelExtensions
|
||||
{
|
||||
public static IResourceBuilder<AIModel> AddAIModel(this IDistributedApplicationBuilder builder, string name)
|
||||
{
|
||||
var model = new AIModel(name);
|
||||
return builder.CreateResourceBuilder(model);
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> RunAsOpenAI(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
|
||||
{
|
||||
return builder.AsOpenAI(modelName, apiKey);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> PublishAsOpenAI(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
|
||||
{
|
||||
return builder.AsOpenAI(modelName, apiKey);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> RunAsAzureOpenAI(this IResourceBuilder<AIModel> builder, string modelName, Action<IResourceBuilder<AzureOpenAIResource>>? configure)
|
||||
{
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
|
||||
{
|
||||
return builder.AsAzureOpenAI(modelName, configure);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> PublishAsAzureOpenAI(this IResourceBuilder<AIModel> builder, string modelName, Action<IResourceBuilder<AzureOpenAIResource>>? configure)
|
||||
{
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
|
||||
{
|
||||
return builder.AsAzureOpenAI(modelName, configure);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> AsAzureOpenAI(this IResourceBuilder<AIModel> builder, string modelName, Action<IResourceBuilder<AzureOpenAIResource>>? configure)
|
||||
{
|
||||
builder.Reset();
|
||||
|
||||
var openAIModel = builder.ApplicationBuilder.AddAzureOpenAI(builder.Resource.Name);
|
||||
|
||||
configure?.Invoke(openAIModel);
|
||||
|
||||
builder.Resource.UnderlyingResource = openAIModel.Resource;
|
||||
// Add the model name to the connection string
|
||||
builder.Resource.ConnectionString = ReferenceExpression.Create($"{openAIModel.Resource.ConnectionStringExpression};Model={modelName}");
|
||||
builder.Resource.Provider = "AzureOpenAI";
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> RunAsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> endpoint, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
|
||||
{
|
||||
return builder.AsAzureAIInference(modelName, endpoint, apiKey);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> PublishAsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> endpoint, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
|
||||
{
|
||||
return builder.AsAzureAIInference(modelName, endpoint, apiKey);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> AsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> endpoint, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
builder.Reset();
|
||||
|
||||
// See: https://github.com/dotnet/aspire/issues/7641
|
||||
var csb = new ReferenceExpressionBuilder();
|
||||
csb.Append($"Endpoint={endpoint.Resource};");
|
||||
csb.Append($"AccessKey={apiKey.Resource};");
|
||||
csb.Append($"Model={modelName}");
|
||||
var cs = csb.Build();
|
||||
|
||||
builder.ApplicationBuilder.AddResource(builder.Resource);
|
||||
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
|
||||
{
|
||||
var csTask = cs.GetValueAsync(default).AsTask();
|
||||
if (!csTask.IsCompletedSuccessfully)
|
||||
{
|
||||
throw new InvalidOperationException("Connection string could not be resolved!");
|
||||
}
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
builder.WithInitialState(new CustomResourceSnapshot
|
||||
{
|
||||
ResourceType = "Azure AI Inference Model",
|
||||
State = KnownResourceStates.Running,
|
||||
Properties = [
|
||||
new("ConnectionString", csTask.Result ) { IsSensitive = true }
|
||||
]
|
||||
});
|
||||
#pragma warning restore VSTHRD002
|
||||
}
|
||||
|
||||
builder.Resource.UnderlyingResource = builder.Resource;
|
||||
builder.Resource.ConnectionString = cs;
|
||||
builder.Resource.Provider = "AzureAIInference";
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> RunAsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, string endpoint, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
|
||||
{
|
||||
return builder.AsAzureAIInference(modelName, endpoint, apiKey);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> PublishAsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, string endpoint, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
|
||||
{
|
||||
return builder.AsAzureAIInference(modelName, endpoint, apiKey);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> AsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, string endpoint, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
builder.Reset();
|
||||
|
||||
// See: https://github.com/dotnet/aspire/issues/7641
|
||||
var csb = new ReferenceExpressionBuilder();
|
||||
csb.Append($"Endpoint={endpoint};");
|
||||
csb.Append($"AccessKey={apiKey.Resource};");
|
||||
csb.Append($"Model={modelName}");
|
||||
var cs = csb.Build();
|
||||
|
||||
builder.ApplicationBuilder.AddResource(builder.Resource);
|
||||
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
|
||||
{
|
||||
var csTask = cs.GetValueAsync(default).AsTask();
|
||||
if (!csTask.IsCompletedSuccessfully)
|
||||
{
|
||||
throw new InvalidOperationException("Connection string could not be resolved!");
|
||||
}
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
builder.WithInitialState(new CustomResourceSnapshot
|
||||
{
|
||||
ResourceType = "Azure AI Inference Model",
|
||||
State = KnownResourceStates.Running,
|
||||
Properties = [
|
||||
new("ConnectionString", csTask.Result ) { IsSensitive = true }
|
||||
]
|
||||
});
|
||||
#pragma warning restore VSTHRD002
|
||||
}
|
||||
|
||||
builder.Resource.UnderlyingResource = builder.Resource;
|
||||
builder.Resource.ConnectionString = cs;
|
||||
builder.Resource.Provider = "AzureAIInference";
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IResourceBuilder<AIModel> AsOpenAI(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> apiKey)
|
||||
{
|
||||
builder.Reset();
|
||||
|
||||
// See: https://github.com/dotnet/aspire/issues/7641
|
||||
var csb = new ReferenceExpressionBuilder();
|
||||
csb.Append($"AccessKey={apiKey.Resource};");
|
||||
csb.Append($"Model={modelName}");
|
||||
var cs = csb.Build();
|
||||
|
||||
builder.ApplicationBuilder.AddResource(builder.Resource);
|
||||
|
||||
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
|
||||
{
|
||||
var csTask = cs.GetValueAsync(default).AsTask();
|
||||
if (!csTask.IsCompletedSuccessfully)
|
||||
{
|
||||
throw new InvalidOperationException("Connection string could not be resolved!");
|
||||
}
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
builder.WithInitialState(new CustomResourceSnapshot
|
||||
{
|
||||
ResourceType = "OpenAI Model",
|
||||
State = KnownResourceStates.Running,
|
||||
Properties = [
|
||||
new("ConnectionString", csTask.Result ) { IsSensitive = true }
|
||||
]
|
||||
});
|
||||
#pragma warning restore VSTHRD002
|
||||
}
|
||||
|
||||
builder.Resource.UnderlyingResource = builder.Resource;
|
||||
builder.Resource.ConnectionString = cs;
|
||||
builder.Resource.Provider = "OpenAI";
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void Reset(this IResourceBuilder<AIModel> builder)
|
||||
{
|
||||
// Reset the properties of the AIModel resource
|
||||
if (builder.Resource.UnderlyingResource is { } underlyingResource)
|
||||
{
|
||||
builder.ApplicationBuilder.Resources.Remove(underlyingResource);
|
||||
|
||||
if (underlyingResource is IResourceWithParent resourceWithParent)
|
||||
{
|
||||
builder.ApplicationBuilder.Resources.Remove(resourceWithParent.Parent);
|
||||
}
|
||||
}
|
||||
|
||||
builder.Resource.ConnectionString = null;
|
||||
builder.Resource.Provider = null;
|
||||
}
|
||||
}
|
||||
|
||||
// A resource representing an AI model.
|
||||
public class AIModel(string name) : Resource(name), IResourceWithConnectionString
|
||||
{
|
||||
internal string? Provider { get; set; }
|
||||
internal IResourceWithConnectionString? UnderlyingResource { get; set; }
|
||||
internal ReferenceExpression? ConnectionString { get; set; }
|
||||
|
||||
public ReferenceExpression ConnectionStringExpression =>
|
||||
this.Build();
|
||||
|
||||
public ReferenceExpression Build()
|
||||
{
|
||||
var connectionString = this.ConnectionString ?? throw new InvalidOperationException("No connection string available.");
|
||||
|
||||
if (this.Provider is null)
|
||||
{
|
||||
throw new InvalidOperationException("No provider configured.");
|
||||
}
|
||||
|
||||
return ReferenceExpression.Create($"{connectionString};Provider={this.Provider}");
|
||||
}
|
||||
}
|
||||
21
dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs
Normal file
21
dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AgentWebChat.AppHost;
|
||||
|
||||
var builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
var azOpenAiResource = builder.AddParameterFromConfiguration("AzureOpenAIName", "AzureOpenAI:Name");
|
||||
var azOpenAiResourceGroup = builder.AddParameterFromConfiguration("AzureOpenAIResourceGroup", "AzureOpenAI:ResourceGroup");
|
||||
var chatModel = builder.AddAIModel("chat-model").AsAzureOpenAI("gpt-4o", o => o.AsExisting(azOpenAiResource, azOpenAiResourceGroup));
|
||||
|
||||
var agentHost = builder.AddProject<Projects.AgentWebChat_AgentHost>("agenthost")
|
||||
.WithHttpEndpoint(name: "devui")
|
||||
.WithUrlForEndpoint("devui", (url) => new() { Url = "/devui", DisplayText = "Dev UI" })
|
||||
.WithReference(chatModel);
|
||||
|
||||
builder.AddProject<Projects.AgentWebChat_Web>("webfrontend")
|
||||
.WithExternalHttpEndpoints()
|
||||
.WithReference(agentHost)
|
||||
.WaitFor(agentHost);
|
||||
|
||||
builder.Build().Run();
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:17277;http://localhost:15143",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21000",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22278"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:15143",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19242",
|
||||
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20010"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Aspire.Hosting.Dcp": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireSharedProject>true</IsAspireSharedProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace Microsoft.Extensions.Hosting;
|
||||
|
||||
// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
|
||||
// This project should be referenced by each service project in your solution.
|
||||
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
|
||||
public static class ServiceDefaultsExtensions
|
||||
{
|
||||
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Trace);
|
||||
builder.ConfigureOpenTelemetry();
|
||||
|
||||
builder.AddDefaultHealthChecks();
|
||||
|
||||
builder.Services.AddServiceDiscovery();
|
||||
|
||||
builder.Services.ConfigureHttpClientDefaults(http =>
|
||||
{
|
||||
// Turn on resilience by default
|
||||
http.AddStandardResilienceHandler();
|
||||
|
||||
// Turn on service discovery by default
|
||||
http.AddServiceDiscovery();
|
||||
});
|
||||
|
||||
// Uncomment the following to restrict the allowed schemes for service discovery.
|
||||
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
|
||||
// {
|
||||
// options.AllowedSchemes = ["https"];
|
||||
// });
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.IncludeFormattedMessage = true;
|
||||
logging.IncludeScopes = true;
|
||||
});
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation();
|
||||
})
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing.AddSource(builder.Environment.ApplicationName)
|
||||
.AddSource("*Microsoft.Agents.AI")
|
||||
.AddSource("Microsoft.Agents.AI.Runtime.InProcess")
|
||||
.AddSource("Microsoft.Agents.AI.Runtime.Abstractions.InMemoryActorStateStorage")
|
||||
.AddAspNetCoreInstrumentation()
|
||||
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
|
||||
//.AddGrpcClientInstrumentation()
|
||||
.AddHttpClientInstrumentation();
|
||||
});
|
||||
|
||||
builder.AddOpenTelemetryExporters();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
|
||||
|
||||
if (useOtlpExporter)
|
||||
{
|
||||
builder.Services.AddOpenTelemetry().UseOtlpExporter();
|
||||
}
|
||||
|
||||
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
|
||||
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
|
||||
//{
|
||||
// builder.Services.AddOpenTelemetry()
|
||||
// .UseAzureMonitor();
|
||||
//}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Services.AddHealthChecks()
|
||||
// Add a default liveness check to ensure app is responsive
|
||||
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplication MapDefaultEndpoints(this WebApplication app)
|
||||
{
|
||||
// Adding health checks endpoints to applications in non-development environments has security implications.
|
||||
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
// All health checks must pass for app to be considered ready to accept traffic after starting
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
// Only health checks tagged with the "live" tag must pass for app to be considered alive
|
||||
app.MapHealthChecks("/alive", new HealthCheckOptions
|
||||
{
|
||||
Predicate = r => r.Tags.Contains("live")
|
||||
});
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
168
dotnet/samples/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs
Normal file
168
dotnet/samples/AgentWebChat/AgentWebChat.Web/A2AAgentClient.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentWebChat.Web;
|
||||
|
||||
internal sealed class A2AAgentClient : AgentClientBase
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly Uri _uri;
|
||||
|
||||
// because A2A sdk does not provide a client which can handle multiple agents, we need a client per agent
|
||||
// for this app the convention is "baseUri/<agentname>"
|
||||
private readonly ConcurrentDictionary<string, (A2AClient, A2ACardResolver)> _clients = [];
|
||||
|
||||
public A2AAgentClient(ILogger logger, Uri baseUri)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._uri = baseUri;
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? threadId = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._logger.LogInformation("Running agent {AgentName} with {MessageCount} messages via A2A", agentName, messages.Count);
|
||||
|
||||
var (a2aClient, _) = this.ResolveClient(agentName);
|
||||
var contextId = threadId ?? Guid.NewGuid().ToString("N");
|
||||
|
||||
// Convert and send messages via A2A without try-catch in yield method
|
||||
var results = new List<AgentResponseUpdate>();
|
||||
|
||||
try
|
||||
{
|
||||
// Convert all messages to A2A parts and create a single message
|
||||
var parts = messages.ToParts();
|
||||
var a2aMessage = new AgentMessage
|
||||
{
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
ContextId = contextId,
|
||||
Role = MessageRole.User,
|
||||
Parts = parts
|
||||
};
|
||||
|
||||
var messageSendParams = new MessageSendParams { Message = a2aMessage };
|
||||
var a2aResponse = await a2aClient.SendMessageAsync(messageSendParams, cancellationToken);
|
||||
|
||||
// Handle different response types
|
||||
if (a2aResponse is AgentMessage message)
|
||||
{
|
||||
var responseMessage = message.ToChatMessage();
|
||||
if (responseMessage is { Contents.Count: > 0 })
|
||||
{
|
||||
results.Add(new AgentResponseUpdate(responseMessage.Role, responseMessage.Contents)
|
||||
{
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (a2aResponse is AgentTask agentTask)
|
||||
{
|
||||
// Manually convert AgentTask artifacts to ChatMessages since the extension method is internal
|
||||
if (agentTask.Artifacts is not null)
|
||||
{
|
||||
foreach (var artifact in agentTask.Artifacts)
|
||||
{
|
||||
List<AIContent>? aiContents = null;
|
||||
|
||||
foreach (var part in artifact.Parts)
|
||||
{
|
||||
(aiContents ??= []).Add(part.ToAIContent());
|
||||
}
|
||||
|
||||
if (aiContents is not null)
|
||||
{
|
||||
var additionalProperties = ConvertMetadataToAdditionalProperties(artifact.Metadata);
|
||||
var chatMessage = new ChatMessage(ChatRole.Assistant, aiContents)
|
||||
{
|
||||
AdditionalProperties = additionalProperties,
|
||||
RawRepresentation = artifact,
|
||||
};
|
||||
|
||||
results.Add(new AgentResponseUpdate(chatMessage.Role, chatMessage.Contents)
|
||||
{
|
||||
MessageId = agentTask.Id,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogWarning("Unsupported A2A response type: {ResponseType}", a2aResponse?.GetType().FullName ?? "null");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error running agent {AgentName} via A2A", agentName);
|
||||
|
||||
results.Add(new AgentResponseUpdate(ChatRole.Assistant, $"Error: {ex.Message}")
|
||||
{
|
||||
MessageId = Guid.NewGuid().ToString("N"),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
// Yield the results
|
||||
foreach (var result in results)
|
||||
{
|
||||
yield return result;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<AgentCard?> GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._logger.LogInformation("Retrieving agent card for {Agent}", agentName);
|
||||
|
||||
var (_, a2aCardResolver) = this.ResolveClient(agentName);
|
||||
try
|
||||
{
|
||||
return await a2aCardResolver.GetAgentCardAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Failed to get agent card for {AgentName}", agentName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private (A2AClient, A2ACardResolver) ResolveClient(string agentName) =>
|
||||
this._clients.GetOrAdd(agentName, name =>
|
||||
{
|
||||
var uri = new Uri($"{this._uri}/{name}/");
|
||||
var a2aClient = new A2AClient(uri);
|
||||
|
||||
// /v1/card is a default path for A2A agent card discovery
|
||||
var a2aCardResolver = new A2ACardResolver(uri, agentCardPath: "/v1/card/");
|
||||
|
||||
this._logger.LogInformation("Built clients for agent {Agent} with baseUri {Uri}", name, uri);
|
||||
return (a2aClient, a2aCardResolver);
|
||||
});
|
||||
|
||||
private static AdditionalPropertiesDictionary? ConvertMetadataToAdditionalProperties(Dictionary<string, JsonElement>? metadata)
|
||||
{
|
||||
if (metadata is not { Count: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var additionalProperties = new AdditionalPropertiesDictionary();
|
||||
foreach (var kvp in metadata)
|
||||
{
|
||||
additionalProperties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
return additionalProperties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AgentWebChat.Web;
|
||||
|
||||
public class AgentDiscoveryClient(HttpClient httpClient, ILogger<AgentDiscoveryClient> logger)
|
||||
{
|
||||
public async Task<List<AgentDiscoveryCard>> GetAgentsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await httpClient.GetAsync(new Uri("/agents", UriKind.Relative), cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var agents = JsonSerializer.Deserialize<List<AgentDiscoveryCard>>(json) ?? [];
|
||||
|
||||
logger.LogInformation("Retrieved {AgentCount} agents from the API", agents.Count);
|
||||
return agents;
|
||||
}
|
||||
|
||||
public class AgentDiscoveryCard
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);CA1812</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.A2A\Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.OpenAI\Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<ProjectReference Include="..\AgentWebChat.ServiceDefaults\AgentWebChat.ServiceDefaults.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="AgentWebChat.Web.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<main>
|
||||
<article class="content">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@requestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
public HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? requestId;
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(requestId);
|
||||
|
||||
protected override void OnInitialized() => requestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,11 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.OutputCaching
|
||||
@using Microsoft.JSInterop
|
||||
@using AgentWebChat.Web
|
||||
@using AgentWebChat.Web.Components
|
||||
49
dotnet/samples/AgentWebChat/AgentWebChat.Web/IAgentClient.cs
Normal file
49
dotnet/samples/AgentWebChat/AgentWebChat.Web/IAgentClient.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentWebChat.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for clients that can interact with agents and provide streaming responses.
|
||||
/// </summary>
|
||||
internal abstract class AgentClientBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs an agent with the specified messages and returns a streaming response.
|
||||
/// </summary>
|
||||
/// <param name="agentName">The name of the agent to run.</param>
|
||||
/// <param name="messages">The messages to send to the agent.</param>
|
||||
/// <param name="threadId">Optional thread identifier for conversation continuity.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An asynchronous enumerable of agent response updates.</returns>
|
||||
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? threadId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent card for the specified agent (A2A protocol only).
|
||||
/// </summary>
|
||||
/// <param name="agentName">The name of the agent.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The agent card if supported, null otherwise.</returns>
|
||||
public virtual Task<AgentCard?> GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<AgentCard?>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to create a thread-like wrapper for agent clients.
|
||||
/// </summary>
|
||||
public class AgentClientThread
|
||||
{
|
||||
public string ThreadId { get; }
|
||||
|
||||
public AgentClientThread(string? threadId = null)
|
||||
{
|
||||
this.ThreadId = threadId ?? Guid.NewGuid().ToString("N");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
|
||||
|
||||
namespace AgentWebChat.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Is a simple frontend client which exercises the ability of exposed agent to communicate via OpenAI ChatCompletions protocol.
|
||||
/// </summary>
|
||||
internal sealed class OpenAIChatCompletionsAgentClient(HttpClient httpClient) : AgentClientBase
|
||||
{
|
||||
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? threadId = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
OpenAIClientOptions options = new()
|
||||
{
|
||||
Endpoint = new Uri(httpClient.BaseAddress!, $"/{agentName}/v1/"),
|
||||
Transport = new HttpClientPipelineTransport(httpClient)
|
||||
};
|
||||
|
||||
var openAiClient = new ChatClient(model: "myModel!", credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
|
||||
await foreach (var update in openAiClient.GetStreamingResponseAsync(messages, cancellationToken: cancellationToken))
|
||||
{
|
||||
yield return new AgentResponseUpdate(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace AgentWebChat.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Is a simple frontend client which exercises the ability of exposed agent to communicate via OpenAI Responses protocol.
|
||||
/// </summary>
|
||||
internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentClientBase
|
||||
{
|
||||
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? threadId = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
OpenAIClientOptions options = new()
|
||||
{
|
||||
Endpoint = new Uri(httpClient.BaseAddress!, "/v1/"),
|
||||
Transport = new HttpClientPipelineTransport(httpClient)
|
||||
};
|
||||
|
||||
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
|
||||
var chatOptions = new ChatOptions()
|
||||
{
|
||||
ConversationId = threadId
|
||||
};
|
||||
|
||||
await foreach (var update in openAiClient.GetStreamingResponseAsync(messages, chatOptions, cancellationToken: cancellationToken))
|
||||
{
|
||||
yield return new AgentResponseUpdate(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
dotnet/samples/AgentWebChat/AgentWebChat.Web/Program.cs
Normal file
52
dotnet/samples/AgentWebChat/AgentWebChat.Web/Program.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AgentWebChat.Web;
|
||||
using AgentWebChat.Web.Components;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add service defaults & Aspire client integrations.
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
builder.Services.AddOutputCache();
|
||||
|
||||
// This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
|
||||
// Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
|
||||
Uri baseAddress = new("https+http://agenthost");
|
||||
|
||||
// for some reason does not resolve with `apiservice` url
|
||||
Uri a2aAddress = new("http://localhost:5390/a2a");
|
||||
|
||||
builder.Services.AddHttpClient<AgentDiscoveryClient>(client => client.BaseAddress = baseAddress);
|
||||
builder.Services.AddSingleton(sp => new A2AAgentClient(sp.GetRequiredService<ILogger<A2AAgentClient>>(), a2aAddress));
|
||||
|
||||
builder.Services.AddHttpClient<OpenAIResponsesAgentClient>(client => client.BaseAddress = baseAddress);
|
||||
builder.Services.AddHttpClient<OpenAIChatCompletionsAgentClient>(client => client.BaseAddress = baseAddress);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.UseOutputCache();
|
||||
|
||||
app.MapStaticAssets();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5154",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7020;http://localhost:5154",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
16
dotnet/samples/AgentWebChat/AgentWebChat.Web/wwwroot/app.css
Normal file
16
dotnet/samples/AgentWebChat/AgentWebChat.Web/wwwroot/app.css
Normal file
@@ -0,0 +1,16 @@
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
BIN
dotnet/samples/AgentWebChat/AgentWebChat.Web/wwwroot/favicon.png
Normal file
BIN
dotnet/samples/AgentWebChat/AgentWebChat.Web/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user