Skip to content
Merged
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
6 changes: 5 additions & 1 deletion AI.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Folder Name="/Sample/">
<Project Path="sample/Aspire/Aspire.csproj" Id="6166be22-a13f-4074-91a9-b0f3b3a6c4fe" />
<Project Path="sample/Client/Client.csproj" />
<Project Path="sample/Server/Server.csproj" Id="34619937-085f-453d-bc12-9ab2d4abccb7" />
</Folder>
<Project Path="src/Agents/Agents.csproj" Id="90827430-b415-47d6-aac9-2dbe4911b348" />
<Project Path="src/Extensions.CodeAnalysis/Extensions.CodeAnalysis.csproj" />
<Project Path="src/Extensions/Extensions.csproj" />
<Project Path="src/SampleChat/SampleChat.csproj" Id="63ca9077-db60-473a-813d-d3bb5befdf35" />
<Project Path="src/Tests/Tests.csproj" />
</Solution>
15 changes: 15 additions & 0 deletions sample/Aspire/AppHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Projects;

var builder = DistributedApplication.CreateBuilder(args);

var server = builder.AddProject<Server>("server");

// For now, we can't really launch a console project and have its terminal shown.
// See https://github.com/dotnet/aspire/issues/8440
//builder.AddProject<Client>("client")
// .WithReference(server)
// // Flow the resolved Server HTTP endpoint to the client config
// .WithEnvironment("ai__clients__chat__endpoint", server.GetEndpoint("http"))
// .WithExternalConsole();

builder.Build().Run();
19 changes: 19 additions & 0 deletions sample/Aspire/Aspire.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Client\Client.csproj" />
<ProjectReference Include="..\Server\Server.csproj" />
</ItemGroup>

</Project>
29 changes: 29 additions & 0 deletions sample/Aspire/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17198;http://localhost:15055",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21263",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22169"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15055",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19208",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20046"
}
}
}
}
8 changes: 8 additions & 0 deletions sample/Aspire/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions sample/Aspire/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
29 changes: 29 additions & 0 deletions sample/Client/Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
<PackageReference Include="Smith" Version="0.2.5" />
<PackageReference Include="Spectre.Console" Version="0.52.0" />
<PackageReference Include="Spectre.Console.Json" Version="0.52.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.1" />
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.6" />
<PackageReference Include="ThisAssembly.Project" Version="2.1.2" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Extensions\Extensions.csproj" />
<ProjectReference Include="..\..\src\Extensions.CodeAnalysis\Extensions.CodeAnalysis.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
79 changes: 79 additions & 0 deletions sample/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Devlooped.Extensions.AI.OpenAI;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Spectre.Console;

var builder = App.CreateBuilder(args);
#if DEBUG
builder.Environment.EnvironmentName = Environments.Development;
#endif

builder.AddServiceDefaults();
builder.Services.AddHttpClient();

var app = builder.Build(async (IServiceProvider services, CancellationToken cancellation) =>
{
var baseUrl = Environment.GetEnvironmentVariable("applicationUrl") ?? "http://localhost:5117";
var http = services.GetRequiredService<IHttpClientFactory>().CreateClient();
var agents = await http.GetFromJsonAsync<AgentCard[]>($"{baseUrl}/agents", cancellation) ?? [];

if (agents.Length == 0)
{
AnsiConsole.MarkupLine(":warning: No agents available");
return;
}

var selectedAgent = AnsiConsole.Prompt(new SelectionPrompt<AgentCard>()
.Title("Select agent:")
.UseConverter(a => $"{a.Name}: {a.Description ?? ""}")
.AddChoices(agents));

var chat = new OpenAIChatClient("none", "default", new OpenAI.OpenAIClientOptions
{
Endpoint = new Uri($"{baseUrl}/{selectedAgent.Name}/v1")
}).AsBuilder().UseOpenTelemetry().UseJsonConsoleLogging().Build(services);

var history = new List<ChatMessage>();

AnsiConsole.MarkupLine($":robot: Ready");
AnsiConsole.Markup($":person_beard: ");
while (!cancellation.IsCancellationRequested)
{
var input = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(input))
continue;

history.Add(new ChatMessage(ChatRole.User, input));
try
{
var response = await AnsiConsole.Status().StartAsync(":robot: Thinking...", ctx => chat.GetResponseAsync(input));
history.AddRange(response.Messages);
try
{
// Try rendering as formatted markup
if (response.Text is { Length: > 0 })
AnsiConsole.MarkupLine($":robot: {response.Text}");
}
catch (Exception)
{
// Fallback to escaped markup text if rendering fails
AnsiConsole.MarkupLineInterpolated($":robot: {response.Text}");
}
AnsiConsole.Markup($":person_beard: ");
}
catch (Exception e)
{
AnsiConsole.WriteException(e);
}
}

AnsiConsole.MarkupLine($":robot: Shutting down...");
});

