Skip to content

Commit eda707f

Browse files
authored
.Net Agents - Fix AIContext instruction formatting for ChatCompletionAgent streaming invocation (#12444)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Additional instruction ``Microsoft.SemanticKernel.AIContext` being added for streaming invocation of `ChatCompletionAgent` ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> - Centralize all formatting of additional instructions - Remove typo that results in `Microsoft.SemanticKernel.AIContext` additional instruction for streaming `ChatCompletionAgent` - Clarify type usage ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1 parent 62e647f commit eda707f

File tree

5 files changed

+81
-82
lines changed

5 files changed

+81
-82
lines changed

dotnet/src/Agents/Abstractions/Agent.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,4 +384,38 @@ protected Task NotifyThreadOfNewMessage(AgentThread thread, ChatMessageContent m
384384
{
385385
return thread.OnNewMessageAsync(message, cancellationToken);
386386
}
387+
388+
/// <summary>
389+
/// Default formatting for additional instructions for the AI agent based on the provided context and invocation options.
390+
/// </summary>
391+
/// <param name="context">The context containing relevant information for the AI agent's operation.</param>
392+
/// <param name="options">Optional parameters that influence the invocation behavior. Can be <see langword="null"/>.</param>
393+
/// <returns>A formatted string representing the additional instructions for the AI agent.</returns>
394+
#pragma warning disable SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
395+
protected static string FormatAdditionalInstructions(AIContext context, AgentInvokeOptions? options)
396+
#pragma warning restore SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
397+
{
398+
return string.Concat(ProcessInstructions());
399+
400+
IEnumerable<string> ProcessInstructions()
401+
{
402+
bool hasInstructions = false;
403+
if (options?.AdditionalInstructions is not null)
404+
{
405+
yield return options!.AdditionalInstructions;
406+
hasInstructions = true;
407+
}
408+
409+
if (!string.IsNullOrWhiteSpace(context.Instructions))
410+
{
411+
if (hasInstructions)
412+
{
413+
yield return Environment.NewLine;
414+
yield return Environment.NewLine;
415+
}
416+
417+
yield return context.Instructions!;
418+
}
419+
}
420+
}
387421
}

dotnet/src/Agents/AzureAI/AzureAIAgent.cs

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System;
43
using System.Collections.Generic;
54
using System.Runtime.CompilerServices;
65
using System.Threading;
@@ -131,21 +130,21 @@ public async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync
131130
{
132131
Verify.NotNull(messages);
133132

134-
var azureAIAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
133+
AzureAIAgentThread azureAIAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
135134
messages,
136135
thread,
137136
() => new AzureAIAgentThread(this.Client),
138137
cancellationToken).ConfigureAwait(false);
139138

140-
var kernel = (options?.Kernel ?? this.Kernel).Clone();
139+
Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();
141140

142141
// Get the context contributions from the AIContextProviders.
143-
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
144-
var providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
142+
#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
143+
AIContext providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
145144
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
146-
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
145+
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
147146

148-
var mergedAdditionalInstructions = MergeAdditionalInstructions(options?.AdditionalInstructions, providersContext.Instructions);
147+
string mergedAdditionalInstructions = FormatAdditionalInstructions(providersContext, options);
149148
var extensionsContextOptions = options is null ?
150149
new AzureAIAgentInvokeOptions() { AdditionalInstructions = mergedAdditionalInstructions } :
151150
new AzureAIAgentInvokeOptions(options) { AdditionalInstructions = mergedAdditionalInstructions };
@@ -223,7 +222,7 @@ public async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> In
223222
{
224223
Verify.NotNull(messages);
225224

226-
var azureAIAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
225+
AzureAIAgentThread azureAIAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
227226
messages,
228227
thread,
229228
() => new AzureAIAgentThread(this.Client),
@@ -232,19 +231,19 @@ public async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> In
232231
var kernel = (options?.Kernel ?? this.Kernel).Clone();
233232

234233
// Get the context contributions from the AIContextProviders.
235-
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
236-
var providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
234+
#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
235+
AIContext providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
237236
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
238-
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
237+
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
239238

240-
var mergedAdditionalInstructions = MergeAdditionalInstructions(options?.AdditionalInstructions, providersContext.Instructions);
239+
string mergedAdditionalInstructions = FormatAdditionalInstructions(providersContext, options);
241240
var extensionsContextOptions = options is null ?
242241
new AzureAIAgentInvokeOptions() { AdditionalInstructions = mergedAdditionalInstructions } :
243242
new AzureAIAgentInvokeOptions(options) { AdditionalInstructions = mergedAdditionalInstructions };
244243

245244
// Invoke the Agent with the thread that we already added our message to, and with
246245
// a chat history to receive complete messages.
247-
var newMessagesReceiver = new ChatHistory();
246+
ChatHistory newMessagesReceiver = [];
248247
var invokeResults = ActivityExtensions.RunWithActivityAsync(
249248
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description),
250249
() => AgentThreadActions.InvokeStreamingAsync(
@@ -324,19 +323,4 @@ protected override async Task<AgentChannel> RestoreChannelAsync(string channelSt
324323

325324
return new AzureAIChannel(this.Client, thread.Id);
326325
}
327-
328-
private static string MergeAdditionalInstructions(string? optionsAdditionalInstructions, string? extensionsContext) =>
329-
(optionsAdditionalInstructions, extensionsContext) switch
330-
{
331-
(string ai, string ec) when !string.IsNullOrWhiteSpace(ai) && !string.IsNullOrWhiteSpace(ec) => string.Concat(
332-
ai,
333-
Environment.NewLine,
334-
Environment.NewLine,
335-
ec),
336-
(string ai, string ec) when string.IsNullOrWhiteSpace(ai) => ec,
337-
(string ai, string ec) when string.IsNullOrWhiteSpace(ec) => ai,
338-
(null, string ec) => ec,
339-
(string ai, null) => ai,
340-
_ => string.Empty
341-
};
342326
}

dotnet/src/Agents/Bedrock/BedrockAgent.cs

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,16 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In
121121
cancellationToken).ConfigureAwait(false);
122122

123123
// Get the context contributions from the AIContextProviders.
124-
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
125-
var providersContext = await bedrockThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
126-
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
124+
#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
125+
AIContext providersContext = await bedrockThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
126+
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
127127

128128
// Ensure that the last message provided is a user message
129129
string message = this.ExtractUserMessage(messages.Last());
130130

131131
// Build session state with conversation history and override instructions if needed
132132
SessionState sessionState = this.ExtractSessionState(messages);
133-
var mergedAdditionalInstructions = MergeAdditionalInstructions(options?.AdditionalInstructions, providersContext.Instructions);
133+
string mergedAdditionalInstructions = FormatAdditionalInstructions(providersContext, options);
134134
sessionState.PromptSessionAttributes = new() { [AdditionalInstructionsSessionAttributeName] = mergedAdditionalInstructions };
135135

136136
// Configure the agent request with the provided options
@@ -196,7 +196,7 @@ public async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync
196196
thread = new BedrockAgentThread(this.RuntimeClient, invokeAgentRequest.SessionId);
197197
}
198198

