Skip to content

Commit 9566520

Browse files
committed
Make markdown configuration compatible with VSC chat modes
See https://code.visualstudio.com/docs/copilot/customization/custom-chat-modes (seems to have been renamed to "custom agents" in VSC insiders now). Our extensions (such as `client`, `use` and `options` can be persisted in other config files (since they are aggregated at run-time) or kept in the same agents.md file since VSCode will ignore them.
1 parent 8da697a commit 9566520

File tree

5 files changed

+110
-28
lines changed

5 files changed

+110
-28
lines changed

assets/img/agent-model.png

46.2 KB
Loading

sample/Server/ai.toml

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,16 @@ instructions = """\
1111
You are an AI agent responsible for processing orders for food or other items.
1212
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.
1313
"""
14+
options = { modelid = "gpt-4o-mini" }
15+
# 👇 alternative syntax to specify options
16+
# [ai.agents.orders.options]
17+
# modelid = "gpt-4o-mini"
1418

1519
# ai.clients.openai, can omit the ai.clients prefix
1620
client = "openai"
17-
use = ["tone", "get_date", "create_order", "cancel_order"]
21+
use = ["tone"]
22+
tools = ["get_date", "create_order", "cancel_order"]
1823

19-
[ai.agents.orders.options]
20-
modelid = "gpt-4o-mini"
21-
# additional properties could be added here
22-
23-
[ai.agents.notes]
24-
description = "Help users create, manage, and retrieve notes effectively."
25-
instructions = """
26-
You are an AI agent that assists users in creating, managing, and retrieving notes.
27-
Your primary goals are to understand user requests related to notes, provide clear and concise responses, and utilize tools to organize and access note data efficiently.
28-
"""
29-
client = "grok"
30-
use = ["tone", "save_notes", "get_date"]
3124

3225
[ai.context.tone]
3326
instructions = """\

sample/Server/notes.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
---
22
id: ai.agents.notes
33
description: Provides free-form memory
4-
client: Grok
5-
options:
6-
modelid: grok-4-fast
4+
client: grok
5+
model: grok-4-fast
6+
use: ["tone"]
7+
tools: ["save_notes", "get_date"]
78
---
89
You organize and keep notes for the user, using JSON-LD

src/Agents/ConfigurableAIAgent.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
106106
if (chat is not null)
107107
options.ChatOptions = chat;
108108

109+
if (options.Model is not null)
110+
{
111+
options.ChatOptions ??= new();
112+
options.ChatOptions.ModelId = options.Model;
113+
}
114+
109115
configure?.Invoke(name, options);
110116

111117
if (options.AIContextProviderFactory is null)
@@ -127,10 +133,10 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
127133

128134
options.AIContextProviderFactory = _ => contextProvider;
129135
}
130-
else if (options.Use?.Count > 0)
136+
else if (options.Use?.Count > 0 || options.Tools?.Count > 0)
131137
{
132138
var contexts = new List<AIContext>();
133-
foreach (var use in options.Use)
139+
foreach (var use in options.Use ?? [])
134140
{
135141
var context = services.GetKeyedService<AIContext>(use);
136142
if (context is not null)
@@ -139,13 +145,6 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
139145
continue;
140146
}
141147

142-
var function = services.GetKeyedService<AITool>(use) ?? services.GetKeyedService<AIFunction>(use);
143-
if (function is not null)
144-
{
145-
contexts.Add(new AIContext { Tools = [function] });
146-
continue;
147-
}
148-
149148
if (configuration.GetSection("ai:context:" + use) is { } ctxSection &&
150149
ctxSection.Get<AIContextConfiguration>() is { } ctxConfig)
151150
{
@@ -161,7 +160,7 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
161160
{
162161
var tool = services.GetKeyedService<AITool>(toolName) ??
163162
services.GetKeyedService<AIFunction>(toolName) ??
164-
throw new InvalidOperationException($"Specified tool '{toolName}' for AI context '{ctxSection.Path}:tools' is not registered, and is required by agent section '{configSection.Path}'.");
163+
throw new InvalidOperationException($"Specified tool '{toolName}' for AI context '{ctxSection.Path}:tools' is not registered as a keyed {nameof(AITool)} or {nameof(AIFunction)}, and is required by agent section '{configSection.Path}'.");
165164

166165
configured.Tools ??= [];
167166
configured.Tools.Add(tool);
@@ -172,7 +171,16 @@ public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnum
172171
continue;
173172
}
174173

175-
throw new InvalidOperationException($"Specified AI context '{use}' for agent '{name}' is not registered as either {nameof(AIContent)}, {nameof(AITool)} or configuration section 'ai:context:{use}'.");
174+
throw new InvalidOperationException($"Specified AI context '{use}' for agent '{name}' is not registered as either {nameof(AIContent)} or configuration section 'ai:context:{use}'.");
175+
}
176+
177+
foreach (var toolName in options.Tools ?? [])
178+
{
179+
var tool = services.GetKeyedService<AITool>(toolName) ??
180+
services.GetKeyedService<AIFunction>(toolName) ??
181+
throw new InvalidOperationException($"Specified tool '{toolName}' for agent '{section}' is not registered as a keyed {nameof(AITool)} or {nameof(AIFunction)}.");
182+
183+
contexts.Add(new AIContext { Tools = [tool] });
176184
}
177185

178186
options.AIContextProviderFactory = _ => new CompositeAIContextProvider(contexts);
@@ -215,7 +223,9 @@ void OnReload(object? state)
215223
internal class AgentClientOptions : ChatClientAgentOptions
216224
{
217225
public string? Client { get; set; }
226+
public string? Model { get; set; }
218227
public IList<string>? Use { get; set; }
228+
public IList<string>? Tools { get; set; }
219229
}
220230
}
221231

src/Tests/ConfigurableAgentTests.cs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ public async Task UseAIToolFromKeyedServiceAsync()
539539
[ai.agents.chat]
540540
description = "Chat agent."
541541
client = "openai"
542-
use = ["get_date"]
542+
tools = ["get_date"]
543543
"""");
544544

