-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
.Net Agents -
AgentChat
Serialization (#7457)
### 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. --> Support ability to capture and restore `AgentChat` history: https://github.com/microsoft/semantic-kernel/blob/main/docs/decisions/0048-agent-chat-serialization.md ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Introduces `AgentChatSerializer` that supports serialization and deserialization of entire conversation state. ```json { "Participants": [ { "Id": "asst_YaLY1TRmxsIReVa0BiuhEDAj", "Name": "agent-1", "Type": "Microsoft.SemanticKernel.Agents.OpenAI.OpenAIAssistantAgent" }, { "Id": "1e884890-c344-4bc0-9367-068c482a499d", "Name": "agent-2", "Type": "Microsoft.SemanticKernel.Agents.ChatCompletionAgent" } ], "History": [{"Role":{"Label":"user"},"Items":[{"$type":"TextContent","Text":"..."}],"ModelId":"gpt-35-turbo-16k"},{"Role":{"Label":"assistant"},"Items":[{"$type":"TextContent","Text":"..."}],"ModelId":"gpt-35-turbo-16k"}], "Channels": [ { "ChannelKey": "4kjDzpCpeOLUNbsSUisHd9cphSzEAb6Hxdnmr\u002Bem1Jw=", "ChannelType": "Microsoft.SemanticKernel.Agents.OpenAI.OpenAIAssistantChannel", "ChannelState": "thread_ZSF1ovTVzyYy8cg9GEwmDWgy" }, { "ChannelKey": "Vdx37EnWT9BS\u002BkkCkEgFCg9uHvHNw1\u002BhXMA4sgNMKs4=", "ChannelType": "Microsoft.SemanticKernel.Agents.ChatHistoryChannel", "ChannelState": [{"Role":{"Label":"user"},"Items":[{"$type":"TextContent","Text":"..."}],"ModelId":"gpt-35-turbo-16k"},{"Role":{"Label":"assistant"},"Items":[{"$type":"TextContent","Text":"..."}],"ModelId":"gpt-35-turbo-16k"}] } ] } ``` Includes sample showing serialization and deserialization with `ChatCompletionAgent` and `OpenAIAssistantAgent` partipating on the same chat, as well as a chat that includes `ChatCompletionAgent` that calls a plug-in. ### 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 😄 --------- Co-authored-by: Ben Thomas <[email protected]>
- Loading branch information
Showing
25 changed files
with
990 additions
and
6 deletions.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
dotnet/samples/Concepts/Agents/ChatCompletion_Serialization.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
using System.ComponentModel; | ||
using Microsoft.SemanticKernel; | ||
using Microsoft.SemanticKernel.Agents; | ||
using Microsoft.SemanticKernel.ChatCompletion; | ||
using Microsoft.SemanticKernel.Connectors.OpenAI; | ||
|
||
namespace Agents; | ||
/// <summary> | ||
/// Demonstrate that serialization of <see cref="AgentGroupChat"/> in with a <see cref="ChatCompletionAgent"/> participant. | ||
/// </summary> | ||
public class ChatCompletion_Serialization(ITestOutputHelper output) : BaseAgentsTest(output) | ||
{ | ||
private const string HostName = "Host"; | ||
private const string HostInstructions = "Answer questions about the menu."; | ||
|
||
[Fact] | ||
public async Task SerializeAndRestoreAgentGroupChatAsync() | ||
{ | ||
// Define the agent | ||
ChatCompletionAgent agent = | ||
new() | ||
{ | ||
Instructions = HostInstructions, | ||
Name = HostName, | ||
Kernel = this.CreateKernelWithChatCompletion(), | ||
Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), | ||
}; | ||
|
||
// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). | ||
KernelPlugin plugin = KernelPluginFactory.CreateFromType<MenuPlugin>(); | ||
agent.Kernel.Plugins.Add(plugin); | ||
|
||
AgentGroupChat chat = CreateGroupChat(); | ||
|
||
// Invoke chat and display messages. | ||
Console.WriteLine("============= Dynamic Agent Chat - Primary (prior to serialization) =============="); | ||
await InvokeAgentAsync(chat, "Hello"); | ||
await InvokeAgentAsync(chat, "What is the special soup?"); | ||
|
||
AgentGroupChat copy = CreateGroupChat(); | ||
Console.WriteLine("\n=========== Serialize and restore the Agent Chat into a new instance ============"); | ||
await CloneChatAsync(chat, copy); | ||
|
||
Console.WriteLine("\n============ Continue with the dynamic Agent Chat (after deserialization) ==============="); | ||
await InvokeAgentAsync(copy, "What is the special drink?"); | ||
await InvokeAgentAsync(copy, "Thank you"); | ||
|
||
Console.WriteLine("\n============ The entire Agent Chat (includes messages prior to serialization and those after deserialization) =============="); | ||
await foreach (ChatMessageContent content in copy.GetChatMessagesAsync()) | ||
{ | ||
this.WriteAgentChatMessage(content); | ||
} | ||
|
||
// Local function to invoke agent and display the conversation messages. | ||
async Task InvokeAgentAsync(AgentGroupChat chat, string input) | ||
{ | ||
ChatMessageContent message = new(AuthorRole.User, input); | ||
chat.AddChatMessage(message); | ||
|
||
this.WriteAgentChatMessage(message); | ||
|
||
await foreach (ChatMessageContent content in chat.InvokeAsync()) | ||
{ | ||
this.WriteAgentChatMessage(content); | ||
} | ||
} | ||
|
||
async Task CloneChatAsync(AgentGroupChat source, AgentGroupChat clone) | ||
{ | ||
await using MemoryStream stream = new(); | ||
await AgentChatSerializer.SerializeAsync(source, stream); | ||
|
||
stream.Position = 0; | ||
using StreamReader reader = new(stream); | ||
Console.WriteLine(await reader.ReadToEndAsync()); | ||
|
||
stream.Position = 0; | ||
AgentChatSerializer serializer = await AgentChatSerializer.DeserializeAsync(stream); | ||
await serializer.DeserializeAsync(clone); | ||
} | ||
|
||
AgentGroupChat CreateGroupChat() => new(agent); | ||
} | ||
|
||
private sealed class MenuPlugin | ||
{ | ||
[KernelFunction, Description("Provides a list of specials from the menu.")] | ||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] | ||
public string GetSpecials() => | ||
""" | ||
Special Soup: Clam Chowder | ||
Special Salad: Cobb Salad | ||
Special Drink: Chai Tea | ||
"""; | ||
|
||
[KernelFunction, Description("Provides the price of the requested menu item.")] | ||
public string GetItemPrice( | ||
[Description("The name of the menu item.")] | ||
string menuItem) => | ||
"$9.99"; | ||
} | ||
} |
128 changes: 128 additions & 0 deletions
128
dotnet/samples/Concepts/Agents/MixedChat_Serialization.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
using Microsoft.SemanticKernel; | ||
using Microsoft.SemanticKernel.Agents; | ||
using Microsoft.SemanticKernel.Agents.Chat; | ||
using Microsoft.SemanticKernel.Agents.OpenAI; | ||
using Microsoft.SemanticKernel.ChatCompletion; | ||
|
||
namespace Agents; | ||
/// <summary> | ||
/// Demonstrate the serialization of <see cref="AgentGroupChat"/> with a <see cref="ChatCompletionAgent"/> | ||
/// and an <see cref="OpenAIAssistantAgent"/>. | ||
/// </summary> | ||
public class MixedChat_Serialization(ITestOutputHelper output) : BaseAgentsTest(output) | ||
{ | ||
private const string TranslatorName = "Translator"; | ||
private const string TranslatorInstructions = | ||
""" | ||
Spell the last number in chat as a word in english and spanish on a single line without any line breaks. | ||
"""; | ||
|
||
private const string CounterName = "Counter"; | ||
private const string CounterInstructions = | ||
""" | ||
Increment the last number from your most recent response. | ||
Never repeat the same number. | ||
Only respond with a single number that is the result of your calculation without explanation. | ||
"""; | ||
|
||
[Fact] | ||
public async Task SerializeAndRestoreAgentGroupChatAsync() | ||
{ | ||
// Define the agents: one of each type | ||
ChatCompletionAgent agentTranslator = | ||
new() | ||
{ | ||
Instructions = TranslatorInstructions, | ||
Name = TranslatorName, | ||
Kernel = this.CreateKernelWithChatCompletion(), | ||
}; | ||
|
||
OpenAIAssistantAgent agentCounter = | ||
await OpenAIAssistantAgent.CreateAsync( | ||
kernel: new(), | ||
clientProvider: this.GetClientProvider(), | ||
definition: new(this.Model) | ||
{ | ||
Instructions = CounterInstructions, | ||
Name = CounterName, | ||
}); | ||
|
||
AgentGroupChat chat = CreateGroupChat(); | ||
|
||
// Invoke chat and display messages. | ||
ChatMessageContent input = new(AuthorRole.User, "1"); | ||
chat.AddChatMessage(input); | ||
this.WriteAgentChatMessage(input); | ||
|
||
Console.WriteLine("============= Dynamic Agent Chat - Primary (prior to serialization) =============="); | ||
await InvokeAgents(chat); | ||
|
||
AgentGroupChat copy = CreateGroupChat(); | ||
Console.WriteLine("\n=========== Serialize and restore the Agent Chat into a new instance ============"); | ||
await CloneChatAsync(chat, copy); | ||
|
||
Console.WriteLine("\n============ Continue with the dynamic Agent Chat (after deserialization) ==============="); | ||
await InvokeAgents(copy); | ||
|
||
Console.WriteLine("\n============ The entire Agent Chat (includes messages prior to serialization and those after deserialization) =============="); | ||
await foreach (ChatMessageContent content in copy.GetChatMessagesAsync()) | ||
{ | ||
this.WriteAgentChatMessage(content); | ||
} | ||
|
||
async Task InvokeAgents(AgentGroupChat chat) | ||
{ | ||
await foreach (ChatMessageContent content in chat.InvokeAsync()) | ||
{ | ||
this.WriteAgentChatMessage(content); | ||
} | ||
} | ||
|
||
async Task CloneChatAsync(AgentGroupChat source, AgentGroupChat clone) | ||
{ | ||
await using MemoryStream stream = new(); | ||
await AgentChatSerializer.SerializeAsync(source, stream); | ||
|
||
stream.Position = 0; | ||
using StreamReader reader = new(stream); | ||
Console.WriteLine(await reader.ReadToEndAsync()); | ||
|
||
stream.Position = 0; | ||
AgentChatSerializer serializer = await AgentChatSerializer.DeserializeAsync(stream); | ||
await serializer.DeserializeAsync(clone); | ||
} | ||
|
||
AgentGroupChat CreateGroupChat() => | ||
new(agentTranslator, agentCounter) | ||
{ | ||
ExecutionSettings = | ||
new() | ||
{ | ||
TerminationStrategy = | ||
new CountingTerminationStrategy(5) | ||
{ | ||
// Only the art-director may approve. | ||
Agents = [agentTranslator], | ||
// Limit total number of turns | ||
MaximumIterations = 20, | ||
} | ||
} | ||
}; | ||
} | ||
|
||
private sealed class CountingTerminationStrategy(int maxTurns) : TerminationStrategy | ||
{ | ||
private int _count = 0; | ||
|
||
protected override Task<bool> ShouldAgentTerminateAsync(Agent agent, IReadOnlyList<ChatMessageContent> history, CancellationToken cancellationToken) | ||
{ | ||
++this._count; | ||
|
||
bool shouldTerminate = this._count >= maxTurns; | ||
|
||
return Task.FromResult(shouldTerminate); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.