diff --git a/AI.slnx b/AI.slnx
index f5d49a4..0e6ed7b 100644
--- a/AI.slnx
+++ b/AI.slnx
@@ -4,12 +4,6 @@
-
-
-
-
-
-
diff --git a/readme.md b/readme.md
index 4fabec2..a12f018 100644
--- a/readme.md
+++ b/readme.md
@@ -3,8 +3,10 @@
[](osmfeula.txt)
[](license.txt)
+[](https://www.nuget.org/packages/Devlooped.Extensions.AI)
+[](https://www.nuget.org/packages/Devlooped.Extensions.AI)
-Extensions for Microsoft.Agents.AI and Microsoft.Extensions.AI.
+Extensions for Microsoft.Extensions.AI.
## Open Source Maintenance Fee
@@ -19,270 +21,6 @@ OSMF tier. A single fee covers all of [Devlooped packages](https://www.nuget.org
-# Devlooped.Agents.AI
-
-[](https://www.nuget.org/packages/Devlooped.Agents.AI)
-[](https://www.nuget.org/packages/Devlooped.Agents.AI)
-
-
-Extensions for Microsoft.Agents.AI, such as configuration-driven auto-reloading agents.
-
-
-
-## Overview
-
-Microsoft.Agents.AI (aka [Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
-is a comprehensive API for building AI agents. Its programatic model (which follows closely
-the [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai)
-approach) provides maximum flexibility with little prescriptive structure.
-
-This package provides additional extensions to make developing agents easier and more
-declarative.
-
-## Configurable Agents
-
-Tweaking agent options such as description, instructions, chat client to use and its
-options, etc. is very common during development/testing. This package provides the ability to
-drive those settings from configuration (with auto-reload support). This makes it far easier
-to experiment with various combinations of agent instructions, chat client providers and
-options, and model parameters without changing code, recompiling or even restarting the application:
-
-> [!NOTE]
-> This example shows integration with configurable chat clients feature from the
-> Devlooped.Extensions.AI package, but any `IChatClient` registered in the DI container
-> with a matching key can be used.
-
-```json
-{
- "AI": {
- "Agents": {
- "MyAgent": {
- "Description": "An AI agent that helps with customer support.",
- "Instructions": "You are a helpful assistant for customer support.",
- "Client": "Grok",
- "Options": {
- "ModelId": "grok-4",
- "Temperature": 0.5,
- }
- }
- },
- "Clients": {
- "Grok": {
- "Endpoint": "https://api.grok.ai/v1",
- "ModelId": "grok-4-fast-non-reasoning",
- "ApiKey": "xai-asdf"
- }
- }
- }
-}
-````
-
-```csharp
-var host = new HostApplicationBuilder(args);
-host.Configuration.AddJsonFile("appsettings.json, optional: false, reloadOnChange: true);
-
-// 👇 implicitly calls AddChatClients
-host.AddAIAgents();
-
-var app = host.Build();
-var agent = app.Services.GetRequiredKeyedService("MyAgent");
-```
-
-Agents are also properly registered in the corresponding Microsoft Agent Framework
-[AgentCatalog](https://learn.microsoft.com/en-us/dotnet/api/microsoft.agents.ai.hosting.agentcatalog):
-
-```csharp
-var catalog = app.Services.GetRequiredService();
-await foreach (AIAgent agent in catalog.GetAgentsAsync())
-{
- var metadata = agent.GetService();
- Console.WriteLine($"Agent: {agent.Name} by {metadata.ProviderName}");
-}
-```
-
-You can of course use any config format supported by .NET configuration, such as
-TOML which is arguably more human-friendly for hand-editing:
-
-```toml
-[ai.clients.openai]
-modelid = "gpt-4.1"
-
-[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.
- Your primary goals are to identify user intent, extract or request provider information, manage order data using tools and friendly responses to guide users through the ordering process.
- """
-
-# ai.clients.openai, can omit the ai.clients prefix
-client = "openai"
-
-[ai.agents.orders.options]
-modelid = "gpt-4o-mini"
-```
-
-This can be used by leveraging [Tomlyn.Extensions.Configuration](https://www.nuget.org/packages/Tomlyn.Extensions.Configuration).
-
-> [!NOTE]
-> This package will automatically dedent and trim start and end newlines from
-> multi-line instructions and descriptions when applying the configuration,
-> avoiding unnecessary tokens being used for indentation while allowing flexible
-> formatting in the config file.
-
-You can also leverage the format pioneered by [VS Code Chat Modes](https://code.visualstudio.com/docs/copilot/customization/custom-chat-modes),
- (por "custom agents") by using markdown format plus YAML front-matter for better readability:
-
-```yaml
----
-id: ai.agents.notes
-description: Provides free-form memory
-client: grok
-model: grok-4-fast
----
-You organize and keep notes for the user.
-# Some header
-More content
-```
-
-Visual Studio Code will ignore the additional attributes used by this project. In particular, the `model`
-property is a shorthand for setting the `options.modelid`, but in our implementation, the latter takes
-precedence over the former, which allows you to rely on `model` to drive the VSCode testing, and the
-longer form for run-time with the Agents Framework:
-
-```yaml
----
-id: ai.agents.notes
-description: Provides free-form memory
-model: Grok Code Fast 1 (copilot)
-client: grok
-options:
- modelid: grok-code-fast-1
----
-// Instructions
-```
-
-
-
-Use the provided `AddAgentMarkdown` extension method to load instructions from files as follows:
-
-```csharp
-var host = new HostApplicationBuilder(args);
-host.Configuration.AddAgentMarkdown("notes.agent.md", optional: false, reloadOnChange: true);
-```
-
-The `id` field in the front-matter is required and specifies the configuration section name, and
-all other fields are added as if they were specified under it in the configuration.
-
-### Extensible AI Contexts
-
-The Microsoft [agent framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) allows extending
-agents with dynamic context via [AIContextProvider](https://learn.microsoft.com/en-us/dotnet/api/microsoft.agents.ai.aicontextprovider)
-and `AIContext`. This package supports dynamic extension of a configured agent in the following ways (in order of priority):
-
-1. A keyed service `AIContextProviderFactory` with the same name as the agent will be set up just as if you had
- set it manually as the [ChatClientAgentOptions.AIContextProviderFactory](https://learn.microsoft.com/en-us/dotnet/api/microsoft.agents.ai.chatclientagentoptions.aicontextproviderfactory)
- in code.
-2. Aggregate of AI contexts from:
- a. Keyed service `AIContextProvider` with the same name as the agent.
- b. Keyed service `AIContext` with the same name as the agent.
- c. Other services pulled in via `use` setting for an agent registered as either `AIContextProvider` or `AIContext`
- with a matching key.
-
-The first option assumes you want full control of the context, so it does not allow futher composition.
-The second alternative allows more declarative scenarios involving reusable and cross-cutting
-context definitions.
-
-For example, let's say you want to provide consistent tone for all your agents. It would be tedious, repetitive and harder
-to maintain if you had to set that in each agent's instructions. Instead, you can define a reusable context named `tone` such as:
-
-```toml
-[ai.context.tone]
-instructions = """\
- Default to using spanish language, using argentinean "voseo" in your responses \
- (unless the user explicitly talks in a different language). \
- This means using "vos" instead of "tú" and conjugating verbs accordingly. \
- Don't use the expression "pa'" instead of "para". Don't mention the word "voseo".
- """
-```
-
-Then, you can reference that context in any agent using the `use` setting:
-```toml
-[ai.agents.support]
-description = "An AI agent that helps with customer support."
-instructions = "..."
-client = "grok"
-use = ["tone"]
-
-[ai.agents.sales]
-description = "An AI agent that helps with sales inquiries."
-instructions = "..."
-client = "openai"
-use = ["tone"]
-```
-
-Configured contexts can provide all three components of an `AIContext`: instructions, messages and tools, such as:
-
-```toml
-[ai.context.timezone]
-instructions = "Always assume the user's timezone is America/Argentina/Buenos_Aires unless specified otherwise."
-messages = [
- { system = "You are aware of the current date and time in America/Argentina/Buenos_Aires." }
-]
-tools = ["get_date"]
-```
-
-If multiple contexts are specified in `use`, they are applied in order, concatenating their instructions, messages and tools.
-
-In addition to configured sections, the `use` property can also reference exported contexts as either `AIContext`
-(for static context) or `AIContextProvider` (for dynamic context) registered in DI with a matching name.
-
-
-### Extensible Tools
-
-The `tools` section allows specifying tool names registered in the DI container, such as:
-
-```csharp
-services.AddKeyedSingleton("get_date", AIFunctionFactory.Create(() => DateTimeOffset.Now, "get_date"));
-```
-
-This tool will be automatically wired into any agent that uses the `timezone` context above.
-
-Agents themselves can also add tools from DI into an agent's context without having to define an entire
-section just for that, by specifying the tool name directly in the `tools` array:
-
-```toml
-[ai.agents.support]
-description = "An AI agent that helps with customer support."
-instructions = "..."
-client = "grok"
-use = ["tone"]
-tools = ["get_date"]
-```
-
-This enables a flexible and convenient mix of static and dynamic context for agents, all driven
-from configuration.
-
-In addition to registering your own tools in DI, you can also use leverage the MCP C# SDK and reuse
-the same tool declarations:
-
-```csharp
-builder.Services.AddMcpServer().WithTools();
-
-// 👇 Reuse same tool definitions in agents
-builder.AddAIAgents().WithTools();
-```
-
-
-
-# Devlooped.Extensions.AI
-
-[](https://www.nuget.org/packages/Devlooped.Extensions.AI)
-[](https://www.nuget.org/packages/Devlooped.Extensions.AI)
-
Extensions for Microsoft.Extensions.AI
diff --git a/sample/Aspire/AppHost.cs b/sample/Aspire/AppHost.cs
deleted file mode 100644
index 475252b..0000000
--- a/sample/Aspire/AppHost.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-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
deleted file mode 100644
index e19de01..0000000
--- a/sample/Aspire/Aspire.csproj
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
- Exe
- net10.0
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sample/Aspire/Properties/launchSettings.json b/sample/Aspire/Properties/launchSettings.json
deleted file mode 100644
index a433d15..0000000
--- a/sample/Aspire/Properties/launchSettings.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "$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
deleted file mode 100644
index 0c208ae..0000000
--- a/sample/Aspire/appsettings.Development.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- }
-}
diff --git a/sample/Aspire/appsettings.json b/sample/Aspire/appsettings.json
deleted file mode 100644
index 31c092a..0000000
--- a/sample/Aspire/appsettings.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning",
- "Aspire.Hosting.Dcp": "Warning"
- }
- }
-}
diff --git a/sample/Client/Client.csproj b/sample/Client/Client.csproj
deleted file mode 100644
index e7d215d..0000000
--- a/sample/Client/Client.csproj
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- Exe
- net10.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sample/Client/Program.cs b/sample/Client/Program.cs
deleted file mode 100644
index 5ae9364..0000000
--- a/sample/Client/Program.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System.Net.Http.Json;
-using Devlooped.Extensions.AI.OpenAI;
-using OpenTelemetry.Metrics;
-using OpenTelemetry.Trace;
-
-var builder = App.CreateBuilder(args);
-#if DEBUG
-builder.Environment.EnvironmentName = Environments.Development;
-#endif
-
-builder.AddServiceDefaults();
-builder.Services.AddHttpClient()
- .ConfigureHttpClientDefaults(b => b.AddStandardResilienceHandler());
-
-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
deleted file mode 100644
index bc06646..0000000
--- a/sample/Client/Properties/launchSettings.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "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
deleted file mode 100644
index 6a8dd9e..0000000
--- a/sample/Client/appsettings.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "AI": {
- "Clients": {
- "Chat": {
- "ApiKey": "dev",
- "ModelId": "default",
- "Endpoint": "http://localhost:5117/notes/v1"
- }
- }
- },
- "Logging": {
- "LogLevel": {
- "Default": "Warning",
- "System.Net.Http.HttpClient": "Error",
- "Microsoft.Hosting": "Error"
- }
- }
-}
\ No newline at end of file
diff --git a/sample/Directory.Build.props b/sample/Directory.Build.props
deleted file mode 100644
index 1ee4654..0000000
--- a/sample/Directory.Build.props
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- 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
deleted file mode 100644
index ddfc56d..0000000
--- a/sample/Directory.Build.targets
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- $(DefineConstants);WEB
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/sample/Server/AgentDiscoveryExtensions.cs b/sample/Server/AgentDiscoveryExtensions.cs
deleted file mode 100644
index 9e2e30b..0000000
--- a/sample/Server/AgentDiscoveryExtensions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-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
deleted file mode 100644
index 018256c..0000000
--- a/sample/Server/ConsoleExtensions.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using Devlooped.Agents.AI;
-using Devlooped.Extensions.AI;
-using Microsoft.Agents.AI.Hosting;
-using Microsoft.Extensions.AI;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Serialization;
-using Spectre.Console;
-using Spectre.Console.Json;
-
-public static class ConsoleExtensions
-{
- extension(IServiceProvider services)
- {
- public async ValueTask RenderAgentsAsync(IServiceCollection collection)
- {
- var catalog = services.GetRequiredService();
- var settings = new JsonSerializerSettings
- {
- NullValueHandling = NullValueHandling.Include,
- DefaultValueHandling = DefaultValueHandling.Ignore,
- ContractResolver = new IgnoreDelegatePropertiesResolver(),
- };
-
- // 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} |"),
- });
- }
- }
- }
-
- class IgnoreDelegatePropertiesResolver : DefaultContractResolver
- {
- protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
- {
- var property = base.CreateProperty(member, memberSerialization);
-
- if (property.PropertyType != null && typeof(Delegate).IsAssignableFrom(property.PropertyType))
- {
- property.ShouldSerialize = _ => false;
- }
-
- return property;
- }
- }
-}
diff --git a/sample/Server/NotesTools.cs b/sample/Server/NotesTools.cs
deleted file mode 100644
index f3931c3..0000000
--- a/sample/Server/NotesTools.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Microsoft.Agents.AI;
-using ModelContextProtocol.Server;
-
-public class NotesContextProvider(NotesTools notes) : AIContextProvider
-{
- public override ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
- => ValueTask.FromResult(new AIContext
- {
- Instructions =
- $"""
- Your current state is:
-
- ${notes.GetNotes()}
-
- """
- });
-}
-
-[McpServerToolType]
-public class NotesTools
-{
- string notes = "";
-
- [McpServerTool]
- public string GetNotes() => notes;
-
- [McpServerTool]
- public void SaveNotes(string notes) => this.notes = notes;
-}
diff --git a/sample/Server/Program.cs b/sample/Server/Program.cs
deleted file mode 100644
index d0ce8cf..0000000
--- a/sample/Server/Program.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System.Runtime.InteropServices;
-using System.Text;
-using Devlooped.Extensions.AI;
-using Microsoft.Agents.AI;
-using Microsoft.Agents.AI.Hosting;
-using Microsoft.Agents.AI.Hosting.OpenAI;
-using Microsoft.Extensions.AI;
-using Spectre.Console;
-
-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();
-
-// 👇 showcases using dynamic AI context from configuration
-builder.Services.AddKeyedSingleton("get_date", AIFunctionFactory.Create(() => DateTimeOffset.UtcNow, "get_date"));
-// dummy ones for illustration
-builder.Services.AddKeyedSingleton("create_order", AIFunctionFactory.Create(() => "OK", "create_order"));
-builder.Services.AddKeyedSingleton("cancel_order", AIFunctionFactory.Create(() => "OK", "cancel_order"));
-
-builder.Services.AddKeyedSingleton("notes");
-
-// 👇 seamless integration of MCP tools
-//builder.Services.AddMcpServer().WithTools();
-
-// 👇 implicitly calls AddChatClients
-builder.AddAIAgents()
- .WithTools();
-
-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();
\ No newline at end of file
diff --git a/sample/Server/Properties/launchSettings.json b/sample/Server/Properties/launchSettings.json
deleted file mode 100644
index 228344d..0000000
--- a/sample/Server/Properties/launchSettings.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "$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
deleted file mode 100644
index d95b24a..0000000
--- a/sample/Server/Server.csproj
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
- net10.0
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sample/Server/ai.toml b/sample/Server/ai.toml
deleted file mode 100644
index 5cbef20..0000000
--- a/sample/Server/ai.toml
+++ /dev/null
@@ -1,31 +0,0 @@
-[ai.clients.openai]
-modelid = "gpt-4.1"
-
-[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.
- Your primary goals are to identify user intent, extract or request provider information, manage order data using tools and friendly responses to guide users through the ordering process.
- """
-options = { modelid = "gpt-4o-mini" }
-# 👇 alternative syntax to specify options
-# [ai.agents.orders.options]
-# modelid = "gpt-4o-mini"
-
-# ai.clients.openai, can omit the ai.clients prefix
-client = "openai"
-use = ["tone"]
-tools = ["get_date", "create_order", "cancel_order"]
-
-
-[ai.context.tone]
-instructions = """\
- Default to using spanish language, using argentinean "voseo" in your responses \
- (unless the user explicitly talks in a different language). \
- This means using "vos" instead of "tú" and conjugating verbs accordingly. \
- Don't mention the word "voseo".
- """
\ No newline at end of file
diff --git a/sample/Server/appsettings.Development.json b/sample/Server/appsettings.Development.json
deleted file mode 100644
index a284d64..0000000
--- a/sample/Server/appsettings.Development.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning",
- "Microsoft.Hosting": "Warning",
- "System.Net.Http.HttpClient": "Warning"
- }
- }
-}
diff --git a/sample/Server/appsettings.json b/sample/Server/appsettings.json
deleted file mode 100644
index ccfa333..0000000
--- a/sample/Server/appsettings.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "AllowedHosts": "*",
- "AI": {
- "Clients": {
- "Grok": {
- "Endpoint": "https://api.x.ai/v1",
- "ModelId": "grok-4-fast-non-reasoning"
- }
- }
- },
- "OpenTelemetry:ConsoleExporter": true
-}
diff --git a/sample/Server/notes.agent.md b/sample/Server/notes.agent.md
deleted file mode 100644
index b0ff16c..0000000
--- a/sample/Server/notes.agent.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-id: ai.agents.notes
-description: Provides free-form memory
-beta: true
-visibility: unlisted
-client: grok
-model: grok-4-fast
-use: ["tone", "notes"]
-tools: ["get_notes", "save_notes", "get_date"]
----
-You organize and keep notes for the user.
-
-You extract key points from the user's input and store them as notes in the agent
-state. You keep track of notes that reference external files too, adding corresponding
-notes relating to them if the user sends more information about them.
-
-You use JSON-LD format to store notes, optimized for easy retrieval, filtering
-or reasoning later. This can involve nested structures, lists, tags, categories, timestamps,
-inferred relationships between nodes and entities, etc. As you collect more notes, you can
-go back and update, merge or reorganize existing notes to improve their structure, cohesion
-and usefulness for retrieval and querying.
-
-When storing relative times (like "last week" or "next week"), always convert them to absolute
-dates or timestamps, so you can be precise when responding about them.
-
-You are NOT a general-purpose assistant that can answer questions or perform tasks unrelated
-to note-taking and recalling notes. If the user asks you to do something outside of
-note-taking, you should politely decline and remind them of your purpose.
-
-Never include technical details about the JSON format or the storage mechanism in your
-responses. Just focus on the content of the notes and how they can help the user.
-
-When recalling information from notes, don't ask for follow-up questions or request
-any more information. Just provide the information.
diff --git a/sample/ServiceDefaults.cs b/sample/ServiceDefaults.cs
deleted file mode 100644
index 16076d7..0000000
--- a/sample/ServiceDefaults.cs
+++ /dev/null
@@ -1,150 +0,0 @@
-using Devlooped.Agents.AI;
-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();
-
- // Only add console exporter if explicitly enabled in configuration
- if (builder.Configuration.GetValue("OpenTelemetry:ConsoleExporter"))
- 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;
- }
-
- ///
- /// Configures automatic configuration reload from either the build output directory (production)
- /// or from the project directory (development).
- ///
- 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);
-
- foreach (var md in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.md", SearchOption.AllDirectories))
- builder.Configuration.AddAgentMarkdown(md, 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 toml in Directory.EnumerateFiles(baseDir, "*.toml", SearchOption.AllDirectories).Where(IsSource))
- builder.Configuration.AddTomlFile(toml, optional: false, reloadOnChange: true);
-
- foreach (var json in Directory.EnumerateFiles(baseDir, "*.json", SearchOption.AllDirectories).Where(IsSource))
- builder.Configuration.AddJsonFile(json, optional: false, reloadOnChange: true);
-
- foreach (var md in Directory.EnumerateFiles(baseDir, "*.md", SearchOption.AllDirectories).Where(IsSource))
- builder.Configuration.AddAgentMarkdown(md, 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/Tests/ConfigurableAgentTests.cs b/src/Tests/ConfigurableAgentTests.cs
deleted file mode 100644
index 89116a0..0000000
--- a/src/Tests/ConfigurableAgentTests.cs
+++ /dev/null
@@ -1,890 +0,0 @@
-using Devlooped.Extensions.AI;
-using Devlooped.Extensions.AI.Grok;
-using Microsoft.Agents.AI;
-using Microsoft.Extensions.AI;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Moq;
-
-namespace Devlooped.Agents.AI;
-
-public class ConfigurableAgentTests(ITestOutputHelper output)
-{
- [Fact]
- public void CanConfigureAgent()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
- ["ai:clients:chat:apikey"] = "sk-asdfasdf",
- ["ai:agents:bot:client"] = "chat",
- ["ai:agents:bot:name"] = "chat",
- ["ai:agents:bot:description"] = "Helpful chat agent",
- ["ai:agents:bot:instructions"] = "You are a helpful chat agent.",
- ["ai:agents:bot:options:temperature"] = "0.5",
- ["ai:agents:bot:emoji"] = "🤖",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("chat");
-
- Assert.Equal("chat", agent.Name);
- Assert.Equal("chat", agent.DisplayName);
- Assert.Equal("Helpful chat agent", agent.Description);
-
- var additional = Assert.IsType(agent, exactMatch: false);
- Assert.Equal("🤖", additional.AdditionalProperties?["emoji"]?.ToString());
- Assert.Equal("🤖", agent.Emoji);
- }
-
- [Fact]
- public void CanGetFromAlternativeKey()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:Chat:modelid"] = "gpt-4.1-nano",
- ["ai:clients:Chat:apikey"] = "sk-asdfasdf",
- // NOTE: mismatched case in client id
- ["ai:agents:bot:client"] = "chat",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService(new ServiceKey("Bot"));
-
- Assert.Equal("bot", agent.Name);
- Assert.Same(agent, app.Services.GetIAAgent("Bot"));
- }
-
- [Fact]
- public void CanGetSectionAndIdFromMetadata()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
- ["ai:clients:chat:apikey"] = "sk-asdfasdf",
- ["ai:agents:bot:client"] = "chat",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("bot");
- var metadata = agent.GetService();
-
- Assert.NotNull(metadata);
- Assert.Equal("bot", metadata.Name);
- Assert.Equal("ai:agents:bot", metadata.ConfigurationSection);
- }
-
- [Fact]
- public void DedentsDescriptionAndInstructions()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
- ["ai:clients:chat:apikey"] = "sk-asdfasdf",
- ["ai:agents:bot:client"] = "chat",
- ["ai:agents:bot:name"] = "chat",
- ["ai:agents:bot:description"] =
- """
-
-
- Line 1
- Line 2
- Line 3
-
- """,
- ["ai:agents:bot:instructions"] =
- """
- Agent Instructions:
- - Step 1
- - Step 2
- - Step 3
- """,
- ["ai:agents:bot:options:temperature"] = "0.5",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("chat");
-
- Assert.Equal(
- """
- Line 1
- Line 2
- Line 3
- """, agent.Description);
-
- Assert.Equal(
- """
- Agent Instructions:
- - Step 1
- - Step 2
- - Step 3
- """, agent.GetService()?.Instructions);
- }
-
- [Fact]
- public void CanReloadConfiguration()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:openai:modelid"] = "gpt-4.1-nano",
- ["ai:clients:openai:apikey"] = "sk-asdfasdf",
- ["ai:clients:grok:modelid"] = "grok-4",
- ["ai:clients:grok:apikey"] = "xai-asdfasdf",
- ["ai:clients:grok:endpoint"] = "https://api.x.ai",
- ["ai:agents:bot:client"] = "openai",
- ["ai:agents:bot:description"] = "Helpful chat agent",
- ["ai:agents:bot:instructions"] = "You are a helpful agent.",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("bot");
-
- Assert.Equal("Helpful chat agent", agent.Description);
- Assert.Equal("You are a helpful agent.", agent.GetService()?.Instructions);
- Assert.Equal("openai", agent.GetService()?.ProviderName);
-
- // Change the configuration to point to a different client
- var configuration = (IConfigurationRoot)app.Services.GetRequiredService();
- configuration["ai:agents:bot:client"] = "grok";
- configuration["ai:agents:bot:description"] = "Very helpful chat agent";
- configuration["ai:agents:bot:instructions"] = "You are a very helpful chat agent.";
-
- // NOTE: the in-memory provider does not support reload on change, so we must trigger it manually.
- configuration.Reload();
-
- Assert.Equal("Very helpful chat agent", agent.Description);
- Assert.Equal("You are a very helpful chat agent.", agent.GetService()?.Instructions);
- Assert.Equal("xai", agent.GetService()?.ProviderName);
- }
-
- [Fact]
- public void AssignsContextProviderFromService()
- {
- var builder = new HostApplicationBuilder();
- var context = Mock.Of();
-
- builder.Services.AddSingleton(
- Mock.Of(x
- => x.CreateProvider(It.IsAny()) == context));
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
- ["ai:clients:chat:apikey"] = "sk-asdfasdf",
- ["ai:agents:bot:client"] = "chat",
- ["ai:agents:bot:options:temperature"] = "0.5",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("bot");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
- Assert.Same(context, options?.AIContextProviderFactory?.Invoke(new()));
- }
-
- [Fact]
- public void AssignsMessageStoreFactoryFromKeyedService()
- {
- var builder = new HostApplicationBuilder();
- var context = Mock.Of();
-
- builder.Services.AddKeyedSingleton("bot",
- Mock.Of(x
- => x.CreateStore(It.IsAny()) == context));
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
- ["ai:clients:chat:apikey"] = "sk-asdfasdf",
- ["ai:agents:bot:client"] = "chat",
- ["ai:agents:bot:options:temperature"] = "0.5",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("bot");
- var options = agent.GetService();
-
- Assert.NotNull(options?.ChatMessageStoreFactory);
- Assert.Same(context, options?.ChatMessageStoreFactory?.Invoke(new()));
- }
-
- [Fact]
- public void AssignsMessageStoreFactoryFromService()
- {
- var builder = new HostApplicationBuilder();
- var context = Mock.Of();
-
- builder.Services.AddSingleton(
- Mock.Of(x
- => x.CreateStore(It.IsAny()) == context));
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
- ["ai:clients:chat:apikey"] = "sk-asdfasdf",
- ["ai:agents:bot:client"] = "chat",
- ["ai:agents:bot:options:temperature"] = "0.5",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("bot");
- var options = agent.GetService();
-
- Assert.NotNull(options?.ChatMessageStoreFactory);
- Assert.Same(context, options?.ChatMessageStoreFactory?.Invoke(new()));
- }
-
- [Fact]
- public void CanSetOpenAIReasoningAndVerbosity()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:openai:modelid"] = "gpt-4.1",
- ["ai:clients:openai:apikey"] = "sk-asdfasdf",
- ["ai:agents:bot:client"] = "openai",
- ["ai:agents:bot:options:reasoningeffort"] = "minimal",
- ["ai:agents:bot:options:verbosity"] = "low",
- });
-
- builder.AddAIAgents();
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("bot");
- var options = agent.GetService();
-
- Assert.Equal(Verbosity.Low, options?.ChatOptions?.Verbosity);
- Assert.Equal(ReasoningEffort.Minimal, options?.ChatOptions?.ReasoningEffort);
- }
-
- //[Fact]
- //public void CanSetGrokOptions()
- //{
- // var builder = new HostApplicationBuilder();
-
- // builder.Configuration.AddInMemoryCollection(new Dictionary
- // {
- // ["ai:clients:grok:modelid"] = "grok-4",
- // ["ai:clients:grok:apikey"] = "xai-asdfasdf",
- // ["ai:clients:grok:endpoint"] = "https://api.x.ai",
- // ["ai:agents:bot:client"] = "grok",
- // ["ai:agents:bot:options:reasoningeffort"] = "low",
- // ["ai:agents:bot:options:search"] = "auto",
- // });
-
- // builder.AddAIAgents();
- // var app = builder.Build();
- // var agent = app.Services.GetRequiredKeyedService("bot");
- // var options = agent.GetService();
-
- // var grok = Assert.IsType(options?.ChatOptions);
-
- // Assert.Equal(ReasoningEffort.Low, grok.ReasoningEffort);
- // Assert.Equal(GrokSearch.Auto, grok.Search);
-
- // var configuration = (IConfigurationRoot)app.Services.GetRequiredService();
- // configuration["ai:agents:bot:options:reasoningeffort"] = "high";
- // configuration["ai:agents:bot:options:search"] = "off";
- // // NOTE: the in-memory provider does not support reload on change, so we must trigger it manually.
- // configuration.Reload();
-
- // options = agent.GetService();
- // grok = Assert.IsType(options?.ChatOptions);
-
- // Assert.Equal(ReasoningEffort.High, grok.ReasoningEffort);
- // Assert.Equal(GrokSearch.Off, grok.Search);
- //}
-
- [Fact]
- public void UseContextProviderFactoryFromKeyedService()
- {
- var builder = new HostApplicationBuilder();
- var context = Mock.Of();
-
- builder.Services.AddKeyedSingleton("bot",
- Mock.Of(x
- => x.CreateProvider(It.IsAny()) == context));
-
- builder.Configuration.AddInMemoryCollection(new Dictionary
- {
- ["ai:clients:chat:modelid"] = "gpt-4.1-nano",
- ["ai:clients:chat:apikey"] = "sk-asdfasdf",
- ["ai:agents:bot:client"] = "chat",
- ["ai:agents:bot:options:temperature"] = "0.5",
- });
-
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("bot");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
- Assert.Same(context, options?.AIContextProviderFactory?.Invoke(new ChatClientAgentOptions.AIContextProviderFactoryContext()));
- }
-
- [Fact]
- public async Task UseContextProviderFromKeyedServiceAsync()
- {
- var builder = new HostApplicationBuilder();
- var context = new AIContext();
-
- var provider = new Mock();
- provider
- .Setup(x => x.InvokingAsync(It.IsAny(), default(CancellationToken)))
- .ReturnsAsync(context);
-
- builder.Services.AddKeyedSingleton("chat", provider.Object);
-
- builder.Configuration.AddToml(
- """"
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- """");
-
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
-
- var actualProvider = options?.AIContextProviderFactory?.Invoke(new());
-
- Assert.NotNull(actualProvider);
-
- Assert.Same(context, await actualProvider.InvokingAsync(new([]), default));
- }
-
- [Fact]
- public void UseAndContextProviderFactoryIncompatible()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- """"
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["voseo"]
-
- [ai.context.voseo]
- instructions = 'Default to using spanish language, using argentinean "voseo" in your responses'
- """");
-
- builder.AddAIAgents(configureOptions: (name, options)
- => options.AIContextProviderFactory = context => Mock.Of());
-
- var app = builder.Build();
-
- var exception = Assert.ThrowsAny(() => app.Services.GetRequiredKeyedService("chat"));
-
- Assert.Contains("ai:agents:chat:use", exception.Message);
- }
-
- [Fact]
- public async Task UseAndContextProviderCompositeAsync()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- """"
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["voseo"]
-
- [ai.context.voseo]
- instructions = """\
- Default to using spanish language, using argentinean "voseo" in your responses \
- (unless the user explicitly talks in a different language). \
- This means using "vos" instead of "tú" and conjugating verbs accordingly. \
- Don't use the expression "pa'" instead of "para". Don't mention the word "voseo".
- """
- """");
-
- var context = new AIContext { Instructions = "foo" };
-
- var provider = new Mock();
- provider
- .Setup(x => x.InvokingAsync(It.IsAny(), default(CancellationToken)))
- .ReturnsAsync(context);
-
- builder.Services.AddKeyedSingleton("chat", provider.Object);
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("chat");
-
- var options = agent.GetService();
- Assert.NotNull(options?.AIContextProviderFactory);
-
- var actualProvider = options?.AIContextProviderFactory?.Invoke(new());
- Assert.NotNull(actualProvider);
-
- var actualContext = await actualProvider.InvokingAsync(new([]), default);
- Assert.Contains("spanish language", actualContext.Instructions);
- Assert.Contains("foo", actualContext.Instructions);
- }
-
- [Fact]
- public async Task UseAIContextFromKeyedServiceAsync()
- {
- var builder = new HostApplicationBuilder();
- var voseo = new AIContext { Instructions = "voseo" };
-
- builder.Configuration.AddToml(
- """"
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["voseo"]
- """");
-
- builder.Services.AddKeyedSingleton("voseo", voseo);
-
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
- var actualProvider = options?.AIContextProviderFactory?.Invoke(new());
- Assert.NotNull(actualProvider);
-
- var actualContext = await actualProvider.InvokingAsync(new([]), default);
-
- Assert.Same(voseo, await actualProvider.InvokingAsync(new([]), default));
- }
-
- [Fact]
- public async Task UseAggregatedAIContextsFromKeyedServiceAsync()
- {
- var builder = new HostApplicationBuilder();
- var voseo = new AIContext { Instructions = "voseo" };
- var formatting = new AIContext { Instructions = "formatting" };
-
- builder.Configuration.AddToml(
- """"
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["voseo", "formatting"]
- """");
-
- builder.Services.AddKeyedSingleton("voseo", voseo);
- builder.Services.AddKeyedSingleton("formatting", formatting);
-
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
- var actualProvider = options?.AIContextProviderFactory?.Invoke(new());
- Assert.NotNull(actualProvider);
-
- var actualContext = await actualProvider.InvokingAsync(new([]), default);
-
- Assert.StartsWith(voseo.Instructions, actualContext.Instructions);
- Assert.EndsWith(formatting.Instructions, actualContext.Instructions);
- }
-
- [Fact]
- public async Task UseAIToolFromKeyedServiceAsync()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- """"
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- tools = ["get_date"]
- """");
-
- AITool tool = AIFunctionFactory.Create(() => DateTimeOffset.Now, "get_date");
- builder.Services.AddKeyedSingleton("get_date", tool);
- builder.AddAIAgents();
-
- var app = builder.Build();
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
- var provider = options?.AIContextProviderFactory?.Invoke(new());
- Assert.NotNull(provider);
-
- var context = await provider.InvokingAsync(new([]), default);
-
- Assert.NotNull(context.Tools);
- Assert.Single(context.Tools);
- Assert.Same(tool, context.Tools[0]);
- }
-
- [Fact]
- public async Task MissingAIToolFromKeyedServiceThrows()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- $$"""
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- tools = ["get_date"]
- """);
-
- builder.AddAIAgents();
- var app = builder.Build();
-
- var exception = Assert.ThrowsAny(() => app.Services.GetRequiredKeyedService("chat"));
-
- Assert.Contains("get_date", exception.Message);
- Assert.Contains("ai:agents:chat", exception.Message);
- }
-
- [Fact]
- public async Task UseAIContextFromSection()
- {
- var builder = new HostApplicationBuilder();
- var voseo =
- """
- Default to using spanish language, using argentinean "voseo" in your responses.
- """;
-
- builder.Configuration.AddToml(
- $$"""
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["default"]
-
- [ai.context.default]
- instructions = '{{voseo}}'
- messages = [
- { system = "You are strictly professional." },
- { user = "Hey you!"},
- { assistant = "Hello there. How can I assist you today?" }
- ]
- tools = ["get_date"]
- """);
-
- var tool = AIFunctionFactory.Create(() => DateTimeOffset.Now, "get_date");
- builder.Services.AddKeyedSingleton("get_date", tool);
- builder.AddAIAgents();
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
- var provider = options?.AIContextProviderFactory?.Invoke(new());
- Assert.NotNull(provider);
-
- var context = await provider.InvokingAsync(new([]), default);
-
- Assert.NotNull(context.Instructions);
- Assert.Equal(voseo, context.Instructions);
- Assert.Equal(3, context.Messages?.Count);
- Assert.Single(context.Messages!, x => x.Role == ChatRole.System && x.Text == "You are strictly professional.");
- Assert.Single(context.Messages!, x => x.Role == ChatRole.User && x.Text == "Hey you!");
- Assert.Single(context.Messages!, x => x.Role == ChatRole.Assistant && x.Text == "Hello there. How can I assist you today?");
- Assert.Same(tool, context.Tools?.First());
- }
-
- [Fact]
- public async Task UseAIContextFromProvider()
- {
- var builder = new HostApplicationBuilder();
- var voseo =
- """
- Default to using spanish language, using argentinean "voseo" in your responses.
- """;
-
- builder.Configuration.AddToml(
- $$"""
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["default"]
- """);
-
- var tool = AIFunctionFactory.Create(() => DateTimeOffset.Now, "get_date");
- builder.Services.AddKeyedSingleton("default", Mock.Of(x
- => x.InvokingAsync(It.IsAny(), default) == ValueTask.FromResult(new AIContext
- {
- Instructions = voseo,
- Tools = new[] { tool }
- })));
-
- builder.AddAIAgents();
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
- var provider = options?.AIContextProviderFactory?.Invoke(new());
- Assert.NotNull(provider);
-
- var context = await provider.InvokingAsync(new([]), default);
-
- Assert.NotNull(context.Instructions);
- Assert.Equal(voseo, context.Instructions);
- Assert.Same(tool, context.Tools?.First());
- }
-
- [Fact]
- public async Task CombineAIContextFromStaticDinamicAndSection()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- $$"""
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["default", "static", "dynamic"]
-
- [ai.context.default]
- instructions = 'foo'
- messages = [
- { system = "You are strictly professional." },
- { user = "Hey you!"},
- { assistant = "Hello there. How can I assist you today?" }
- ]
- tools = ["get_date"]
- """);
-
- var tool = AIFunctionFactory.Create(() => DateTimeOffset.Now, "get_date");
- builder.Services.AddKeyedSingleton("get_date", tool);
-
- builder.Services.AddKeyedSingleton("static", new AIContext
- {
- Instructions = "bar",
- Tools = new AITool[] { AIFunctionFactory.Create(() => "bar", "get_bar") }
- });
-
- AITool[] getbaz = [AIFunctionFactory.Create(() => "baz", "get_baz")];
-
- builder.Services.AddKeyedSingleton("dynamic", Mock.Of(x
- => x.InvokingAsync(It.IsAny(), default) == ValueTask.FromResult(new AIContext
- {
- Instructions = "baz",
- Tools = getbaz
- })));
-
- builder.AddAIAgents();
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.NotNull(options?.AIContextProviderFactory);
- var provider = options?.AIContextProviderFactory?.Invoke(new());
- Assert.NotNull(provider);
-
- var context = await provider.InvokingAsync(new([]), default);
-
- Assert.NotNull(context.Instructions);
- Assert.Contains("foo", context.Instructions);
- Assert.Contains("bar", context.Instructions);
- Assert.Contains("baz", context.Instructions);
-
- Assert.Equal(3, context.Messages?.Count);
- Assert.Single(context.Messages!, x => x.Role == ChatRole.System && x.Text == "You are strictly professional.");
- Assert.Single(context.Messages!, x => x.Role == ChatRole.User && x.Text == "Hey you!");
- Assert.Single(context.Messages!, x => x.Role == ChatRole.Assistant && x.Text == "Hello there. How can I assist you today?");
-
- Assert.NotNull(context.Tools);
- Assert.Contains(tool, context.Tools!);
- Assert.Contains(context.Tools, x => x.Name == "get_bar");
- Assert.Contains(context.Tools, x => x.Name == "get_baz");
- }
-
- [Fact]
- public async Task MissingToolAIContextFromSectionThrows()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- $$"""
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["default"]
-
- [ai.context.default]
- tools = ["get_date"]
- """);
-
- builder.AddAIAgents();
- var app = builder.Build();
-
- var exception = Assert.ThrowsAny(() => app.Services.GetRequiredKeyedService("chat"));
-
- Assert.Contains("get_date", exception.Message);
- Assert.Contains("ai:context:default:tools", exception.Message);
- Assert.Contains("ai:agents:chat", exception.Message);
- }
-
- [Fact]
- public async Task UnknownUseThrows()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- $$"""
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat agent."
- client = "openai"
- use = ["foo"]
- """);
-
- builder.AddAIAgents();
- var app = builder.Build();
-
- var exception = Assert.ThrowsAny(() => app.Services.GetRequiredKeyedService("chat"));
-
- Assert.Contains("foo", exception.Message);
- }
-
- [Fact]
- public async Task OverrideModelFromAgentChatOptions()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- $$"""
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat"
- client = "openai"
- options = { modelid = "gpt-5" }
- """);
-
- builder.AddAIAgents();
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.Equal("gpt-5", options?.ChatOptions?.ModelId);
- }
-
- [Fact]
- public async Task OverrideModelFromAgentModel()
- {
- var builder = new HostApplicationBuilder();
-
- builder.Configuration.AddToml(
- $$"""
- [ai.clients.openai]
- modelid = "gpt-4.1"
- apikey = "sk-asdf"
-
- [ai.agents.chat]
- description = "Chat"
- client = "openai"
- model = "gpt-5"
- """);
-
- builder.AddAIAgents();
- var app = builder.Build();
-
- var agent = app.Services.GetRequiredKeyedService("chat");
- var options = agent.GetService();
-
- Assert.Equal("gpt-5", options?.ChatOptions?.ModelId);
- }
-}
-
diff --git a/src/Tests/Misc.cs b/src/Tests/Misc.cs
deleted file mode 100644
index bf0fad3..0000000
--- a/src/Tests/Misc.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Text;
-using Devlooped.Agents.AI;
-using Microsoft.Extensions.AI;
-using Microsoft.Extensions.Configuration;
-
-namespace Devlooped;
-
-public class Misc
-{
- [Fact]
- public void AddMarkdown()
- {
- var markdown =
- """
- ---
- id: ai.agents.tests
- name: TestAgent
- description: Test agent
- options:
- temperature: 0.7
- use: ["foo", "bar"]
- ---
- Hello world
- """;
-
- var configuration = new ConfigurationBuilder()
- .AddAgentMarkdown(new MemoryStream(Encoding.UTF8.GetBytes(markdown)))
- .Build();
-
- Assert.Equal("TestAgent", configuration["ai:agents:tests:name"]);
- Assert.Equal("Hello world", configuration["ai:agents:tests:instructions"]);
-
- var agent = configuration.GetSection("ai:agents:tests").Get();
-
- Assert.NotNull(agent);
- Assert.Equal("TestAgent", agent.Name);
- Assert.Equal("Test agent", agent.Description);
- Assert.Equal(0.7f, agent.Options?.Temperature);
- Assert.Equal(["foo", "bar"], agent.Use);
- Assert.Equal("Hello world", agent.Instructions);
- }
-
- record AgentConfig(string Name, string Description, string Instructions, ChatOptions? Options, List Use);
-}
diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj
index cefafa6..0057abe 100644
--- a/src/Tests/Tests.csproj
+++ b/src/Tests/Tests.csproj
@@ -31,7 +31,6 @@
-