199-
var bedrockThread = await this.EnsureThreadExistsWithMessagesAsync(
199+
BedrockAgentThread bedrockThread = await this.EnsureThreadExistsWithMessagesAsync(
200200
[],
201201
thread,
202202
() => new BedrockAgentThread(this.RuntimeClient),
@@ -255,23 +255,23 @@ public override async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageCon
255255
}
256256

257257
// Create a thread if needed
258-
var bedrockThread = await this.EnsureThreadExistsWithMessagesAsync(
258+
BedrockAgentThread bedrockThread = await this.EnsureThreadExistsWithMessagesAsync(
259259
messages,
260260
thread,
261261
() => new BedrockAgentThread(this.RuntimeClient),
262262
cancellationToken).ConfigureAwait(false);
263263

264264
// Get the context contributions from the AIContextProviders.
265-
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
266-
var providersContext = await bedrockThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
267-
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
265+
#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
266+
AIContext providersContext = await bedrockThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
267+
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
268268

269269
// Ensure that the last message provided is a user message
270270
string? message = this.ExtractUserMessage(messages.Last());
271271

272272
// Build session state with conversation history and override instructions if needed
273273
SessionState sessionState = this.ExtractSessionState(messages);
274-
var mergedAdditionalInstructions = MergeAdditionalInstructions(options?.AdditionalInstructions, providersContext.Instructions);
274+
string mergedAdditionalInstructions = FormatAdditionalInstructions(providersContext, options);
275275
sessionState.PromptSessionAttributes = new() { [AdditionalInstructionsSessionAttributeName] = mergedAdditionalInstructions };
276276

277277
// Configure the agent request with the provided options
@@ -579,20 +579,5 @@ private Amazon.BedrockAgentRuntime.ConversationRole MapBedrockAgentUser(AuthorRo
579579
throw new ArgumentOutOfRangeException($"Invalid role: {authorRole}");
580580
}
581581

582-
private static string MergeAdditionalInstructions(string? optionsAdditionalInstructions, string? extensionsContext) =>
583-
(optionsAdditionalInstructions, extensionsContext) switch
584-
{
585-
(string ai, string ec) when !string.IsNullOrWhiteSpace(ai) && !string.IsNullOrWhiteSpace(ec) => string.Concat(
586-
ai,
587-
Environment.NewLine,
588-
Environment.NewLine,
589-
ec),
590-
(string ai, string ec) when string.IsNullOrWhiteSpace(ai) => ec,
591-
(string ai, string ec) when string.IsNullOrWhiteSpace(ec) => ai,
592-
(null, string ec) => ec,
593-
(string ai, null) => ai,
594-
_ => string.Empty
595-
};
596-
597582
#endregion
598583
}

dotnet/src/Agents/Core/ChatCompletionAgent.cs

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,22 +67,22 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In
6767
{
6868
Verify.NotNull(messages);
6969

70-
var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
70+
ChatHistoryAgentThread chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
7171
messages,
7272
thread,
7373
() => new ChatHistoryAgentThread(),
7474
cancellationToken).ConfigureAwait(false);
7575

76-
var kernel = (options?.Kernel ?? this.Kernel).Clone();
76+
Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();
7777

7878
// Get the context contributions from the AIContextProviders.
79-
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
80-
var providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
79+
#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
80+
AIContext providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
8181
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
82-
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
82+
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
8383

8484
// Invoke Chat Completion with the updated chat history.
85-
var chatHistory = new ChatHistory();
85+
ChatHistory chatHistory = [];
8686
await foreach (var existingMessage in chatHistoryAgentThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false))
8787
{
8888
chatHistory.Add(existingMessage);
@@ -100,9 +100,7 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In
100100
},
101101
options?.KernelArguments,
102102
kernel,
103-
options?.AdditionalInstructions == null ?
104-
providersContext.Instructions :
105-
string.Concat(options.AdditionalInstructions, Environment.NewLine, Environment.NewLine, providersContext.Instructions),
103+
FormatAdditionalInstructions(providersContext, options),
106104
cancellationToken);
107105

