Skip to content

Commit b52e08b

Browse files
committed
Add support for configurable and composable AI contexts
In order to make configured agents more dynamic and extensible, we introduce the `use` setting which is an array of named AI contexts to include as part of the agent execution. These cannot be used at the same time with AIContextProvider (for now?) to simplify usage. You either pick exporting your own context provider (or factory), or you rely on more granular and reusable AIContext exported services.
1 parent d576d5f commit b52e08b

File tree

5 files changed

+342
-32
lines changed

5 files changed

+342
-32
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Microsoft.Agents.AI;
2+
using Microsoft.Extensions.AI;
3+
4+
namespace Devlooped.Agents.AI;
5+
6+
/// <summary>
7+
/// Concatenates multiple <see cref="AIContext"/> instances into a single one.
8+
/// </summary>
9+
class CompositeAIContextProvider : AIContextProvider
10+
{
11+
readonly AIContext context;
12+
13+
public CompositeAIContextProvider(IList<AIContext> contexts)
14+
{
15+
if (contexts.Count == 1)
16+
{
17+
context = contexts[0];
18+
return;
19+
}
20+
21+
// Concatenate instructions from all contexts
22+
context = new();
23+
var instructions = new List<string>();
24+
var messages = new List<ChatMessage>();
25+
var tools = new List<AITool>();
26+
27+
foreach (var ctx in contexts)
28+
{
29+
if (!string.IsNullOrEmpty(ctx.Instructions))
30+
instructions.Add(ctx.Instructions);
31+
32+
if (ctx.Messages != null)
33+
messages.AddRange(ctx.Messages);
34+
35+
if (ctx.Tools != null)
36+
tools.AddRange(ctx.Tools);
37+
}
38+
39+
// Same separator used by M.A.AI for instructions appending from AIContext
40+
if (instructions.Count > 0)
41+
context.Instructions = string.Join('\n', instructions);
42+
43+
if (messages.Count > 0)
44+
context.Messages = messages;
45+
46+
if (tools.Count > 0)
47+
context.Tools = tools;
48+
}
49+
50+
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
51+
=> ValueTask.FromResult(this.context);
52+
}

src/Agents/ConfigurableAIAgent.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,45 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
113113
services.GetService<AIContextProviderFactory>();
114114

115115
if (contextFactory is not null)
116+
{
117+
if (options.Use?.Count > 0)
118+
throw new InvalidOperationException($"Invalid simultaneous use of keyed service {nameof(AIContextProviderFactory)} and '{section}:use' in configuration.");
119+
116120
options.AIContextProviderFactory = contextFactory.CreateProvider;
121+
}
122+
else if (services.GetKeyedService<AIContextProvider>(name) is { } contextProvider)
123+
{
124+
if (options.Use?.Count > 0)
125+
throw new InvalidOperationException($"Invalid simultaneous use of keyed service {nameof(AIContextProvider)} and '{section}:use' in configuration.");
126+
127+
options.AIContextProviderFactory = _ => contextProvider;
128+
}
129+
else if (options.Use?.Count > 0)
130+
{
131+
var contexts = new List<AIContext>();
132+
foreach (var use in options.Use)
133+
{
134+
var context = services.GetKeyedService<AIContext>(use);
135+
if (context is null)
136+
{
137+
var function = services.GetKeyedService<AITool>(use) ??
138+
services.GetKeyedService<AIFunction>(use) ??
139+
throw new InvalidOperationException($"Specified AI context '{use}' for agent '{name}' is not registered as either an {nameof(AIContent)} or an {nameof(AITool)}.");
140+
141+
contexts.Add(new AIContext { Tools = [function] });
142+
}
143+
else
144+
{
145+
contexts.Add(context);
146+
}
147+
}
148+
149+
options.AIContextProviderFactory = _ => new CompositeAIContextProvider(contexts);
150+
}
151+
}
152+
else if (options.Use?.Count > 0)
153+
{
154+
throw new InvalidOperationException($"Invalid simultaneous use of {nameof(ChatClientAgentOptions)}.{nameof(ChatClientAgentOptions.AIContextProviderFactory)} and '{section}:use' in configuration.");
117155
}
118156

119157
if (options.ChatMessageStoreFactory is null)
@@ -148,6 +186,7 @@ void OnReload(object? state)
148186
internal class AgentClientOptions : ChatClientAgentOptions
149187
{
150188
public string? Client { get; set; }
189+
public IList<string>? Use { get; set; }
151190
}
152191
}
153192

0 commit comments

Comments
 (0)