diff --git a/AI.slnx b/AI.slnx index f6dcf9d..6f1f494 100644 --- a/AI.slnx +++ b/AI.slnx @@ -4,9 +4,13 @@ + + + + + - diff --git a/sample/Aspire/AppHost.cs b/sample/Aspire/AppHost.cs new file mode 100644 index 0000000..475252b --- /dev/null +++ b/sample/Aspire/AppHost.cs @@ -0,0 +1,15 @@ +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var server = builder.AddProject("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") +// .WithReference(server) +// // Flow the resolved Server HTTP endpoint to the client config +// .WithEnvironment("ai__clients__chat__endpoint", server.GetEndpoint("http")) +// .WithExternalConsole(); + +builder.Build().Run(); diff --git a/sample/Aspire/Aspire.csproj b/sample/Aspire/Aspire.csproj new file mode 100644 index 0000000..bb2ca40 --- /dev/null +++ b/sample/Aspire/Aspire.csproj @@ -0,0 +1,19 @@ + + + + + + Exe + net10.0 + + + + + + + + + + + + diff --git a/sample/Aspire/Properties/launchSettings.json b/sample/Aspire/Properties/launchSettings.json new file mode 100644 index 0000000..a433d15 --- /dev/null +++ b/sample/Aspire/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/sample/Aspire/appsettings.Development.json b/sample/Aspire/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/sample/Aspire/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/sample/Aspire/appsettings.json b/sample/Aspire/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/sample/Aspire/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/sample/Client/Client.csproj b/sample/Client/Client.csproj new file mode 100644 index 0000000..53ca217 --- /dev/null +++ b/sample/Client/Client.csproj @@ -0,0 +1,29 @@ + + + + Exe + net10.0 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/Client/Program.cs b/sample/Client/Program.cs new file mode 100644 index 0000000..ea3c50a --- /dev/null +++ b/sample/Client/Program.cs @@ -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().CreateClient(); + var agents = await http.GetFromJsonAsync($"{baseUrl}/agents", cancellation) ?? []; + + if (agents.Length == 0) + { + AnsiConsole.MarkupLine(":warning: No agents available"); + return; + } + + var selectedAgent = AnsiConsole.Prompt(new SelectionPrompt() + .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(); + + 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); \ No newline at end of file diff --git a/sample/Client/Properties/launchSettings.json b/sample/Client/Properties/launchSettings.json new file mode 100644 index 0000000..bc06646 --- /dev/null +++ b/sample/Client/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Client": { + "commandName": "Project", + "environmentVariables": { + "applicationUrl": "http://localhost:5117" + } + } + } +} \ No newline at end of file diff --git a/sample/Client/appsettings.json b/sample/Client/appsettings.json new file mode 100644 index 0000000..1be98d3 --- /dev/null +++ b/sample/Client/appsettings.json @@ -0,0 +1,11 @@ +{ + "AI": { + "Clients": { + "Chat": { + "ApiKey": "dev", + "ModelId": "default", + "Endpoint": "http://localhost:5117/notes/v1" + } + } + } +} \ No newline at end of file diff --git a/sample/Directory.Build.props b/sample/Directory.Build.props new file mode 100644 index 0000000..1ee4654 --- /dev/null +++ b/sample/Directory.Build.props @@ -0,0 +1,7 @@ + + + enable + enable + b420aaad-e6e3-43d7-8d91-5a07b19f20ab + + \ No newline at end of file diff --git a/sample/Directory.Build.targets b/sample/Directory.Build.targets new file mode 100644 index 0000000..bee8e0b --- /dev/null +++ b/sample/Directory.Build.targets @@ -0,0 +1,22 @@ + + + + $(DefineConstants);WEB + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/Server/AgentDiscoveryExtensions.cs b/sample/Server/AgentDiscoveryExtensions.cs new file mode 100644 index 0000000..9e2e30b --- /dev/null +++ b/sample/Server/AgentDiscoveryExtensions.cs @@ -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); +} diff --git a/sample/Server/ConsoleExtensions.cs b/sample/Server/ConsoleExtensions.cs new file mode 100644 index 0000000..c2890da --- /dev/null +++ b/sample/Server/ConsoleExtensions.cs @@ -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(); + 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(description.ServiceKey); + if (client is null) + continue; + + var metadata = client.GetService(); + 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(); + + AnsiConsole.Write(new Panel(new JsonText(JsonConvert.SerializeObject(new { Agent = agent, Metadata = metadata }, settings))) + { + Header = new PanelHeader($"| 🤖 {agent.DisplayName} from {metadata?.ConfigurationSection} |"), + }); + } + } +} diff --git a/sample/Server/Program.cs b/sample/Server/Program.cs new file mode 100644 index 0000000..dc07688 --- /dev/null +++ b/sample/Server/Program.cs @@ -0,0 +1,74 @@ +using System.Runtime.InteropServices; +using System.Text; +using Devlooped.Extensions.AI; +using DotNetEnv.Configuration; +using Microsoft.Agents.AI.Hosting; +using Microsoft.Agents.AI.Hosting.OpenAI; +using Microsoft.Extensions.AI; +using Spectre.Console; +using Tomlyn.Extensions.Configuration; + +var builder = WebApplication.CreateBuilder(args); + +#if DEBUG +builder.Environment.EnvironmentName = Environments.Development; +// Fixes console rendering when running from Visual Studio +if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8; +#endif + +builder.AddServiceDefaults(); +builder.ConfigureReload(); + +// 👇 implicitly calls AddChatClients +builder.AddAIAgents(); + +var app = builder.Build(); + +// From ServiceDefaults.cs +app.MapDefaultEndpoints(); + +#if DEBUG +// 👇 render all configured agents +await app.Services.RenderAgentsAsync(builder.Services); +#endif + +// Map each agent's endpoints via response API +var catalog = app.Services.GetRequiredService(); +// List configured agents +await foreach (var agent in catalog.GetAgentsAsync()) +{ + if (agent.Name != null) + app.MapOpenAIResponses(agent.Name); +} + +// Map the agents HTTP endpoints +app.MapAgentDiscovery("/agents"); + +if (!app.Environment.IsProduction()) +{ + app.Lifetime.ApplicationStarted.Register(() => + { + var baseUrl = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); + AnsiConsole.MarkupLine("[orange1]Registered Routes:[/]"); + + var endpoints = ((IEndpointRouteBuilder)app).DataSources + .SelectMany(es => es.Endpoints) + .OfType() + .Where(e => e.RoutePattern.RawText != null) + .OrderBy(e => e.RoutePattern.RawText); + + foreach (var endpoint in endpoints) + { + var httpMethods = endpoint.Metadata + .OfType() + .SelectMany(m => m.HttpMethods) ?? []; + + var methods = httpMethods.Any() ? $"{string.Join(", ", httpMethods)}" : "ANY"; + + AnsiConsole.MarkupLineInterpolated($"[blue][[{methods}]][/] [lime][link={baseUrl}{endpoint.RoutePattern.RawText}]{endpoint.RoutePattern.RawText}[/][/]"); + } + }); +} + +app.Run(); diff --git a/sample/Server/Properties/launchSettings.json b/sample/Server/Properties/launchSettings.json new file mode 100644 index 0000000..228344d --- /dev/null +++ b/sample/Server/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://server.dev.localhost:5117", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/sample/Server/Server.csproj b/sample/Server/Server.csproj new file mode 100644 index 0000000..978e7d6 --- /dev/null +++ b/sample/Server/Server.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SampleChat/ai.toml b/sample/Server/ai.toml similarity index 84% rename from src/SampleChat/ai.toml rename to sample/Server/ai.toml index 24ef42f..15b0804 100644 --- a/src/SampleChat/ai.toml +++ b/sample/Server/ai.toml @@ -1,9 +1,11 @@ [ai.clients.openai] modelid = "gpt-4.1" -apikey = "sk-asdf" -[ai.agents.reminder] -name = "Orders" +[ai.clients.grok] +endpoint = "https://api.x.ai/v1" +modelid = "grok-4-fast-non-reasoning" + +[ai.agents.orders] description = "Manage orders using catalogs for food or any other item." instructions = """ You are an AI agent responsible for processing orders for food or other items. diff --git a/sample/Server/appsettings.Development.json b/sample/Server/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/sample/Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/SampleChat/appsettings.json b/sample/Server/appsettings.json similarity index 64% rename from src/SampleChat/appsettings.json rename to sample/Server/appsettings.json index 8c76f9d..78b8db9 100644 --- a/src/SampleChat/appsettings.json +++ b/sample/Server/appsettings.json @@ -1,7 +1,15 @@ { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", "AI": { "Agents": { "Notes": { + "Name": "notes", "Description": "Provides free-form memory", "Instructions": "You organize and keep notes for the user, using JSON-LD", "Client": "Grok", @@ -13,9 +21,8 @@ "Clients": { "Grok": { "Endpoint": "https://api.grok.ai/v1", - "ModelId": "grok-4-fast-non-reasoning", - "ApiKey": "xai-asdf" + "ModelId": "grok-4-fast-non-reasoning" } } } -} \ No newline at end of file +} diff --git a/sample/ServiceDefaults.cs b/sample/ServiceDefaults.cs new file mode 100644 index 0000000..b0fa98b --- /dev/null +++ b/sample/ServiceDefaults.cs @@ -0,0 +1,134 @@ +using DotNetEnv.Configuration; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Tomlyn.Extensions.Configuration; + + +#if WEB +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +#endif + +static class ConfigureOpenTelemetryExtensions +{ + const string HealthEndpointPath = "/health"; + const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + // .env/secrets override other config, which may contain dummy API keys, for example + builder.Configuration + .AddDotNetEnv() + .AddEnvironmentVariables() + .AddUserSecrets(); + + builder.ConfigureReload(); + +#if WEB + builder.AddDefaultHealthChecks(); +#endif + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + var serviceName = builder.Environment.ApplicationName + ?? throw new InvalidOperationException("Application name is not set in the hosting environment."); + + builder.Services.AddOpenTelemetry() + .ConfigureResource(rb => rb.AddService(serviceName)) + .WithTracing(tracing => + { +#if WEB + tracing.AddAspNetCoreInstrumentation(tracing => + // Don't trace requests to the health endpoint to avoid filling the dashboard with noise + tracing.Filter = httpContext => + !(httpContext.Request.Path.StartsWithSegments(HealthEndpointPath) + || httpContext.Request.Path.StartsWithSegments(AlivenessEndpointPath))); +#endif + tracing.AddHttpClientInstrumentation(); + tracing.AddConsoleExporter(); + }) + .WithMetrics(metrics => + { +#if WEB + metrics.AddAspNetCoreInstrumentation(); +#endif + metrics.AddRuntimeInstrumentation(); + metrics.AddHttpClientInstrumentation(); + }); + + + if (string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"])) + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + + return builder; + } + + public static TBuilder ConfigureReload(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + if (builder.Environment.IsProduction()) + { + foreach (var toml in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.toml", SearchOption.AllDirectories)) + builder.Configuration.AddTomlFile(toml, optional: false, reloadOnChange: true); + foreach (var json in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.json", SearchOption.AllDirectories)) + builder.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true); + } + else + { + var baseDir = ThisAssembly.Project.MSBuildProjectDirectory; + var outDir = Path.Combine(baseDir, ThisAssembly.Project.BaseOutputPath); + var objDir = Path.Combine(baseDir, ThisAssembly.Project.BaseIntermediateOutputPath); + + // Only use configs outside of bin/ and obj/ directories since we want reload to happen from source files not output files + bool IsSource(string path) => !path.StartsWith(outDir) && !path.StartsWith(objDir); + + foreach (var json in Directory.EnumerateFiles(baseDir, "*.json", SearchOption.AllDirectories).Where(IsSource)) + builder.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true); + + foreach (var toml in Directory.EnumerateFiles(baseDir, "*.toml", SearchOption.AllDirectories).Where(IsSource)) + builder.Configuration.AddTomlFile(toml, optional: false, reloadOnChange: true); + } + + return builder; + } + +#if WEB + public static TBuilder AddDefaultHealthChecks(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(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +#endif +} diff --git a/src/Agents/Agents.csproj b/src/Agents/Agents.csproj index 199cbb1..f4b074f 100644 --- a/src/Agents/Agents.csproj +++ b/src/Agents/Agents.csproj @@ -14,19 +14,19 @@ true - $(NoWarn);CS0436;SYSLIB1100;SYSLIB1101 + $(NoWarn);CS0436;SYSLIB1100;SYSLIB1101;MEAI001 - - - - - - - - - + + + + + + + + + diff --git a/src/Directory.props b/src/Directory.props index 43ac06f..294c754 100644 --- a/src/Directory.props +++ b/src/Directory.props @@ -2,7 +2,7 @@ Devlooped AI Extensions - enable + true 6eb457f9-16bc-49c5-81f2-33399b254e04 https://api.nuget.org/v3/index.json;https://pkg.kzu.app/index.json diff --git a/src/Extensions.CodeAnalysis/Extensions.CodeAnalysis.csproj b/src/Extensions.CodeAnalysis/Extensions.CodeAnalysis.csproj index 5ac03e9..c4e2782 100644 --- a/src/Extensions.CodeAnalysis/Extensions.CodeAnalysis.csproj +++ b/src/Extensions.CodeAnalysis/Extensions.CodeAnalysis.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Extensions/Extensions.csproj b/src/Extensions/Extensions.csproj index 7a5a3a2..612d6a4 100644 --- a/src/Extensions/Extensions.csproj +++ b/src/Extensions/Extensions.csproj @@ -21,15 +21,15 @@ - - + + - - - - - - + + + + + + @@ -52,14 +52,8 @@ - - + + \ No newline at end of file diff --git a/src/SampleChat/AppInitializer.cs b/src/SampleChat/AppInitializer.cs deleted file mode 100644 index 4324e89..0000000 --- a/src/SampleChat/AppInitializer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; - -namespace SampleChat; - -class AppInitializer -{ - [ModuleInitializer] - public static void Init() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8; - - // Load environment variables from .env files in current dir and above. - DotNetEnv.Env.TraversePath().Load(); - - // Load environment variables from user profile directory. - var userEnv = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".env"); - if (File.Exists(userEnv)) - DotNetEnv.Env.Load(userEnv); - } -} diff --git a/src/SampleChat/Program.cs b/src/SampleChat/Program.cs deleted file mode 100644 index 453834f..0000000 --- a/src/SampleChat/Program.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Devlooped.Agents.AI; -using Devlooped.Extensions.AI; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Hosting; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Newtonsoft.Json; -using Spectre.Console; -using Spectre.Console.Json; -using Tomlyn.Extensions.Configuration; - -var host = new HostApplicationBuilder(args); - -#if DEBUG -host.Environment.EnvironmentName = Environments.Development; -#endif - -// Setup config files from output directory vs project directory -// depending on environment, so we can reload by editing the source -if (host.Environment.IsProduction()) -{ - foreach (var json in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.json", SearchOption.AllDirectories)) - host.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true); - - foreach (var toml in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.toml", SearchOption.AllDirectories)) - host.Configuration.AddTomlFile(toml, optional: false, reloadOnChange: true); -} -else -{ - var baseDir = ThisAssembly.Project.MSBuildProjectDirectory; - var outDir = Path.Combine(baseDir, ThisAssembly.Project.BaseOutputPath); - var objDir = Path.Combine(baseDir, ThisAssembly.Project.BaseIntermediateOutputPath); - - bool IsSource(string path) => !path.StartsWith(outDir) && !path.StartsWith(objDir); - - foreach (var json in Directory.EnumerateFiles(baseDir, "*.json", SearchOption.AllDirectories).Where(IsSource)) - host.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true); - - foreach (var toml in Directory.EnumerateFiles(baseDir, "*.toml", SearchOption.AllDirectories).Where(IsSource)) - host.Configuration.AddTomlFile(toml, optional: false, reloadOnChange: true); -} - -// .env/secrets override other config, which may contain dummy API keys, for example -host.Configuration - .AddEnvironmentVariables() - .AddUserSecrets(); - -// 👇 implicitly calls AddChatClients -host.AddAIAgents(); - -var app = host.Build(); -var catalog = app.Services.GetRequiredService(); -var settings = new JsonSerializerSettings -{ - NullValueHandling = NullValueHandling.Include, - DefaultValueHandling = DefaultValueHandling.Ignore -}; - -// List configured clients -foreach (var description in host.Services.AsEnumerable().Where(x => x.ServiceType == typeof(IChatClient) && x.IsKeyedService)) -{ - var client = app.Services.GetKeyedService(description.ServiceKey); - if (client is null) - continue; - - var metadata = client.GetService(); - var chatopt = (client as ConfigurableChatClient)?.Options; - - AnsiConsole.Write(new Panel(new JsonText(JsonConvert.SerializeObject(new { Metadata = metadata, Options = chatopt }, settings))) - { - Header = new PanelHeader($"| 💬 {description.ServiceKey} |"), - }); -} - -// List configured agents -await foreach (var agent in catalog.GetAgentsAsync()) -{ - var metadata = agent.GetService(); - - AnsiConsole.Write(new Panel(new JsonText(JsonConvert.SerializeObject(new { Agent = agent, Metadata = metadata }, settings))) - { - Header = new PanelHeader($"| 🤖 {agent.DisplayName} |"), - }); -} - -Console.ReadLine(); diff --git a/src/SampleChat/SampleChat.csproj b/src/SampleChat/SampleChat.csproj deleted file mode 100644 index b632474..0000000 --- a/src/SampleChat/SampleChat.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 0ba67c8..b7e726d 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -14,16 +14,16 @@ - - - - - - - - - - + + + + + + + + + +