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
9 changes: 9 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,15 @@ 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<NotesTools>();

// 👇 Reuse same tool definitions in agents
builder.AddAIAgents().WithTools<NotesTools>();
```

<!-- #agents -->

Expand Down
29 changes: 29 additions & 0 deletions sample/Server/NotesTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.Agents.AI;
using ModelContextProtocol.Server;

public class NotesContextProvider(NotesTools notes) : AIContextProvider
{
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(new AIContext
{
Instructions =
$"""
Your current state is:
<notes>
${notes.GetNotes()}
</notes>
"""
});
}

[McpServerToolType]
public class NotesTools
{
string notes = "";

[McpServerTool]
public string GetNotes() => notes;

[McpServerTool]
public void SaveNotes(string notes) => this.notes = notes;
}
16 changes: 12 additions & 4 deletions sample/Server/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
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 ModelContextProtocol.Server;
using Spectre.Console;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -23,10 +26,15 @@
// 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("save_notes", AIFunctionFactory.Create((string notes) => true, "save_notes"));

builder.Services.AddKeyedSingleton<AIContextProvider, NotesContextProvider>("notes");

// 👇 seamless integration of MCP tools
//builder.Services.AddMcpServer().WithTools<NotesTools>();

// 👇 implicitly calls AddChatClients
builder.AddAIAgents();
builder.AddAIAgents()
.WithTools<NotesTools>();

var app = builder.Build();

Expand Down Expand Up @@ -76,4 +84,4 @@
});
}

app.Run();
app.Run();
2 changes: 2 additions & 0 deletions sample/Server/Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.OpenAI" Version="1.0.0-alpha.251016.1" />
<PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="ThisAssembly.Project" Version="2.1.2" PrivateAssets="all" />
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.6" />
Expand Down
29 changes: 26 additions & 3 deletions sample/Server/notes.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,30 @@ id: ai.agents.notes
description: Provides free-form memory
client: grok
model: grok-4-fast
use: ["tone"]
tools: ["save_notes", "get_date"]
use: ["tone", "notes"]
tools: ["get_notes", "save_notes", "get_date"]
---
You organize and keep notes for the user, using JSON-LD
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.
19 changes: 15 additions & 4 deletions src/Agents/Agents.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,29 @@
<NoWarn>$(NoWarn);CS0436;SYSLIB1100;SYSLIB1101;MEAI001</NoWarn>
</PropertyGroup>

<ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
<PackageReference Include="NuGetizer" Version="1.4.5" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-rc.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-rc.*" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.*" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.*" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251016.1" />
<PackageReference Include="Microsoft.Agents.AI.Hosting" Version="1.0.0-preview.251016.1" />
<PackageReference Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.251016.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251016.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.3" />
<PackageReference Include="PolySharp" Version="1.15.0" PrivateAssets="all" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
<PackageReference Include="NuGetizer" Version="1.4.5" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/Agents/CompositeAIContextProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ public override async ValueTask<AIContext> InvokingAsync(InvokingContext invokin
if (staticContext is not null)
return staticContext;

if (providers.Count == 1)
return await providers[0].InvokingAsync(invoking, cancellationToken);

var context = new AIContext();
var instructions = new List<string>();
var messages = new List<ChatMessage>();
Expand Down
25 changes: 13 additions & 12 deletions src/Agents/ConfigurableAIAgent.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System.ComponentModel;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json;
using Devlooped.Extensions.AI;
using Devlooped.Extensions.AI.Grok;
Expand All @@ -8,6 +12,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;

namespace Devlooped.Agents.AI;

Expand Down Expand Up @@ -134,21 +139,17 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum

if (contextFactory is not null)
{
if (options.Use?.Count > 0)
throw new InvalidOperationException($"Invalid simultaneous use of keyed service {nameof(AIContextProviderFactory)} and '{section}:use' in configuration.");
if (options.Use?.Count > 0 || options.Tools?.Count > 0)
throw new InvalidOperationException($"Invalid simultaneous use of keyed service {nameof(AIContextProviderFactory)} and '{section}:use/tools' in configuration.");

options.AIContextProviderFactory = contextFactory.CreateProvider;
}
else if (services.GetKeyedService<AIContextProvider>(name) is { } contextProvider)
{
if (options.Use?.Count > 0)
throw new InvalidOperationException($"Invalid simultaneous use of keyed service {nameof(AIContextProvider)} and '{section}:use' in configuration.");

options.AIContextProviderFactory = _ => contextProvider;
}
else if (options.Use?.Count > 0 || options.Tools?.Count > 0)
else
{
var contexts = new List<AIContextProvider>();
if (services.GetKeyedService<AIContextProvider>(name) is { } contextProvider)
contexts.Add(contextProvider);

foreach (var use in options.Use ?? [])
{
if (services.GetKeyedService<AIContext>(use) is { } staticContext)
Expand Down Expand Up @@ -196,7 +197,7 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
{
var tool = services.GetKeyedService<AITool>(toolName) ??
services.GetKeyedService<AIFunction>(toolName) ??
throw new InvalidOperationException($"Specified tool '{toolName}' for agent '{section}' is not registered as a keyed {nameof(AITool)} or {nameof(AIFunction)}.");
throw new InvalidOperationException($"Specified tool '{toolName}' for agent '{section}' is not registered as a keyed {nameof(AITool)}, {nameof(AIFunction)} or MCP server tools.");

contexts.Add(new StaticAIContextProvider(new AIContext { Tools = [tool] }));
}
Expand Down
55 changes: 52 additions & 3 deletions src/Agents/ConfigurableAgentsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using Devlooped.Agents.AI;
using Devlooped.Extensions.AI;
using Microsoft.Agents.AI;
Expand All @@ -7,6 +10,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;

namespace Microsoft.Extensions.DependencyInjection;

Expand All @@ -16,6 +20,52 @@ namespace Microsoft.Extensions.DependencyInjection;
[EditorBrowsable(EditorBrowsableState.Never)]
public static class ConfigurableAgentsExtensions
{
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <typeparam name="TToolType">The tool type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This method discovers all instance and static methods (public and non-public) on the specified <typeparamref name="TToolType"/>
/// type, where the methods are attributed as <see cref="McpServerToolAttribute"/>, and adds an <see cref="AIFunction"/>
/// instance for each. For instance methods, an instance will be constructed for each invocation of the tool.
/// </remarks>
public static IAIAgentsBuilder WithTools<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicConstructors)] TToolType>(
this IAIAgentsBuilder builder,
JsonSerializerOptions? serializerOptions = null,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);

// Preserve existing registration if any, such as when using Devlooped.Extensions.DependencyInjection
// via [Service] attribute or by convention.
builder.Services.TryAdd(ServiceDescriptor.Describe(typeof(TToolType), typeof(TToolType), lifetime));

serializerOptions ??= ToolJsonOptions.Default;

foreach (var toolMethod in typeof(TToolType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is { } toolAttribute)
{
var function = toolMethod.IsStatic
? AIFunctionFactory.Create(toolMethod, null, toolAttribute.Name ?? ToolJsonOptions.Default.PropertyNamingPolicy!.ConvertName(toolMethod.Name))
: AIFunctionFactory.Create(toolMethod, args => args.Services?.GetRequiredService(typeof(TToolType)) ??
throw new InvalidOperationException("Could not determine target instance for tool."),
new AIFunctionFactoryOptions { Name = toolAttribute.Name ?? ToolJsonOptions.Default.PropertyNamingPolicy!.ConvertName(toolMethod.Name) });

builder.Services.TryAdd(ServiceDescriptor.DescribeKeyed(
typeof(AIFunction), function.Name,
(_, _) => function, lifetime));
}
}

return builder;
}

/// <summary>
/// Adds AI agents to the host application builder based on configuration.
/// </summary>
Expand All @@ -24,8 +74,7 @@ public static class ConfigurableAgentsExtensions
/// <param name="configureOptions">Optional action to configure options for each agent.</param>
/// <param name="prefix">The configuration prefix for agents, defaults to "ai:agents".</param>
/// <returns>The host application builder with AI agents added.</returns>
public static TBuilder AddAIAgents<TBuilder>(this TBuilder builder, Action<string, AIAgentBuilder>? configurePipeline = default, Action<string, ChatClientAgentOptions>? configureOptions = default, string prefix = "ai:agents")
where TBuilder : IHostApplicationBuilder
public static IAIAgentsBuilder AddAIAgents(this IHostApplicationBuilder builder, Action<string, AIAgentBuilder>? configurePipeline = default, Action<string, ChatClientAgentOptions>? configureOptions = default, string prefix = "ai:agents")
{
builder.AddChatClients();

Expand Down Expand Up @@ -61,7 +110,7 @@ public static TBuilder AddAIAgents<TBuilder>(this TBuilder builder, Action<strin
=> sp.GetRequiredKeyedService<AIAgent>(name)));
}

return builder;
return new DefaultAIAgentsBuilder(builder);
}

/// <summary>Gets an AI agent by name (case-insensitive) from the service provider.</summary>
Expand Down
29 changes: 29 additions & 0 deletions src/Agents/IAIAgentsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Metrics;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Devlooped.Agents.AI;

/// <summary>Provides a mechanism to configure AI agents.</summary>
public interface IAIAgentsBuilder : IHostApplicationBuilder
{
}

class DefaultAIAgentsBuilder(IHostApplicationBuilder builder) : IAIAgentsBuilder
{
public IDictionary<object, object> Properties => builder.Properties;

public IConfigurationManager Configuration => builder.Configuration;

public IHostEnvironment Environment => builder.Environment;

public ILoggingBuilder Logging => builder.Logging;

public IMetricsBuilder Metrics => builder.Metrics;

public IServiceCollection Services => builder.Services;

public void ConfigureContainer<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory, Action<TContainerBuilder>? configure = null) where TContainerBuilder : notnull => builder.ConfigureContainer(factory, configure);
}
14 changes: 12 additions & 2 deletions src/Extensions/Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,22 @@
<PackageReference Include="NuGetizer" Version="1.4.5" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.10.0" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
<PackageReference Include="Spectre.Console" Version="0.52.0" />
<PackageReference Include="Spectre.Console.Json" Version="0.52.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="Spectre.Console" Version="0.52.0" />
<PackageReference Include="Spectre.Console.Json" Version="0.52.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-rc.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-rc.*" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.*" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.*" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading