Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e46238e
Checkpoint
crickman Jul 10, 2024
7f963a5
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 10, 2024
0e91ce5
Checkpoint
crickman Jul 11, 2024
a6840a7
Resolve merge from main
crickman Jul 11, 2024
6c2e2a6
Remove local test harness
crickman Jul 11, 2024
b146347
Checkpoint
crickman Jul 12, 2024
2ac076a
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 12, 2024
699b745
Namespace
crickman Jul 12, 2024
7f2d1af
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 12, 2024
6ffeb0c
Update mock channels
crickman Jul 12, 2024
bbde9cd
Merge branch 'fix_agent_history_propagation' of https://github.com/mi…
crickman Jul 12, 2024
194f39c
Update
crickman Jul 12, 2024
1ec70fd
Real UT fix
crickman Jul 12, 2024
dc53a80
Clean
crickman Jul 15, 2024
4d3cf9f
namespace
crickman Jul 15, 2024
7af86fd
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 15, 2024
dd57cab
Clean-up
crickman Jul 15, 2024
58817d8
Namespace
crickman Jul 15, 2024
3571d53
Checkpoint
crickman Jul 15, 2024
27fead0
Sync test mocks
crickman Jul 15, 2024
d30a9ce
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 15, 2024
a1833d0
Logic clean-up
crickman Jul 15, 2024
0ca0197
Comment clean-up
crickman Jul 15, 2024
1ed5439
Assistant logic fix
crickman Jul 15, 2024
af28337
Typo
crickman Jul 15, 2024
f4dadf3
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 15, 2024
c8b034a
Fix logic fix ;)
crickman Jul 15, 2024
07fc67e
One more
crickman Jul 15, 2024
1fb4ad9
Add integration test
crickman Jul 15, 2024
712f64e
Loop execution protection
crickman Jul 16, 2024
cc7bd3c
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 16, 2024
970ad3d
Respond to comments
crickman Jul 16, 2024
d72e085
Another comment
crickman Jul 16, 2024
1f2a328
Added sample
crickman Jul 16, 2024
2118b9e
Comment
crickman Jul 16, 2024
0de6c6a
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 16, 2024
dea8588
Merge branch 'main' into fix_agent_history_propagation
crickman Jul 17, 2024
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
158 changes: 158 additions & 0 deletions dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// 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 usage of <see cref="IAutoFunctionInvocationFilter"/> for both direction invocation
/// of <see cref="ChatCompletionAgent"/> and via <see cref="AgentChat"/>.
/// </summary>
public class ChatCompletion_FunctionTermination(ITestOutputHelper output) : BaseTest(output)
{
[Fact]
public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync()
{
// Define the agent
ChatCompletionAgent agent =
new()
{
Instructions = "Answer questions about the menu.",
Kernel = CreateKernelWithChatCompletion(),
ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions },
};

KernelPlugin plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();
agent.Kernel.Plugins.Add(plugin);

/// Create the chat history to capture the agent interaction.
ChatHistory chat = [];

// Respond to user input, invoking functions where appropriate.
await InvokeAgentAsync("Hello");
await InvokeAgentAsync("What is the special soup?");
await InvokeAgentAsync("What is the special drink?");
await InvokeAgentAsync("Thank you");

// Display the chat history.
Console.WriteLine("================================");
Console.WriteLine("CHAT HISTORY");
Console.WriteLine("================================");
foreach (ChatMessageContent message in chat)
{
this.WriteContent(message);
}

// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
ChatMessageContent userContent = new(AuthorRole.User, input);
chat.Add(userContent);
this.WriteContent(userContent);

await foreach (ChatMessageContent content in agent.InvokeAsync(chat))
{
// Do not add a message implicitly added to the history.
if (!content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent))
{
chat.Add(content);
}

this.WriteContent(content);
}
}
}

[Fact]
public async Task UseAutoFunctionInvocationFilterWithAgentChatAsync()
{
// Define the agent
ChatCompletionAgent agent =
new()
{
Instructions = "Answer questions about the menu.",
Kernel = CreateKernelWithChatCompletion(),
ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions },
};

KernelPlugin plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();
agent.Kernel.Plugins.Add(plugin);

// Create a chat for agent interaction.
AgentGroupChat chat = new();

// Respond to user input, invoking functions where appropriate.
await InvokeAgentAsync("Hello");
await InvokeAgentAsync("What is the special soup?");
await InvokeAgentAsync("What is the special drink?");
await InvokeAgentAsync("Thank you");