108106
// Notify the thread of new messages and return them to the caller.
@@ -164,22 +162,22 @@ public override async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageCon
164162
{
165163
Verify.NotNull(messages);
166164

167-
var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
165+
ChatHistoryAgentThread chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
168166
messages,
169167
thread,
170168
() => new ChatHistoryAgentThread(),
171169
cancellationToken).ConfigureAwait(false);
172170

173-
var kernel = (options?.Kernel ?? this.Kernel).Clone();
171+
Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();
174172

175173
// Get the context contributions from the AIContextProviders.
176-
#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
177-
var providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
174+
#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
175+
AIContext providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
178176
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
179-
#pragma warning restore SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
177+
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
180178

181179
// Invoke Chat Completion with the updated chat history.
182-
var chatHistory = new ChatHistory();
180+
ChatHistory chatHistory = [];
183181
await foreach (var existingMessage in chatHistoryAgentThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false))
184182
{
185183
chatHistory.Add(existingMessage);
@@ -198,9 +196,7 @@ public override async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageCon
198196
},
199197
options?.KernelArguments,
200198
kernel,
201-
options?.AdditionalInstructions == null ?
202-
providersContext.Instructions :
203-
string.Concat(options.AdditionalInstructions, Environment.NewLine, Environment.NewLine, providersContext),
199+
FormatAdditionalInstructions(providersContext, options),
204200
cancellationToken);
205201

206202
await foreach (var result in invokeResults.ConfigureAwait(false))

0 commit comments

Comments
 (0)