Console.WriteLine("Powered by Smith");

await app.RunAsync();

record AgentCard(string Name, string? Description);
10 changes: 10 additions & 0 deletions sample/Client/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"profiles": {
"Client": {
"commandName": "Project",
"environmentVariables": {
"applicationUrl": "http://localhost:5117"
}
}
}
}
11 changes: 11 additions & 0 deletions sample/Client/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"AI": {
"Clients": {
"Chat": {
"ApiKey": "dev",
"ModelId": "default",
"Endpoint": "http://localhost:5117/notes/v1"
}
}
}
}
7 changes: 7 additions & 0 deletions sample/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>b420aaad-e6e3-43d7-8d91-5a07b19f20ab</UserSecretsId>
</PropertyGroup>
</Project>
22 changes: 22 additions & 0 deletions sample/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project>

<PropertyGroup Condition="'$(UsingMicrosoftNETSdkWeb)' == 'true'">
<DefineConstants>$(DefineConstants);WEB</DefineConstants>
</PropertyGroup>

<ItemGroup>
<ProjectProperty Include="MSBuildProjectDirectory" />
<ProjectProperty Include="BaseOutputPath" />
<ProjectProperty Include="BaseIntermediateOutputPath" />
</ItemGroup>

<ItemGroup>
<None Update=".env" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Never" />
<Content Include="*.json;*.ini;*.toml" Exclude="@(Content)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup Condition="'$(IsAspireHost)' != 'true'">
<Compile Include="$(MSBuildThisFileDirectory)ServiceDefaults.cs" Link="ServiceDefaults.cs" Visible="false" />
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions sample/Server/AgentDiscoveryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting;

static class AgentDiscoveryExtensions
{
public static void MapAgentDiscovery(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string path)
{
var routeGroup = endpoints.MapGroup(path);
routeGroup.MapGet("/", async (AgentCatalog catalog, CancellationToken cancellation)
=> Results.Ok(await catalog
.GetAgentsAsync(cancellation)
.Select(agent => new AgentDiscoveryCard(agent.Name!, agent.Description))
.ToArrayAsync()))
.WithName("GetAgents");
}

record AgentDiscoveryCard(string Name,
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? Description);
}
47 changes: 47 additions & 0 deletions sample/Server/ConsoleExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Devlooped.Agents.AI;
using Devlooped.Extensions.AI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Extensions.AI;
using Newtonsoft.Json;
using Spectre.Console;
using Spectre.Console.Json;

public static class ConsoleExtensions
{
public static async ValueTask RenderAgentsAsync(this IServiceProvider services, IServiceCollection collection)
{
var catalog = services.GetRequiredService<AgentCatalog>();
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Include,
DefaultValueHandling = DefaultValueHandling.Ignore
};

// List configured clients
foreach (var description in collection.AsEnumerable().Where(x => x.ServiceType == typeof(IChatClient) && x.IsKeyedService && x.ServiceKey is string))
{
var client = services.GetKeyedService<IChatClient>(description.ServiceKey);
if (client is null)
continue;

var metadata = client.GetService<ConfigurableChatClientMetadata>();
var chatopt = (client as ConfigurableChatClient)?.Options;

AnsiConsole.Write(new Panel(new JsonText(JsonConvert.SerializeObject(new { Metadata = metadata, Options = chatopt }, settings)))
{
Header = new PanelHeader($"| 💬 {metadata?.Id} from {metadata?.ConfigurationSection} |"),
});
}

// List configured agents
await foreach (var agent in catalog.GetAgentsAsync())
{
var metadata = agent.GetService<ConfigurableAIAgentMetadata>();

AnsiConsole.Write(new Panel(new JsonText(JsonConvert.SerializeObject(new { Agent = agent, Metadata = metadata }, settings)))
{
Header = new PanelHeader($"| 🤖 {agent.DisplayName} from {metadata?.ConfigurationSection} |"),
});
}
}
}
Loading
Loading