// Display the chat history.
Console.WriteLine("================================");
Console.WriteLine("CHAT HISTORY");
Console.WriteLine("================================");
ChatMessageContent[] history = await chat.GetChatMessagesAsync().ToArrayAsync();
for (int index = history.Length; index > 0; --index)
{
this.WriteContent(history[index - 1]);
}

// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
ChatMessageContent userContent = new(AuthorRole.User, input);
chat.AddChatMessage(userContent);
this.WriteContent(userContent);

await foreach (ChatMessageContent content in chat.InvokeAsync(agent))
{
this.WriteContent(content);
}
}
}

private void WriteContent(ChatMessageContent content)
{
Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'");
}

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()
{
return @"
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)
{
return "$9.99";
}
}

private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter
{
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
// Execution the function
await next(context);

// Signal termination if the function is from the MenuPlugin
if (context.Function.PluginName == nameof(MenuPlugin))
{
context.Terminate = terminate;
}
}
}
}
102 changes: 76 additions & 26 deletions dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

namespace Agents;

Expand Down Expand Up @@ -30,40 +32,88 @@ public async Task UseStreamingChatCompletionAgentAsync()
ChatHistory chat = [];

// Respond to user input
await InvokeAgentAsync("Fortune favors the bold.");
await InvokeAgentAsync("I came, I saw, I conquered.");
await InvokeAgentAsync("Practice makes perfect.");
await InvokeAgentAsync(agent, chat, "Fortune favors the bold.");
await InvokeAgentAsync(agent, chat, "I came, I saw, I conquered.");
await InvokeAgentAsync(agent, chat, "Practice makes perfect.");
}

// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
chat.Add(new ChatMessageContent(AuthorRole.User, input));
[Fact]
public async Task UseStreamingChatCompletionAgentWithPluginAsync()
{
const string MenuInstructions = "Answer questions about the menu.";

// Define the agent
ChatCompletionAgent agent =
new()
{
Name = "Host",
Instructions = MenuInstructions,
Kernel = this.CreateKernelWithChatCompletion(),
ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions },
};

// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage).
KernelPlugin plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();
agent.Kernel.Plugins.Add(plugin);

ChatHistory chat = [];

// Respond to user input
await InvokeAgentAsync(agent, chat, "What is the special soup?");
await InvokeAgentAsync(agent, chat, "What is the special drink?");
}

// Local function to invoke agent and display the conversation messages.
private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, string input)
{
chat.Add(new ChatMessageContent(AuthorRole.User, input));

Console.WriteLine($"# {AuthorRole.User}: '{input}'");
Console.WriteLine($"# {AuthorRole.User}: '{input}'");

StringBuilder builder = new();
await foreach (StreamingChatMessageContent message in agent.InvokeStreamingAsync(chat))
StringBuilder builder = new();
await foreach (StreamingChatMessageContent message in agent.InvokeStreamingAsync(chat))
{
if (string.IsNullOrEmpty(message.Content))
{
if (string.IsNullOrEmpty(message.Content))
{
continue;
}

if (builder.Length == 0)
{
Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}:");
}

Console.WriteLine($"\t > streamed: '{message.Content}'");
builder.Append(message.Content);
continue;
}

if (builder.Length > 0)
if (builder.Length == 0)
{
// Display full response and capture in chat history
Console.WriteLine($"\t > complete: '{builder}'");
chat.Add(new ChatMessageContent(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name });
Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}:");
}

Console.WriteLine($"\t > streamed: '{message.Content}'");
builder.Append(message.Content);
}

if (builder.Length > 0)
{
// Display full response and capture in chat history
Console.WriteLine($"\t > complete: '{builder}'");
chat.Add(new ChatMessageContent(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name });
}
}

public 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()
{
return @"
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)
{
return "$9.99";
}
}
}
8 changes: 4 additions & 4 deletions dotnet/src/Agents/Abstractions/AgentChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public abstract class AgentChannel
/// </summary>
/// <param name="history">The chat history at the point the channel is created.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
protected internal abstract Task ReceiveAsync(IReadOnlyList<ChatMessageContent> history, CancellationToken cancellationToken = default);
protected internal abstract Task ReceiveAsync(IEnumerable<ChatMessageContent> history, CancellationToken cancellationToken = default);