545545
AITool tool = AIFunctionFactory.Create(() => DateTimeOffset.Now, "get_date");
@@ -561,6 +561,32 @@ public async Task UseAIToolFromKeyedServiceAsync()
561561
Assert.Same(tool, context.Tools[0]);
562562
}
563563

564+
[Fact]
565+
public async Task MissingAIToolFromKeyedServiceThrows()
566+
{
567+
var builder = new HostApplicationBuilder();
568+
569+
builder.Configuration.AddToml(
570+
$$"""
571+
[ai.clients.openai]
572+
modelid = "gpt-4.1"
573+
apikey = "sk-asdf"
574+
575+
[ai.agents.chat]
576+
description = "Chat agent."
577+
client = "openai"
578+
tools = ["get_date"]
579+
""");
580+
581+
builder.AddAIAgents();
582+
var app = builder.Build();
583+
584+
var exception = Assert.ThrowsAny<Exception>(() => app.Services.GetRequiredKeyedService<AIAgent>("chat"));
585+
586+
Assert.Contains("get_date", exception.Message);
587+
Assert.Contains("ai:agents:chat", exception.Message);
588+
}
589+
564590
[Fact]
565591
public async Task UseAIContextFromSection()
566592
{
@@ -668,5 +694,57 @@ public async Task UnknownUseThrows()
668694

669695
Assert.Contains("foo", exception.Message);
670696
}
697+
698+
[Fact]
699+
public async Task OverrideModelFromAgentChatOptions()
700+
{
701+
var builder = new HostApplicationBuilder();
702+
703+
builder.Configuration.AddToml(
704+
$$"""
705+
[ai.clients.openai]
706+
modelid = "gpt-4.1"
707+
apikey = "sk-asdf"
708+
709+
[ai.agents.chat]
710+
description = "Chat"
711+
client = "openai"
712+
options = { modelid = "gpt-5" }
713+
""");
714+
715+
builder.AddAIAgents();
716+
var app = builder.Build();
717+
718+
var agent = app.Services.GetRequiredKeyedService<AIAgent>("chat");
719+
var options = agent.GetService<ChatClientAgentOptions>();
720+
721+
Assert.Equal("gpt-5", options?.ChatOptions?.ModelId);
722+
}
723+
724+
[Fact]
725+
public async Task OverrideModelFromAgentModel()
726+
{
727+
var builder = new HostApplicationBuilder();
728+
729+
builder.Configuration.AddToml(
730+
$$"""
731+
[ai.clients.openai]
732+
modelid = "gpt-4.1"
733+
apikey = "sk-asdf"
734+
735+
[ai.agents.chat]
736+
description = "Chat"
737+
client = "openai"
738+
model = "gpt-5"
739+
""");
740+
741+
builder.AddAIAgents();
742+
var app = builder.Build();
743+
744+
var agent = app.Services.GetRequiredKeyedService<AIAgent>("chat");
745+
var options = agent.GetService<ChatClientAgentOptions>();
746+
747+
Assert.Equal("gpt-5", options?.ChatOptions?.ModelId);
748+
}
671749
}
672750

0 commit comments

Comments
 (0)