Skip to content

.Net Agents - Fix AIContext instruction formatting for ChatCompletionAgent streaming invocation #12444

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 11, 2025
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
34 changes: 34 additions & 0 deletions dotnet/src/Agents/Abstractions/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,4 +384,38 @@ protected Task NotifyThreadOfNewMessage(AgentThread thread, ChatMessageContent m
{
return thread.OnNewMessageAsync(message, cancellationToken);
}

/// <summary>
/// Default formatting for additional instructions for the AI agent based on the provided context and invocation options.
/// </summary>
/// <param name="context">The context containing relevant information for the AI agent's operation.</param>
/// <param name="options">Optional parameters that influence the invocation behavior. Can be <see langword="null"/>.</param>
/// <returns>A formatted string representing the additional instructions for the AI agent.</returns>
#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.
protected static string FormatAdditionalInstructions(AIContext context, AgentInvokeOptions? options)
#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.
{
return string.Concat(ProcessInstructions());

IEnumerable<string> ProcessInstructions()
{
bool hasInstructions = false;
if (options?.AdditionalInstructions is not null)
{
yield return options!.AdditionalInstructions;
hasInstructions = true;
}

if (!string.IsNullOrWhiteSpace(context.Instructions))
{
if (hasInstructions)
{
yield return Environment.NewLine;
yield return Environment.NewLine;
}

yield return context.Instructions!;
}
}
}
}
40 changes: 12 additions & 28 deletions dotnet/src/Agents/AzureAI/AzureAIAgent.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
Expand Down Expand Up @@ -131,21 +130,21 @@ public async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync
{
Verify.NotNull(messages);

var azureAIAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
AzureAIAgentThread azureAIAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
messages,
thread,
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);

var kernel = (options?.Kernel ?? this.Kernel).Clone();
Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();

// Get the context contributions from the AIContextProviders.
#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.
var providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
#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.
AIContext providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#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.
#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.

var mergedAdditionalInstructions = MergeAdditionalInstructions(options?.AdditionalInstructions, providersContext.Instructions);
string mergedAdditionalInstructions = FormatAdditionalInstructions(providersContext, options);
var extensionsContextOptions = options is null ?
new AzureAIAgentInvokeOptions() { AdditionalInstructions = mergedAdditionalInstructions } :
new AzureAIAgentInvokeOptions(options) { AdditionalInstructions = mergedAdditionalInstructions };
Expand Down Expand Up @@ -223,7 +222,7 @@ public async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> In
{
Verify.NotNull(messages);

var azureAIAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
AzureAIAgentThread azureAIAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
messages,
thread,
() => new AzureAIAgentThread(this.Client),
Expand All @@ -232,19 +231,19 @@ public async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> In
var kernel = (options?.Kernel ?? this.Kernel).Clone();

// Get the context contributions from the AIContextProviders.
#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.
var providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
#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.
AIContext providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#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.
#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.

var mergedAdditionalInstructions = MergeAdditionalInstructions(options?.AdditionalInstructions, providersContext.Instructions);
string mergedAdditionalInstructions = FormatAdditionalInstructions(providersContext, options);
var extensionsContextOptions = options is null ?
new AzureAIAgentInvokeOptions() { AdditionalInstructions = mergedAdditionalInstructions } :
new AzureAIAgentInvokeOptions(options) { AdditionalInstructions = mergedAdditionalInstructions };

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

return new AzureAIChannel(this.Client, thread.Id);
}

private static string MergeAdditionalInstructions(string? optionsAdditionalInstructions, string? extensionsContext) =>
(optionsAdditionalInstructions, extensionsContext) switch
{
(string ai, string ec) when !string.IsNullOrWhiteSpace(ai) && !string.IsNullOrWhiteSpace(ec) => string.Concat(
ai,
Environment.NewLine,
Environment.NewLine,
ec),
(string ai, string ec) when string.IsNullOrWhiteSpace(ai) => ec,
(string ai, string ec) when string.IsNullOrWhiteSpace(ec) => ai,
(null, string ec) => ec,
(string ai, null) => ai,
_ => string.Empty
};
}
35 changes: 10 additions & 25 deletions dotnet/src/Agents/Bedrock/BedrockAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,16 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In
cancellationToken).ConfigureAwait(false);

// Get the context contributions from the AIContextProviders.
#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.
var providersContext = await bedrockThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
#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.
#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.
AIContext providersContext = await bedrockThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
#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.

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

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

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

var bedrockThread = await this.EnsureThreadExistsWithMessagesAsync(
BedrockAgentThread bedrockThread = await this.EnsureThreadExistsWithMessagesAsync(
[],
thread,
() => new BedrockAgentThread(this.RuntimeClient),
Expand Down Expand Up @@ -255,23 +255,23 @@ public override async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageCon
}

// Create a thread if needed
var bedrockThread = await this.EnsureThreadExistsWithMessagesAsync(
BedrockAgentThread bedrockThread = await this.EnsureThreadExistsWithMessagesAsync(
messages,
thread,
() => new BedrockAgentThread(this.RuntimeClient),
cancellationToken).ConfigureAwait(false);

// Get the context contributions from the AIContextProviders.
#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.
var providersContext = await bedrockThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
#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.
#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.
AIContext providersContext = await bedrockThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
#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.

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

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

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

private static string MergeAdditionalInstructions(string? optionsAdditionalInstructions, string? extensionsContext) =>
(optionsAdditionalInstructions, extensionsContext) switch
{
(string ai, string ec) when !string.IsNullOrWhiteSpace(ai) && !string.IsNullOrWhiteSpace(ec) => string.Concat(
ai,
Environment.NewLine,
Environment.NewLine,
ec),
(string ai, string ec) when string.IsNullOrWhiteSpace(ai) => ec,
(string ai, string ec) when string.IsNullOrWhiteSpace(ec) => ai,
(null, string ec) => ec,
(string ai, null) => ai,
_ => string.Empty
};

#endregion
}
32 changes: 14 additions & 18 deletions dotnet/src/Agents/Core/ChatCompletionAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,22 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In
{
Verify.NotNull(messages);

var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
ChatHistoryAgentThread chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
messages,
thread,
() => new ChatHistoryAgentThread(),
cancellationToken).ConfigureAwait(false);

var kernel = (options?.Kernel ?? this.Kernel).Clone();
Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();

// Get the context contributions from the AIContextProviders.
#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.
var providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
#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.
AIContext providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#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.
#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.

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

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

var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
ChatHistoryAgentThread chatHistoryAgentThread = await this.EnsureThreadExistsWithMessagesAsync(
messages,
thread,
() => new ChatHistoryAgentThread(),
cancellationToken).ConfigureAwait(false);

var kernel = (options?.Kernel ?? this.Kernel).Clone();
Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();

// Get the context contributions from the AIContextProviders.
#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.
var providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
#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.
AIContext providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#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.
#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.

// Invoke Chat Completion with the updated chat history.
var chatHistory = new ChatHistory();
ChatHistory chatHistory = [];
await foreach (var existingMessage in chatHistoryAgentThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false))
{
chatHistory.Add(existingMessage);
Expand All @@ -198,9 +196,7 @@ public override async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageCon
},
options?.KernelArguments,
kernel,
options?.AdditionalInstructions == null ?
providersContext.Instructions :
string.Concat(options.AdditionalInstructions, Environment.NewLine, Environment.NewLine, providersContext),
FormatAdditionalInstructions(providersContext, options),
cancellationToken);

await foreach (var result in invokeResults.ConfigureAwait(false))
Expand Down
Loading
Loading