/// <summary>
/// Perform a discrete incremental interaction between a single <see cref="Agent"/> and <see cref="AgentChat"/>.
/// </summary>
/// <param name="agent">The agent actively interacting with the chat.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>Asynchronous enumeration of messages.</returns>
protected internal abstract IAsyncEnumerable<ChatMessageContent> InvokeAsync(
protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(
Agent agent,
CancellationToken cancellationToken = default);

Expand Down Expand Up @@ -59,12 +59,12 @@ public abstract class AgentChannel<TAgent> : AgentChannel where TAgent : Agent
/// <param name="agent">The agent actively interacting with the chat.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>Asynchronous enumeration of messages.</returns>
protected internal abstract IAsyncEnumerable<ChatMessageContent> InvokeAsync(
protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(
TAgent agent,
CancellationToken cancellationToken = default);

/// <inheritdoc/>
protected internal override IAsyncEnumerable<ChatMessageContent> InvokeAsync(
protected internal override IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(
Agent agent,
CancellationToken cancellationToken = default)
{
Expand Down
18 changes: 9 additions & 9 deletions dotnet/src/Agents/Abstractions/AgentChat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,22 +209,21 @@ protected async IAsyncEnumerable<ChatMessageContent> InvokeAgentAsync(

// Invoke agent & process response
List<ChatMessageContent> messages = [];
await foreach (ChatMessageContent message in channel.InvokeAsync(agent, cancellationToken).ConfigureAwait(false))

await foreach ((bool isVisible, ChatMessageContent message) in channel.InvokeAsync(agent, cancellationToken).ConfigureAwait(false))
{
this.Logger.LogAgentChatInvokedAgentMessage(nameof(InvokeAgentAsync), agent.GetType(), agent.Id, message);

messages.Add(message);

// Add to primary history
this.History.Add(message);
messages.Add(message);

// Don't expose function-call and function-result messages to caller.
if (message.Items.All(i => i is FunctionCallContent || i is FunctionResultContent))
if (isVisible)
{
continue;
// Yield message to caller
yield return message;
}

// Yield message to caller
yield return message;
}

// Broadcast message to other channels (in parallel)
Expand All @@ -233,7 +232,7 @@ protected async IAsyncEnumerable<ChatMessageContent> InvokeAgentAsync(
this._agentChannels
.Where(kvp => kvp.Value != channel)
.Select(kvp => new ChannelReference(kvp.Value, kvp.Key));
this._broadcastQueue.Enqueue(channelRefs, messages.Where(m => m.Role != AuthorRole.Tool).ToArray());
this._broadcastQueue.Enqueue(channelRefs, messages);

this.Logger.LogAgentChatInvokedAgent(nameof(InvokeAgentAsync), agent.GetType(), agent.Id);
}
Expand All @@ -256,6 +255,7 @@ async Task<AgentChannel> GetOrCreateChannelAsync()

if (this.History.Count > 0)
{
// Sync channel with existing history
await channel.ReceiveAsync(this.History, cancellationToken).ConfigureAwait(false);
}

Expand Down
8 changes: 4 additions & 4 deletions dotnet/src/Agents/Abstractions/AggregatorChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ protected internal override IAsyncEnumerable<ChatMessageContent> GetHistoryAsync
return this._chat.GetChatMessagesAsync(cancellationToken);
}

protected internal override async IAsyncEnumerable<ChatMessageContent> InvokeAsync(AggregatorAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default)
protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(AggregatorAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ChatMessageContent? lastMessage = null;

Expand All @@ -27,7 +27,7 @@ protected internal override async IAsyncEnumerable<ChatMessageContent> InvokeAsy
// For AggregatorMode.Flat, the entire aggregated chat is merged into the owning chat.
if (agent.Mode == AggregatorMode.Flat)
{
yield return message;
yield return (IsVisible: true, message);
}

lastMessage = message;
Expand All @@ -43,11 +43,11 @@ protected internal override async IAsyncEnumerable<ChatMessageContent> InvokeAsy
AuthorName = agent.Name
};

yield return message;
yield return (IsVisible: true, message);
}
}

protected internal override Task ReceiveAsync(IReadOnlyList<ChatMessageContent> history, CancellationToken cancellationToken = default)
protected internal override Task ReceiveAsync(IEnumerable<ChatMessageContent> history, CancellationToken cancellationToken = default)
{
// Always receive the initial history from the owning chat.
this._chat.AddChatMessages([.. history]);
Expand Down
Loading