From e46238ebec59c4ba14bb31541da32862b2756e91 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 10 Jul 2024 14:32:27 -0700 Subject: [PATCH 01/26] Checkpoint --- .../Agents/ChatCompletion_Streaming.cs | 103 +++++++++++++----- .../src/Agents/Abstractions/AgentChannel.cs | 2 +- dotnet/src/Agents/Abstractions/AgentChat.cs | 12 +- .../Agents/Abstractions/AggregatorChannel.cs | 2 +- .../Agents/Abstractions/ChatHistoryChannel.cs | 16 ++- dotnet/src/Agents/Core/ChatCompletionAgent.cs | 18 +-- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 2 +- 7 files changed, 112 insertions(+), 43 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index ee6fb9b38f2a..fc1fadf0f446 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; using System.Text; +using Microsoft.Graph; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; namespace Agents; @@ -30,40 +33,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(); + 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"; } } } diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index ad58deedb017..6abe382cc24c 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -23,7 +23,7 @@ public abstract class AgentChannel /// /// The chat history at the point the channel is created. /// The to monitor for cancellation requests. The default is . - protected internal abstract Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken = default); + protected internal abstract Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default); /// /// Perform a discrete incremental interaction between a single and . diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 7e7dea00a805..ee13e5c38087 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -221,14 +221,15 @@ protected async IAsyncEnumerable InvokeAgentAsync( // 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)) + // Don't expose function-call or function-result messages to caller. + if (message.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) { continue; } + messages.Add(message); + // Yield message to caller yield return message; } @@ -262,7 +263,10 @@ async Task GetOrCreateChannelAsync() if (this.History.Count > 0) { - await channel.ReceiveAsync(this.History, cancellationToken).ConfigureAwait(false); + // Sync channel with existing history (user and assistant messages only / no function content) + await channel.ReceiveAsync( + this.History.Where(m => m.Role != AuthorRole.Tool && !m.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)), + cancellationToken).ConfigureAwait(false); } this.Logger.LogInformation("[{MethodName}] Created channel for {AgentType}: {AgentId}", nameof(InvokeAgentAsync), agent.GetType(), agent.Id); diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 60b1cd4367f6..80d7b4250631 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -47,7 +47,7 @@ protected internal override async IAsyncEnumerable InvokeAsy } } - protected internal override Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken = default) + protected internal override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) { // Always receive the initial history from the owning chat. this._chat.AddChatMessages([.. history]); diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 2bb5616ff959..cf7c9ef941b0 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -25,16 +27,26 @@ protected internal sealed override async IAsyncEnumerable In throw new KernelException($"Invalid channel binding for agent: {agent.Id} ({agent.GetType().FullName})"); } + int messageCount = this._history.Count; + await foreach (ChatMessageContent message in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false)) { - this._history.Add(message); + // Don't append messages already added to the history. + if (messageCount < this._history.Count) + { + messageCount = this._history.Count; + } + else + { + this._history.Add(message); + } yield return message; } } /// - protected internal sealed override Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken) + protected internal sealed override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken) { this._history.AddRange(history); diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs index b84d29494b8e..c792752cea1f 100644 --- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs +++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs @@ -55,6 +55,8 @@ await chatCompletionService.GetChatMessageContentsAsync( message.AuthorName = this.Name; history.Add(message); + + yield return message; } foreach (ChatMessageContent message in messages ?? []) @@ -91,22 +93,22 @@ public override async IAsyncEnumerable InvokeStream this.Logger.LogInformation("[{MethodName}] Invoked {ServiceType} with streaming messages.", nameof(InvokeAsync), chatCompletionService.GetType()); } - // Capture mutated messages related function calling / tools - for (int messageIndex = messageCount; messageIndex < chat.Count; messageIndex++) + await foreach (StreamingChatMessageContent message in messages.ConfigureAwait(false)) { - ChatMessageContent message = chat[messageIndex]; - + // TODO: MESSAGE SOURCE - ISSUE #5731 message.AuthorName = this.Name; - history.Add(message); + yield return message; } - await foreach (StreamingChatMessageContent message in messages.ConfigureAwait(false)) + // Capture mutated messages related function calling / tools + for (int messageIndex = messageCount; messageIndex < chat.Count; messageIndex++) { - // TODO: MESSAGE SOURCE - ISSUE #5731 + ChatMessageContent message = chat[messageIndex]; + message.AuthorName = this.Name; - yield return message; + history.Add(message); } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index b84ef800ebd4..6bbc33545f46 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -16,7 +16,7 @@ internal sealed class OpenAIAssistantChannel(AssistantsClient client, string thr private readonly string _threadId = threadId; /// - protected override async Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken) + protected override async Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken) { foreach (ChatMessageContent message in history) { From 0e91ce56824878d7528e6ddb2bc7845a7da909ec Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 11 Jul 2024 13:32:29 -0700 Subject: [PATCH 02/26] Checkpoint --- .../Agents/ChatCompletion_Streaming.cs | 1 - .../AutoFunctionInvocationFiltering.cs | 2 +- dotnet/samples/Concepts/FunctionSanity.cs | 419 ++++++++++++++++++ .../Concepts/FunctionSanity_Streaming.cs | 324 ++++++++++++++ .../GettingStartedWithAgents/Step1_Agent.cs | 1 + .../GettingStartedWithAgents/Step2_Plugins.cs | 3 +- .../GettingStartedWithAgents/Step3_Chat.cs | 2 +- .../Step4_KernelFunctionStrategies.cs | 2 +- .../Step5_JsonResult.cs | 2 +- .../Step6_DependencyInjection.cs | 2 +- .../GettingStartedWithAgents/Step7_Logging.cs | 2 +- .../Step8_OpenAIAssistant.cs | 2 +- .../src/Agents/Abstractions/AgentChannel.cs | 8 + dotnet/src/Agents/Abstractions/AgentChat.cs | 76 +++- .../Agents/Abstractions/AggregatorChannel.cs | 5 + .../Agents/Abstractions/ChatHistoryChannel.cs | 22 +- dotnet/src/Agents/Core/ChatCompletionAgent.cs | 2 - .../Agents/OpenAI/OpenAIAssistantChannel.cs | 6 + .../src/Agents/UnitTests/AgentChannelTests.cs | 2 +- .../UnitTests/Internal/BroadcastQueueTests.cs | 4 +- 20 files changed, 851 insertions(+), 36 deletions(-) create mode 100644 dotnet/samples/Concepts/FunctionSanity.cs create mode 100644 dotnet/samples/Concepts/FunctionSanity_Streaming.cs diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index fc1fadf0f446..258e12166a6b 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; using System.Text; -using Microsoft.Graph; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs b/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs index 1e56b8f36878..6b7c8cf26b90 100644 --- a/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs +++ b/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs @@ -138,7 +138,7 @@ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext co var result = context.Result; // Example: override function result value - context.Result = new FunctionResult(context.Result, "Result from auto function invocation filter"); + context.Result = new FunctionResult(context.Function, "Result from auto function invocation filter"); // Example: Terminate function invocation context.Terminate = true; diff --git a/dotnet/samples/Concepts/FunctionSanity.cs b/dotnet/samples/Concepts/FunctionSanity.cs new file mode 100644 index 000000000000..221127fa1e01 --- /dev/null +++ b/dotnet/samples/Concepts/FunctionSanity.cs @@ -0,0 +1,419 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace Sanity; + +public class FunctionSanity(ITestOutputHelper output) : BaseTest(output) +{ + private static readonly string[] s_userInput = + [ + //"Hello", + "What is the special soup and what is its price?", + "What is the special drink and what is its price?", + //"Thank you" + ]; + + ////////////////////////////// + // CHAT COMPLETION SERVICE + + [Fact] + public async Task ServiceBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServiceManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunServiceTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task ServiceFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServicePromptFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new PromptFilter()); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServiceAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunServiceTestAsync(kernel); + } + + ////////////////////////////// + // KERNEL PROMPT FUNCTION + + [Fact] + public async Task KernelBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunKernelTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task KernelFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelPromptFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new PromptFilter()); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunKernelTestAsync(kernel); + } + + ////////////////////////////// + // AGENT + + [Fact] + public async Task AgentInvokeBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentChatBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentChatTestAsync(kernel); + } + + [Fact] + public async Task AgentInvokeManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task AgentChatManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentChatTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task AgentInvokeFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentChatFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunAgentChatTestAsync(kernel); + } + + [Fact] + public async Task AgentInvokeAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentChatAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunAgentChatTestAsync(kernel); + } + + ////////////////////////////// + // KERNEL TEST + private async Task RunKernelTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + async Task InvokeWithInputAsync(string input) + { + Console.WriteLine($"[TextContent] {AuthorRole.User}: '{input}'"); + + KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(input, new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }); + + ChatMessageContent content = (await kernel.InvokeAsync(promptFunction))!; + WriteContent(content); + } + } + + ////////////////////////////// + // CHAT COMPLETION SERVICE TEST + private async Task RunServiceTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + IChatCompletionService service = kernel.GetRequiredService(); + + ChatHistory chat = []; + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.Add(userContent); + WriteContent(userContent); + + foreach (ChatMessageContent content in await service.GetChatMessageContentsAsync(chat, new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) + { + if (content.Role != AuthorRole.Tool && !content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) // %%% BIG PROBLEM + { + chat.Add(content); + } + + WriteContent(content); + } + } + } + + ////////////////////////////// + // AGENT TEST + private async Task RunAgentTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + ChatCompletionAgent agent = + new() + { + Instructions = "Answer questions about the menu.", + Kernel = kernel, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, + }; + + ChatHistory chat = []; + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.Add(userContent); + WriteContent(userContent); + + await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) + { + if (content.Role != AuthorRole.Tool && !content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) // %%% BIG PROBLEM + { + chat.Add(content); + } + + WriteContent(content); + } + } + } + + ////////////////////////////// + // AGENT CHAT TEST + private async Task RunAgentChatTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + ChatCompletionAgent agent = + new() + { + Instructions = "Answer questions about the menu.", + Kernel = kernel, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, + }; + + AgentGroupChat chat = new(); + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.AddChatMessage(userContent); + WriteContent(userContent); + + await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + { + WriteContent(content); + } + } + } + + ////////////////////////////// + // UTILITY + private void WriteContent(ChatMessageContent content) + { + Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); + } + + ////////////////////////////// + // PLUGIN + 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 prices of the specials from the menu.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] + public string GetPrices() + { + return @" +Clam Chowder: $9.99 +Cobb Salad: $9.99 +Chai Tea: $9.99 +"; + } + } + + ////////////////////////////// + // FUNCTION FILTER + private sealed class FunctionFilter : IFunctionInvocationFilter + { + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) + { + if (context.Function.PluginName == nameof(MenuPlugin)) + { + context.Result = new FunctionResult(context.Function, "Menu not available."); + return Task.CompletedTask; + } + + return next(context); + } + } + + ////////////////////////////// + // PROMPT FILTER + private sealed class PromptFilter : IFunctionInvocationFilter + { + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) + { + if (context.Function.PluginName != nameof(MenuPlugin)) + { + context.Result = new FunctionResult(context.Function, new ChatMessageContent(AuthorRole.Assistant, "Intercepted message.")); + return Task.CompletedTask; + } + + return next(context); + } + } + + ////////////////////////////// + // AUTO INVOCATION FILTER + private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter + { + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + FunctionCallContent[] functionCalls = FunctionCallContent.GetFunctionCalls(context.ChatHistory.Last()).ToArray(); + + await next(context); // %%% MIGHT BE SKIPPED / NO IMPACT HERE + + if (context.Function.PluginName == nameof(MenuPlugin)) + { + //context.Result = new FunctionResult(context.Function, "Menu not available."); + context.Terminate = terminate; + } + } + } +} diff --git a/dotnet/samples/Concepts/FunctionSanity_Streaming.cs b/dotnet/samples/Concepts/FunctionSanity_Streaming.cs new file mode 100644 index 000000000000..073760151266 --- /dev/null +++ b/dotnet/samples/Concepts/FunctionSanity_Streaming.cs @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace Sanity; + +public class FunctionSanity_Streaming(ITestOutputHelper output) : BaseTest(output) +{ + private static readonly string[] s_userInput = + [ + "Hello", + "What is the special soup and what is its price?", + "What is the special drink and what is its price?", + "Thank you" + ]; + + ////////////////////////////// + // CHAT COMPLETION SERVICE + + [Fact] + public async Task ServiceBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServiceFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServicePromptFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new PromptFilter()); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServiceAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunServiceTestAsync(kernel); + } + + ////////////////////////////// + // KERNEL PROMPT FUNCTION + + [Fact] + public async Task KernelBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelPromptFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new PromptFilter()); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunKernelTestAsync(kernel); + } + + ////////////////////////////// + // AGENT + + [Fact] + public async Task AgentInvokeBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentInvokeFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentInvokeAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunAgentTestAsync(kernel); + } + + ////////////////////////////// + // KERNEL TEST + private async Task RunKernelTestAsync(Kernel kernel) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + async Task InvokeWithInputAsync(string input) + { + Console.WriteLine($"[TextContent] {AuthorRole.User}: '{input}'"); + + KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(input, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }); + + await foreach (StreamingChatMessageContent content in kernel.InvokeStreamingAsync(promptFunction)) + { + WriteContent(content); + } + } + } + + ////////////////////////////// + // CHAT COMPLETION SERVICE TEST + private async Task RunServiceTestAsync(Kernel kernel) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + IChatCompletionService service = kernel.GetRequiredService(); + + ChatHistory chat = []; + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.Add(userContent); + WriteContent(userContent); + + await foreach (StreamingChatMessageContent content in service.GetStreamingChatMessageContentsAsync(chat, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) + { + WriteContent(content); + } + } + } + + ////////////////////////////// + // AGENT TEST + private async Task RunAgentTestAsync(Kernel kernel) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + ChatCompletionAgent agent = + new() + { + Instructions = "Answer questions about the menu.", + Kernel = kernel, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + }; + + ChatHistory chat = []; + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.Add(userContent); + WriteContent(userContent); + + await foreach (StreamingChatMessageContent content in agent.InvokeStreamingAsync(chat)) + { + //if (content.Role != AuthorRole.Tool) // %%% BIG PROBLEM + //{ + // chat.Add(content); // %%% AWKWARD (BUILDING HISTORY) + //} + + WriteContent(content); + } + } + } + + ////////////////////////////// + // UTILITY + private void WriteContent(ChatMessageContent content) + { + Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); + } + + private void WriteContent(StreamingChatMessageContent content) + { + Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); + } + + ////////////////////////////// + // PLUGIN + 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"; + } + } + + ////////////////////////////// + // FUNCTION FILTER + private sealed class FunctionFilter : IFunctionInvocationFilter + { + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) + { + if (context.Function.PluginName == nameof(MenuPlugin)) + { + context.Result = new FunctionResult(context.Function, "Menu not available."); + return Task.CompletedTask; + } + + return next(context); + } + } + + ////////////////////////////// + // PROMPT FILTER + private sealed class PromptFilter : IFunctionInvocationFilter + { + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) + { + if (context.Function.PluginName != nameof(MenuPlugin)) + { + StreamingChatMessageContent[] contents = [new StreamingChatMessageContent(AuthorRole.Assistant, "Intercepted message.")]; + IAsyncEnumerable contentsAsync = contents.ToAsyncEnumerable(); + context.Result = new FunctionResult(context.Function, contentsAsync); + return Task.CompletedTask; + } + + return next(context); + } + } + + ////////////////////////////// + // AUTO INVOCATION FILTER + private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter + { + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + ChatHistory chatHistory = context.ChatHistory; + + //await next(context); // %%% MIGHT BE SKIPPED / NO IMPACT HERE + + if (context.Function.PluginName == nameof(MenuPlugin)) + { + context.Result = new FunctionResult(context.Function, "Menu not available."); + context.Terminate = terminate; + } + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs b/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs index ddab79f032b0..272b2f787138 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs @@ -43,6 +43,7 @@ async Task InvokeAgentAsync(string input) await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) { + chat.Add(content); Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs index 61737de498be..02445878789f 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs @@ -48,8 +48,9 @@ async Task InvokeAgentAsync(string input) chat.Add(new ChatMessageContent(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (var content in agent.InvokeAsync(chat)) + await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) { + chat.Add(content); Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs b/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs index 0c9c60f870a7..5d0c185f95f5 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs @@ -78,7 +78,7 @@ public async Task UseAgentGroupChatWithTwoAgentsAsync() chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (var content in chat.InvokeAsync()) + await foreach (ChatMessageContent content in chat.InvokeAsync()) { Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs b/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs index cd99531ec27b..9cabe0193d3e 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs @@ -120,7 +120,7 @@ State only the name of the participant to take the next turn. chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (var content in chat.InvokeAsync()) + await foreach (ChatMessageContent content in chat.InvokeAsync()) { Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs b/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs index b1e83a202505..20ad4c2096d4 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs @@ -64,7 +64,7 @@ async Task InvokeAgentAsync(string input) Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (var content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) { Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); diff --git a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs b/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs index a7e3b9b41450..21af5db70dce 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs @@ -82,7 +82,7 @@ async Task WriteAgentResponse(string input) { Console.WriteLine($"# {AuthorRole.User}: {input}"); - await foreach (var content in agentClient.RunDemoAsync(input)) + await foreach (ChatMessageContent content in agentClient.RunDemoAsync(input)) { Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs b/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs index 4372d71e37f8..1ab559e668fb 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs @@ -85,7 +85,7 @@ public async Task UseLoggerFactoryWithAgentGroupChatAsync() chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (var content in chat.InvokeAsync()) + await foreach (ChatMessageContent content in chat.InvokeAsync()) { Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 09afcfc44826..db2d14704989 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -60,7 +60,7 @@ async Task InvokeAgentAsync(string input) Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (var content in agent.InvokeAsync(threadId)) + await foreach (ChatMessageContent content in agent.InvokeAsync(threadId)) { if (content.Role != AuthorRole.Tool) { diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index 6abe382cc24c..002f508e0269 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -41,6 +41,14 @@ protected internal abstract IAsyncEnumerable InvokeAsync( /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. protected internal abstract IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken = default); + + /// + /// %%% + /// + /// + /// + /// + protected internal abstract Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default); } /// diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index ee13e5c38087..48d3f8ec526e 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -215,24 +216,45 @@ protected async IAsyncEnumerable InvokeAgentAsync( // Invoke agent & process response List messages = []; - await foreach (ChatMessageContent message in channel.InvokeAsync(agent, cancellationToken).ConfigureAwait(false)) + bool didPostFunctionResult; + do { - this.Logger.LogTrace("[{MethodName}] Agent message {AgentType}: {Message}", nameof(InvokeAgentAsync), agent.GetType(), message); + didPostFunctionResult = false; + await foreach (ChatMessageContent message in channel.InvokeAsync(agent, cancellationToken).ConfigureAwait(false)) + { + this.Logger.LogTrace("[{MethodName}] Agent message {AgentType}: {Message}", nameof(InvokeAgentAsync), agent.GetType(), message); - // Add to primary history - this.History.Add(message); + messages.Add(message); - // Don't expose function-call or function-result messages to caller. - if (message.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) - { - continue; - } + if (message.Items.Any(i => i is FunctionCallContent)) // %%% AGENTCHAT FILTER: Manual Function Invocation + { + ChatMessageContent functionResultContent = await this.OnManualFunctionInvocationAsync(agent, message).ConfigureAwait(false); + await channel.CaptureFunctionResultAsync(functionResultContent, cancellationToken).ConfigureAwait(false); + didPostFunctionResult = true; + continue; + } + + ChatMessageContent assistantMessage = message; + + if (message.Items.Any(i => i is FunctionResultContent)) // %%% AGENTCHAT FILTER: Autocomplete Function Termination, et al... + { + assistantMessage = this.OnFunctionResultTransformation(agent, message); + } + else + { - messages.Add(message); + } - // Yield message to caller - yield return message; + // Add to primary history + this.History.Add(assistantMessage); + + //messages.Add(assistantMessage); + + // Yield message to caller + yield return assistantMessage; + } } + while (didPostFunctionResult); // Broadcast message to other channels (in parallel) // Note: Able to queue messages without synchronizing channels. @@ -240,7 +262,7 @@ protected async IAsyncEnumerable 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); // %%% BROADCAST ALL this.Logger.LogInformation("[{MethodName}] Invoked agent {AgentType}: {AgentId}", nameof(InvokeAgentAsync), agent.GetType(), agent.Id); } @@ -265,7 +287,7 @@ async Task GetOrCreateChannelAsync() { // Sync channel with existing history (user and assistant messages only / no function content) await channel.ReceiveAsync( - this.History.Where(m => m.Role != AuthorRole.Tool && !m.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)), + this.History.Where(m => m.Role != AuthorRole.Tool && !m.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)), // %%% BIG PROBLEM cancellationToken).ConfigureAwait(false); } @@ -276,6 +298,32 @@ await channel.ReceiveAsync( } } + private async Task OnManualFunctionInvocationAsync(Agent agent, ChatMessageContent message) + { + // %%% GET FILTER IF EXISTS + + // %%% FAKE + KernelAgent kernelAgent = agent as KernelAgent ?? throw new KernelException("Agent must be a KernelAgent to invoke functions."); + FunctionCallContent functionCall = message.Items.OfType().Single(); + FunctionResultContent functionResult = await functionCall.InvokeAsync(kernelAgent.Kernel).ConfigureAwait(false); + return functionResult.ToChatMessage(); + } + + private ChatMessageContent OnFunctionResultTransformation(Agent agent, ChatMessageContent message) + { + // %%% GET FILTER IF EXISTS + + // Default logic if no filter + ChatMessageContent transformedResult = + new(AuthorRole.Assistant, content: message.Content) + { + Items = [.. message.Items.OfType().Cast()], + //Items = [new TextContent("transformed"], + }; + + return transformedResult; + } + /// /// Clear activity signal to indicate that activity has ceased. /// diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 80d7b4250631..891baa5524f7 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -13,6 +13,11 @@ internal sealed class AggregatorChannel(AgentChat chat) : AgentChannel GetHistoryAsync(CancellationToken cancellationToken = default) { return this._chat.GetChatMessagesAsync(cancellationToken); diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index cf7c9ef941b0..0e1be702bc32 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -31,12 +29,12 @@ protected internal sealed override async IAsyncEnumerable In await foreach (ChatMessageContent message in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false)) { - // Don't append messages already added to the history. - if (messageCount < this._history.Count) - { - messageCount = this._history.Count; - } - else + //for (int messageIndex = messageCount; messageIndex < this._history.Count; messageIndex++) // %%% DECISION POINT + //{ + // yield return this._history[messageIndex]; + //} + + if (message.Role != AuthorRole.Tool) // %%% BIG PROBLEM { this._history.Add(message); } @@ -59,6 +57,14 @@ protected internal sealed override IAsyncEnumerable GetHisto return this._history.ToDescendingAsync(); } + /// + protected internal override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) + { + this._history.Add(functionResultsMessage); + + return Task.CompletedTask; + } + /// /// Initializes a new instance of the class. /// diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs index c792752cea1f..e5f4044f08be 100644 --- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs +++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs @@ -55,8 +55,6 @@ await chatCompletionService.GetChatMessageContentsAsync( message.AuthorName = this.Name; history.Add(message); - - yield return message; } foreach (ChatMessageContent message in messages ?? []) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 6bbc33545f46..71edaba070e8 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -39,4 +39,10 @@ protected override IAsyncEnumerable GetHistoryAsync(Cancella { return AssistantThreadActions.GetMessagesAsync(this._client, this._threadId, cancellationToken); } + + /// + protected override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } } diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs index 7223b8d46805..c35bd5bc365d 100644 --- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs @@ -57,7 +57,7 @@ protected internal override IAsyncEnumerable GetHistoryAsync throw new NotImplementedException(); } - protected internal override Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken = default) + protected internal override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs index 482c4cfa09a3..4c90aa9c2cdc 100644 --- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs @@ -141,7 +141,7 @@ protected internal override IAsyncEnumerable InvokeAsync(Age throw new NotImplementedException(); } - protected internal override async Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken = default) + protected internal override async Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) { this.ReceivedMessages.AddRange(history); this.ReceiveCount++; @@ -164,7 +164,7 @@ protected internal override IAsyncEnumerable InvokeAsync(Age throw new NotImplementedException(); } - protected internal override async Task ReceiveAsync(IReadOnlyList history, CancellationToken cancellationToken = default) + protected internal override async Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) { await Task.Delay(this.ReceiveDuration, cancellationToken); From 6c2e2a6a2102ecc61a91dce35298b04bffa523ae Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 11 Jul 2024 14:04:01 -0700 Subject: [PATCH 03/26] Remove local test harness --- dotnet/samples/Concepts/FunctionSanity.cs | 419 ------------------ .../Concepts/FunctionSanity_Streaming.cs | 324 -------------- 2 files changed, 743 deletions(-) delete mode 100644 dotnet/samples/Concepts/FunctionSanity.cs delete mode 100644 dotnet/samples/Concepts/FunctionSanity_Streaming.cs diff --git a/dotnet/samples/Concepts/FunctionSanity.cs b/dotnet/samples/Concepts/FunctionSanity.cs deleted file mode 100644 index 221127fa1e01..000000000000 --- a/dotnet/samples/Concepts/FunctionSanity.cs +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.ComponentModel; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Sanity; - -public class FunctionSanity(ITestOutputHelper output) : BaseTest(output) -{ - private static readonly string[] s_userInput = - [ - //"Hello", - "What is the special soup and what is its price?", - "What is the special drink and what is its price?", - //"Thank you" - ]; - - ////////////////////////////// - // CHAT COMPLETION SERVICE - - [Fact] - public async Task ServiceBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServiceManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunServiceTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task ServiceFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServicePromptFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new PromptFilter()); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServiceAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunServiceTestAsync(kernel); - } - - ////////////////////////////// - // KERNEL PROMPT FUNCTION - - [Fact] - public async Task KernelBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunKernelTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task KernelFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelPromptFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new PromptFilter()); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunKernelTestAsync(kernel); - } - - ////////////////////////////// - // AGENT - - [Fact] - public async Task AgentInvokeBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentChatBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentChatTestAsync(kernel); - } - - [Fact] - public async Task AgentInvokeManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task AgentChatManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentChatTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task AgentInvokeFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentChatFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunAgentChatTestAsync(kernel); - } - - [Fact] - public async Task AgentInvokeAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentChatAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunAgentChatTestAsync(kernel); - } - - ////////////////////////////// - // KERNEL TEST - private async Task RunKernelTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - async Task InvokeWithInputAsync(string input) - { - Console.WriteLine($"[TextContent] {AuthorRole.User}: '{input}'"); - - KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(input, new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }); - - ChatMessageContent content = (await kernel.InvokeAsync(promptFunction))!; - WriteContent(content); - } - } - - ////////////////////////////// - // CHAT COMPLETION SERVICE TEST - private async Task RunServiceTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - IChatCompletionService service = kernel.GetRequiredService(); - - ChatHistory chat = []; - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - WriteContent(userContent); - - foreach (ChatMessageContent content in await service.GetChatMessageContentsAsync(chat, new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) - { - if (content.Role != AuthorRole.Tool && !content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) // %%% BIG PROBLEM - { - chat.Add(content); - } - - WriteContent(content); - } - } - } - - ////////////////////////////// - // AGENT TEST - private async Task RunAgentTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - ChatCompletionAgent agent = - new() - { - Instructions = "Answer questions about the menu.", - Kernel = kernel, - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, - }; - - ChatHistory chat = []; - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - WriteContent(userContent); - - await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) - { - if (content.Role != AuthorRole.Tool && !content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) // %%% BIG PROBLEM - { - chat.Add(content); - } - - WriteContent(content); - } - } - } - - ////////////////////////////// - // AGENT CHAT TEST - private async Task RunAgentChatTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - ChatCompletionAgent agent = - new() - { - Instructions = "Answer questions about the menu.", - Kernel = kernel, - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, - }; - - AgentGroupChat chat = new(); - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.AddChatMessage(userContent); - WriteContent(userContent); - - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) - { - WriteContent(content); - } - } - } - - ////////////////////////////// - // UTILITY - private void WriteContent(ChatMessageContent content) - { - Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); - } - - ////////////////////////////// - // PLUGIN - 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 prices of the specials from the menu.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetPrices() - { - return @" -Clam Chowder: $9.99 -Cobb Salad: $9.99 -Chai Tea: $9.99 -"; - } - } - - ////////////////////////////// - // FUNCTION FILTER - private sealed class FunctionFilter : IFunctionInvocationFilter - { - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - if (context.Function.PluginName == nameof(MenuPlugin)) - { - context.Result = new FunctionResult(context.Function, "Menu not available."); - return Task.CompletedTask; - } - - return next(context); - } - } - - ////////////////////////////// - // PROMPT FILTER - private sealed class PromptFilter : IFunctionInvocationFilter - { - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - if (context.Function.PluginName != nameof(MenuPlugin)) - { - context.Result = new FunctionResult(context.Function, new ChatMessageContent(AuthorRole.Assistant, "Intercepted message.")); - return Task.CompletedTask; - } - - return next(context); - } - } - - ////////////////////////////// - // AUTO INVOCATION FILTER - private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter - { - public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) - { - FunctionCallContent[] functionCalls = FunctionCallContent.GetFunctionCalls(context.ChatHistory.Last()).ToArray(); - - await next(context); // %%% MIGHT BE SKIPPED / NO IMPACT HERE - - if (context.Function.PluginName == nameof(MenuPlugin)) - { - //context.Result = new FunctionResult(context.Function, "Menu not available."); - context.Terminate = terminate; - } - } - } -} diff --git a/dotnet/samples/Concepts/FunctionSanity_Streaming.cs b/dotnet/samples/Concepts/FunctionSanity_Streaming.cs deleted file mode 100644 index 073760151266..000000000000 --- a/dotnet/samples/Concepts/FunctionSanity_Streaming.cs +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.ComponentModel; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Sanity; - -public class FunctionSanity_Streaming(ITestOutputHelper output) : BaseTest(output) -{ - private static readonly string[] s_userInput = - [ - "Hello", - "What is the special soup and what is its price?", - "What is the special drink and what is its price?", - "Thank you" - ]; - - ////////////////////////////// - // CHAT COMPLETION SERVICE - - [Fact] - public async Task ServiceBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServiceFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServicePromptFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new PromptFilter()); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServiceAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunServiceTestAsync(kernel); - } - - ////////////////////////////// - // KERNEL PROMPT FUNCTION - - [Fact] - public async Task KernelBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelPromptFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new PromptFilter()); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunKernelTestAsync(kernel); - } - - ////////////////////////////// - // AGENT - - [Fact] - public async Task AgentInvokeBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentInvokeFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentInvokeAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunAgentTestAsync(kernel); - } - - ////////////////////////////// - // KERNEL TEST - private async Task RunKernelTestAsync(Kernel kernel) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - async Task InvokeWithInputAsync(string input) - { - Console.WriteLine($"[TextContent] {AuthorRole.User}: '{input}'"); - - KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(input, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }); - - await foreach (StreamingChatMessageContent content in kernel.InvokeStreamingAsync(promptFunction)) - { - WriteContent(content); - } - } - } - - ////////////////////////////// - // CHAT COMPLETION SERVICE TEST - private async Task RunServiceTestAsync(Kernel kernel) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - IChatCompletionService service = kernel.GetRequiredService(); - - ChatHistory chat = []; - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - WriteContent(userContent); - - await foreach (StreamingChatMessageContent content in service.GetStreamingChatMessageContentsAsync(chat, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) - { - WriteContent(content); - } - } - } - - ////////////////////////////// - // AGENT TEST - private async Task RunAgentTestAsync(Kernel kernel) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - ChatCompletionAgent agent = - new() - { - Instructions = "Answer questions about the menu.", - Kernel = kernel, - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, - }; - - ChatHistory chat = []; - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - WriteContent(userContent); - - await foreach (StreamingChatMessageContent content in agent.InvokeStreamingAsync(chat)) - { - //if (content.Role != AuthorRole.Tool) // %%% BIG PROBLEM - //{ - // chat.Add(content); // %%% AWKWARD (BUILDING HISTORY) - //} - - WriteContent(content); - } - } - } - - ////////////////////////////// - // UTILITY - private void WriteContent(ChatMessageContent content) - { - Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); - } - - private void WriteContent(StreamingChatMessageContent content) - { - Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); - } - - ////////////////////////////// - // PLUGIN - 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"; - } - } - - ////////////////////////////// - // FUNCTION FILTER - private sealed class FunctionFilter : IFunctionInvocationFilter - { - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - if (context.Function.PluginName == nameof(MenuPlugin)) - { - context.Result = new FunctionResult(context.Function, "Menu not available."); - return Task.CompletedTask; - } - - return next(context); - } - } - - ////////////////////////////// - // PROMPT FILTER - private sealed class PromptFilter : IFunctionInvocationFilter - { - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - if (context.Function.PluginName != nameof(MenuPlugin)) - { - StreamingChatMessageContent[] contents = [new StreamingChatMessageContent(AuthorRole.Assistant, "Intercepted message.")]; - IAsyncEnumerable contentsAsync = contents.ToAsyncEnumerable(); - context.Result = new FunctionResult(context.Function, contentsAsync); - return Task.CompletedTask; - } - - return next(context); - } - } - - ////////////////////////////// - // AUTO INVOCATION FILTER - private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter - { - public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) - { - ChatHistory chatHistory = context.ChatHistory; - - //await next(context); // %%% MIGHT BE SKIPPED / NO IMPACT HERE - - if (context.Function.PluginName == nameof(MenuPlugin)) - { - context.Result = new FunctionResult(context.Function, "Menu not available."); - context.Terminate = terminate; - } - } - } -} From b1463470cf3d116e878f174e8a667a2c110c3c1c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 11 Jul 2024 18:54:35 -0700 Subject: [PATCH 04/26] Checkpoint --- .../src/Agents/Abstractions/AgentChannel.cs | 31 ++++ dotnet/src/Agents/Abstractions/AgentChat.cs | 132 ++++++++++-------- .../Agents/Abstractions/ChatHistoryChannel.cs | 60 ++++++-- .../Filters/AssistantMessageContext.cs | 45 ++++++ .../Abstractions/Filters/ChannelProcessors.cs | 92 ++++++++++++ .../Filters/IAssistantMessageFilter.cs | 17 +++ .../Filters/IManualFunctionCallProcessor.cs | 16 +++ .../ITerminatedFunctionResultProcessor.cs | 16 +++ .../Filters/ManualFunctionCallContext.cs | 49 +++++++ .../TerminatedFunctionResultContext.cs | 41 ++++++ .../Contents/FunctionResultContent.cs | 21 ++- 11 files changed, 449 insertions(+), 71 deletions(-) create mode 100644 dotnet/src/Agents/Abstractions/Filters/AssistantMessageContext.cs create mode 100644 dotnet/src/Agents/Abstractions/Filters/ChannelProcessors.cs create mode 100644 dotnet/src/Agents/Abstractions/Filters/IAssistantMessageFilter.cs create mode 100644 dotnet/src/Agents/Abstractions/Filters/IManualFunctionCallProcessor.cs create mode 100644 dotnet/src/Agents/Abstractions/Filters/ITerminatedFunctionResultProcessor.cs create mode 100644 dotnet/src/Agents/Abstractions/Filters/ManualFunctionCallContext.cs create mode 100644 dotnet/src/Agents/Abstractions/Filters/TerminatedFunctionResultContext.cs diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index 002f508e0269..53635874b6c1 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Agents.Filters; namespace Microsoft.SemanticKernel.Agents; @@ -49,6 +50,36 @@ protected internal abstract IAsyncEnumerable InvokeAsync( /// /// protected internal abstract Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default); + + /// + /// %%% + /// + /// + /// + /// + /// + protected Task OnManualFunctionCallAsync(Agent agent, ChatMessageContent message, CancellationToken cancellationToken) + => ChannelProcessors.ProcessManualFunctionCallAsync(this, agent, message, cancellationToken); + + /// + /// %%% + /// + /// + /// + /// + /// + protected Task OnTerminatedFunctionResultAsync(Agent agent, ChatMessageContent message, CancellationToken cancellationToken) + => ChannelProcessors.ProcessTerminatedFunctionResultAsync(this, agent, message, cancellationToken); + + /// + /// %%% + /// + internal IManualFunctionCallProcessor? ManualFunctionCallProcessor { get; set; } // %%% HACK + + /// + /// %%% + /// + internal ITerminatedFunctionResultProcessor? TerminatedFunctionResultProcessor { get; set; } // %%% HACK } /// diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 48d3f8ec526e..01e886328bce 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Agents.Extensions; +using Microsoft.SemanticKernel.Agents.Filters; using Microsoft.SemanticKernel.Agents.Internal; using Microsoft.SemanticKernel.ChatCompletion; @@ -50,6 +51,21 @@ public abstract class AgentChat /// protected ChatHistory History { get; } + /// + /// %%% + /// + public IAssistantMessageFilter? AssistantMessageFilter { get; set; } + + /// + /// %%% + /// + public IManualFunctionCallProcessor? ManualFunctionCallProcessor { get; set; } = new HackFunctionCallProcessor(); // %%% HACK + + /// + /// %%% + /// + public ITerminatedFunctionResultProcessor? TerminatedFunctionResultProcessor { get; set; } // = new HackTerminatedFunctionResultProcessor(); // %%% HACK + /// /// Process a series of interactions between the agents participating in this chat. /// @@ -216,45 +232,26 @@ protected async IAsyncEnumerable InvokeAgentAsync( // Invoke agent & process response List messages = []; - bool didPostFunctionResult; - do - { - didPostFunctionResult = false; - await foreach (ChatMessageContent message in channel.InvokeAsync(agent, cancellationToken).ConfigureAwait(false)) - { - this.Logger.LogTrace("[{MethodName}] Agent message {AgentType}: {Message}", nameof(InvokeAgentAsync), agent.GetType(), message); - - messages.Add(message); - if (message.Items.Any(i => i is FunctionCallContent)) // %%% AGENTCHAT FILTER: Manual Function Invocation - { - ChatMessageContent functionResultContent = await this.OnManualFunctionInvocationAsync(agent, message).ConfigureAwait(false); - await channel.CaptureFunctionResultAsync(functionResultContent, cancellationToken).ConfigureAwait(false); - didPostFunctionResult = true; - continue; - } - - ChatMessageContent assistantMessage = message; - - if (message.Items.Any(i => i is FunctionResultContent)) // %%% AGENTCHAT FILTER: Autocomplete Function Termination, et al... - { - assistantMessage = this.OnFunctionResultTransformation(agent, message); - } - else - { + await foreach (ChatMessageContent message in channel.InvokeAsync(agent, cancellationToken).ConfigureAwait(false)) + { + this.Logger.LogTrace("[{MethodName}] Agent message {AgentType}: {Message}", nameof(InvokeAgentAsync), agent.GetType(), message); - } + // Capture potential message replacement + ChatMessageContent effectiveMessage = await this.OnAgentInvokedFilterAsync(agent, this.History, message, cancellationToken).ConfigureAwait(false); - // Add to primary history - this.History.Add(assistantMessage); + // %%% Broadcast everything + messages.Add(effectiveMessage); - //messages.Add(assistantMessage); + // Add to primary history + this.History.Add(effectiveMessage); + if (!effectiveMessage.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) + { // Yield message to caller - yield return assistantMessage; + yield return effectiveMessage; } } - while (didPostFunctionResult); // Broadcast message to other channels (in parallel) // Note: Able to queue messages without synchronizing channels. @@ -280,15 +277,15 @@ async Task GetOrCreateChannelAsync() this.Logger.LogDebug("[{MethodName}] Creating channel for {AgentType}: {AgentId}", nameof(InvokeAgentAsync), agent.GetType(), agent.Id); channel = await agent.CreateChannelAsync(cancellationToken).ConfigureAwait(false); + channel.ManualFunctionCallProcessor = this.ManualFunctionCallProcessor; // %%% HACK + channel.TerminatedFunctionResultProcessor = this.TerminatedFunctionResultProcessor; // %%% HACK this._agentChannels.Add(channelKey, channel); if (this.History.Count > 0) { // Sync channel with existing history (user and assistant messages only / no function content) - await channel.ReceiveAsync( - this.History.Where(m => m.Role != AuthorRole.Tool && !m.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)), // %%% BIG PROBLEM - cancellationToken).ConfigureAwait(false); + await channel.ReceiveAsync(this.History, cancellationToken).ConfigureAwait(false); } this.Logger.LogInformation("[{MethodName}] Created channel for {AgentType}: {AgentId}", nameof(InvokeAgentAsync), agent.GetType(), agent.Id); @@ -298,32 +295,6 @@ await channel.ReceiveAsync( } } - private async Task OnManualFunctionInvocationAsync(Agent agent, ChatMessageContent message) - { - // %%% GET FILTER IF EXISTS - - // %%% FAKE - KernelAgent kernelAgent = agent as KernelAgent ?? throw new KernelException("Agent must be a KernelAgent to invoke functions."); - FunctionCallContent functionCall = message.Items.OfType().Single(); - FunctionResultContent functionResult = await functionCall.InvokeAsync(kernelAgent.Kernel).ConfigureAwait(false); - return functionResult.ToChatMessage(); - } - - private ChatMessageContent OnFunctionResultTransformation(Agent agent, ChatMessageContent message) - { - // %%% GET FILTER IF EXISTS - - // Default logic if no filter - ChatMessageContent transformedResult = - new(AuthorRole.Assistant, content: message.Content) - { - Items = [.. message.Items.OfType().Cast()], - //Items = [new TextContent("transformed"], - }; - - return transformedResult; - } - /// /// Clear activity signal to indicate that activity has ceased. /// @@ -333,6 +304,28 @@ private void ClearActivitySignal() Interlocked.Exchange(ref this._isActive, 0); } + /// + /// %%% + /// + /// + /// + /// + /// + /// + /// + private async Task OnAgentInvokedFilterAsync(Agent agent, ChatHistory history, ChatMessageContent message, CancellationToken cancellationToken) + { + if (this.AssistantMessageFilter != null) + { + AssistantMessageContext context = new(agent, history, message) { CancellationToken = cancellationToken }; + IEnumerable content = await this.AssistantMessageFilter.OnFilterAssistantMessage(context).ConfigureAwait(false); + + return new(message.Role, [.. content], message.ModelId, message.InnerContent, message.Encoding, message.Metadata); // %%% + } + + return message; + } + /// /// Test to ensure chat is not concurrently active and throw exception if it is. /// If not, activity is signaled. @@ -387,4 +380,23 @@ protected AgentChat() this._channelMap = []; this.History = []; } + + private sealed class HackFunctionCallProcessor : IManualFunctionCallProcessor // %%% HACK + { + /// + public async Task OnProcessFunctionCallAsync(ManualFunctionCallContext context) + { + context.Result = await context.Function.InvokeAsync(context.Kernel, context.Arguments).ConfigureAwait(false); + } + } + + private class HackTerminatedFunctionResultProcessor : ITerminatedFunctionResultProcessor + { + public Task OnProcessTerminatedFunctionResultAsync(TerminatedFunctionResultContext context) + { + context.TransformedContent = context.FunctionResults.Select(r => new TextContent(r.ToString())).ToArray(); + + return Task.CompletedTask; + } + } } diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 0e1be702bc32..f8dff236dd7f 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -25,22 +27,60 @@ protected internal sealed override async IAsyncEnumerable In throw new KernelException($"Invalid channel binding for agent: {agent.Id} ({agent.GetType().FullName})"); } - int messageCount = this._history.Count; - - await foreach (ChatMessageContent message in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false)) + bool didPostFunctionResult; + do { - //for (int messageIndex = messageCount; messageIndex < this._history.Count; messageIndex++) // %%% DECISION POINT - //{ - // yield return this._history[messageIndex]; - //} + didPostFunctionResult = false; // Clear on each iteration + + int messageCount = this._history.Count; + HashSet mutatedHistory = []; - if (message.Role != AuthorRole.Tool) // %%% BIG PROBLEM + Queue messageQueue = []; + ChatMessageContent? yieldMessage = null; + await foreach (ChatMessageContent responseMessage in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false)) { - this._history.Add(message); + for (int messageIndex = messageCount; messageIndex < this._history.Count; messageIndex++) + { + ChatMessageContent mutatedMessage = this._history[messageIndex]; + mutatedHistory.Add(mutatedMessage); + messageQueue.Enqueue(mutatedMessage); + } + + if (!mutatedHistory.Contains(responseMessage)) + { + this._history.Add(responseMessage); + messageQueue.Enqueue(responseMessage); + } + + yieldMessage = messageQueue.Dequeue(); + yield return yieldMessage; } - yield return message; + while (messageQueue.Count > 0) + { + yieldMessage = messageQueue.Dequeue(); + + yield return yieldMessage; + } + + if (yieldMessage != null) + { + // Process manual Function Invocation + if (yieldMessage.Items.Any(i => i is FunctionCallContent)) + { + ChatMessageContent functionResultContent = await this.OnManualFunctionCallAsync(agent, yieldMessage, cancellationToken).ConfigureAwait(false); + yield return functionResultContent; + didPostFunctionResult = true; + } + + // Autocomplete Function Termination, et al... + if (yieldMessage.Items.Any(i => i is FunctionResultContent)) + { + yield return await this.OnTerminatedFunctionResultAsync(agent, yieldMessage, cancellationToken).ConfigureAwait(false); + } + } } + while (didPostFunctionResult); } /// diff --git a/dotnet/src/Agents/Abstractions/Filters/AssistantMessageContext.cs b/dotnet/src/Agents/Abstractions/Filters/AssistantMessageContext.cs new file mode 100644 index 000000000000..66ffb1751eb4 --- /dev/null +++ b/dotnet/src/Agents/Abstractions/Filters/AssistantMessageContext.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.SemanticKernel.Agents.Filters; + +/// +/// %%% +/// +public class AssistantMessageContext +{ + /// + /// %%% + /// + /// + /// + /// + public AssistantMessageContext(Agent agent, IReadOnlyList history, ChatMessageContent message) + { + this.Agent = agent; + this.History = history; + this.Message = message; + } + + /// + /// %%% + /// + public Agent Agent { get; } // %%% METADATA ONLY + + /// + /// %%% + /// + public IReadOnlyList History { get; } + + /// + /// %%% + /// + public ChatMessageContent Message { get; } + + /// + /// %%% + /// + public CancellationToken CancellationToken { get; internal set; } +} diff --git a/dotnet/src/Agents/Abstractions/Filters/ChannelProcessors.cs b/dotnet/src/Agents/Abstractions/Filters/ChannelProcessors.cs new file mode 100644 index 000000000000..26bb355a2f4c --- /dev/null +++ b/dotnet/src/Agents/Abstractions/Filters/ChannelProcessors.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Filters; + +internal static class ChannelProcessors +{ + /// + /// %%% + /// + /// + /// + /// + /// + /// + public static async Task ProcessManualFunctionCallAsync( + AgentChannel channel, + Agent agent, + ChatMessageContent messageContent, + CancellationToken cancellationToken) + { + FunctionResultContent resultContent; + FunctionCallContent? functionCallContent = null; + + try + { + functionCallContent = messageContent.Items.OfType().Single(); // %%% CARDINALITY + + if (channel.ManualFunctionCallProcessor == null) + { + throw new KernelException("Manual function call processor not available"); // %%% INFO + } + + KernelAgent kernelAgent = agent as KernelAgent ?? throw new KernelException("Agent must be a KernelAgent to invoke functions."); // %%% + + if (!kernelAgent.Kernel.Plugins.TryGetFunction(functionCallContent.PluginName, functionCallContent.FunctionName, out KernelFunction? function)) + { + throw new KernelException("Unable to resolve function"); // %%% INFO + } + + ManualFunctionCallContext context = new(kernelAgent.Kernel, function, functionCallContent.Arguments) { CancellationToken = cancellationToken }; + + await channel.ManualFunctionCallProcessor.OnProcessFunctionCallAsync(context).ConfigureAwait(false); + + if (context.Result == null) + { + throw new KernelException("Unknown function result"); // %%% INFO + } + + resultContent = new(functionCallContent, context.Result); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception exception) +#pragma warning restore CA1031 // Do not catch general exception types + { + functionCallContent ??= new("unknown function"); + resultContent = new FunctionResultContent(functionCallContent, exception); + } + + ChatMessageContent resultMessage = resultContent.ToChatMessage(); + + await channel.CaptureFunctionResultAsync(resultMessage, cancellationToken).ConfigureAwait(false); + + return resultMessage; + } + + internal static async Task ProcessTerminatedFunctionResultAsync(AgentChannel agentChannel, Agent agent, ChatMessageContent message, CancellationToken cancellationToken) + { + if (agentChannel.TerminatedFunctionResultProcessor != null) + { + TerminatedFunctionResultContext context = new(message.Items.OfType().ToArray()) { CancellationToken = cancellationToken }; // %%% LINQ + + await agentChannel.TerminatedFunctionResultProcessor.OnProcessTerminatedFunctionResultAsync(context).ConfigureAwait(false); + + return + new(AuthorRole.Assistant, content: null) + { + //Items = context.FunctionResults, %%% TBD + }; + } + // Default logic if no filter + return + new(AuthorRole.Assistant, content: message.Content) + { + Items = [.. message.Items.OfType().Cast()], + }; + } +} diff --git a/dotnet/src/Agents/Abstractions/Filters/IAssistantMessageFilter.cs b/dotnet/src/Agents/Abstractions/Filters/IAssistantMessageFilter.cs new file mode 100644 index 000000000000..7e3bf8bc229d --- /dev/null +++ b/dotnet/src/Agents/Abstractions/Filters/IAssistantMessageFilter.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Filters; + +/// +/// %%% +/// +public interface IAssistantMessageFilter +{ + /// + /// %%% + /// + /// The containing the result of the function's invocation. + Task> OnFilterAssistantMessage(AssistantMessageContext context); +} diff --git a/dotnet/src/Agents/Abstractions/Filters/IManualFunctionCallProcessor.cs b/dotnet/src/Agents/Abstractions/Filters/IManualFunctionCallProcessor.cs new file mode 100644 index 000000000000..5e1387786e17 --- /dev/null +++ b/dotnet/src/Agents/Abstractions/Filters/IManualFunctionCallProcessor.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Filters; + +/// +/// %%% +/// +public interface IManualFunctionCallProcessor +{ + /// + /// %%% + /// + /// The containing the result of the function's invocation. + Task OnProcessFunctionCallAsync(ManualFunctionCallContext context); +} diff --git a/dotnet/src/Agents/Abstractions/Filters/ITerminatedFunctionResultProcessor.cs b/dotnet/src/Agents/Abstractions/Filters/ITerminatedFunctionResultProcessor.cs new file mode 100644 index 000000000000..1c57e2f4fc45 --- /dev/null +++ b/dotnet/src/Agents/Abstractions/Filters/ITerminatedFunctionResultProcessor.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Filters; + +/// +/// %%% +/// +public interface ITerminatedFunctionResultProcessor +{ + /// + /// %%% + /// + /// The containing the result of the terminated function. + Task OnProcessTerminatedFunctionResultAsync(TerminatedFunctionResultContext context); +} diff --git a/dotnet/src/Agents/Abstractions/Filters/ManualFunctionCallContext.cs b/dotnet/src/Agents/Abstractions/Filters/ManualFunctionCallContext.cs new file mode 100644 index 000000000000..6567d4d9bd65 --- /dev/null +++ b/dotnet/src/Agents/Abstractions/Filters/ManualFunctionCallContext.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Threading; + +namespace Microsoft.SemanticKernel.Agents.Filters; + +/// +/// %%% +/// +public class ManualFunctionCallContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The containing services, plugins, and other state for use throughout the operation. + /// The with which this filter is associated. + /// The arguments associated with the operation. + internal ManualFunctionCallContext(Kernel kernel, KernelFunction function, KernelArguments? arguments) + { + this.Kernel = kernel; + this.Function = function; + this.Arguments = arguments ?? []; + } + + /// + /// The to monitor for cancellation requests. + /// The default is . + /// + public CancellationToken CancellationToken { get; init; } + + /// + /// Gets the containing services, plugins, and other state for use throughout the operation. + /// + public Kernel Kernel { get; } + + /// + /// Gets the with which this filter is associated. + /// + public KernelFunction Function { get; } + + /// + /// Gets the arguments associated with the operation. + /// + public KernelArguments Arguments { get; } + + /// + /// Gets or sets the result of the function's invocation. + /// + public FunctionResult? Result { get; set; } +} diff --git a/dotnet/src/Agents/Abstractions/Filters/TerminatedFunctionResultContext.cs b/dotnet/src/Agents/Abstractions/Filters/TerminatedFunctionResultContext.cs new file mode 100644 index 000000000000..9b3655350c96 --- /dev/null +++ b/dotnet/src/Agents/Abstractions/Filters/TerminatedFunctionResultContext.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.SemanticKernel.Agents.Filters; + +/// +/// %%% +/// +public class TerminatedFunctionResultContext +{ + /// + /// Initializes a new instance of the class. + /// + /// + internal TerminatedFunctionResultContext(IReadOnlyList functionResults) + { + this.FunctionResults = functionResults; + } + + /// + /// The to monitor for cancellation requests. + /// The default is . + /// + public CancellationToken CancellationToken { get; init; } + + ///// + ///// Gets the containing services, plugins, and other state for use throughout the operation. + ///// + //public Kernel Kernel { get; } // %%% + + /// + /// Gets the with which this filter is associated. + /// + public IReadOnlyList FunctionResults { get; } + + /// + /// %%% + /// + public IEnumerable? TransformedContent { get; set; } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index ab1e342f7906..b76152fa5578 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.SemanticKernel.ChatCompletion; @@ -82,6 +83,24 @@ public FunctionResultContent(FunctionCallContent functionCallContent, FunctionRe /// The instance. public ChatMessageContent ToChatMessage() { - return new ChatMessageContent(AuthorRole.Tool, [this]); + string? resultContent = this.Result as string; + + if (string.IsNullOrEmpty(resultContent) && + this.Result is ChatMessageContent chatMessageContent) + { + resultContent = chatMessageContent.ToString(); + } + + if (string.IsNullOrEmpty(resultContent) && + this.Result is Exception exception) + { + resultContent = exception.ToString(); + } + + ChatMessageContent chatMessage = new(AuthorRole.Tool, content: resultContent); + + chatMessage.Items.Add(this); + + return chatMessage; } } From 699b7454bffc23a0167cb316ee96b4b418d4aa29 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 08:47:48 -0700 Subject: [PATCH 05/26] Namespace --- dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index f8dff236dd7f..f4b68ef8c75a 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; From 6ffeb0cab224d429b8abd977f2bff0ad7123d000 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 09:22:14 -0700 Subject: [PATCH 06/26] Update mock channels --- dotnet/src/Agents/UnitTests/AgentChannelTests.cs | 5 +++++ .../Agents/UnitTests/Internal/BroadcastQueueTests.cs | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs index c35bd5bc365d..e23c538576a5 100644 --- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs @@ -61,6 +61,11 @@ protected internal override Task ReceiveAsync(IEnumerable hi { throw new NotImplementedException(); } + + protected internal override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } } private sealed class NextAgent : TestAgent; diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs index 4c90aa9c2cdc..8b1c27ab6c8c 100644 --- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs @@ -131,6 +131,11 @@ private sealed class TestChannel : AgentChannel public List ReceivedMessages { get; } = []; + protected internal override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -154,6 +159,11 @@ private sealed class BadChannel : AgentChannel { public TimeSpan ReceiveDuration { get; set; } = TimeSpan.FromSeconds(0.1); + protected internal override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); From 194f39c30e652eec8cf28a6fb768cc11abc08852 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 09:35:56 -0700 Subject: [PATCH 07/26] Update --- dotnet/src/Agents/Abstractions/AggregatorChannel.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs | 2 +- .../Contents/FunctionResultContentTests.cs | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 891baa5524f7..199a64f33818 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -15,7 +15,7 @@ internal sealed class AggregatorChannel(AgentChat chat) : AgentChannel GetHistoryAsync(CancellationToken cancellationToken = default) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 71edaba070e8..a0602f22a1a3 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -43,6 +43,6 @@ protected override IAsyncEnumerable GetHistoryAsync(Cancella /// protected override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + throw new System.NotImplementedException(); // %%% TODO } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs index 6229f98863fe..0a5b37c8ea47 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Text.Json; using Microsoft.SemanticKernel; using Xunit; @@ -99,7 +100,8 @@ public void ItShouldCreateChatMessageContent() // Assert Assert.NotNull(chatMessageContent); - Assert.Single(chatMessageContent.Items); - Assert.Same(sut, chatMessageContent.Items[0]); + Assert.Equal(2, chatMessageContent.Items.Count); + Assert.Single(chatMessageContent.Items.OfType()); + Assert.Same(sut, chatMessageContent.Items.OfType().Single()); } } From 1ec70fd456464b624d83a6c41aa9f412f5f37879 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 10:42:56 -0700 Subject: [PATCH 08/26] Real UT fix --- .../Contents/FunctionResultContentTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs index 0a5b37c8ea47..e9a20f9eda9b 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -101,7 +101,7 @@ public void ItShouldCreateChatMessageContent() // Assert Assert.NotNull(chatMessageContent); Assert.Equal(2, chatMessageContent.Items.Count); - Assert.Single(chatMessageContent.Items.OfType()); - Assert.Same(sut, chatMessageContent.Items.OfType().Single()); + Assert.Single(chatMessageContent.Items.OfType()); + Assert.Same(sut, chatMessageContent.Items.OfType().Single()); } } From dc53a803abd19fb112a25a1c5949b9a709e79c72 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 07:55:36 -0700 Subject: [PATCH 09/26] Clean --- .../Contents/FunctionResultContent.cs | 20 +------------------ .../Contents/FunctionResultContentTests.cs | 5 ++--- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index b76152fa5578..6c0b538ad822 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -83,24 +83,6 @@ public FunctionResultContent(FunctionCallContent functionCallContent, FunctionRe /// The instance. public ChatMessageContent ToChatMessage() { - string? resultContent = this.Result as string; - - if (string.IsNullOrEmpty(resultContent) && - this.Result is ChatMessageContent chatMessageContent) - { - resultContent = chatMessageContent.ToString(); - } - - if (string.IsNullOrEmpty(resultContent) && - this.Result is Exception exception) - { - resultContent = exception.ToString(); - } - - ChatMessageContent chatMessage = new(AuthorRole.Tool, content: resultContent); - - chatMessage.Items.Add(this); - - return chatMessage; + return new ChatMessageContent(AuthorRole.Tool, [this]); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs index e9a20f9eda9b..95433ecb77b0 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -100,8 +100,7 @@ public void ItShouldCreateChatMessageContent() // Assert Assert.NotNull(chatMessageContent); - Assert.Equal(2, chatMessageContent.Items.Count); - Assert.Single(chatMessageContent.Items.OfType()); - Assert.Same(sut, chatMessageContent.Items.OfType().Single()); + Assert.Single(chatMessageContent.Items); + Assert.Same(sut, chatMessageContent.Items[0]); } } From 4d3cf9fe67c8480ab7b515b10ffe61480dea1414 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 07:57:38 -0700 Subject: [PATCH 10/26] namespace --- .../Contents/FunctionResultContent.cs | 1 - .../Contents/FunctionResultContentTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index 6c0b538ad822..ab1e342f7906 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs index 95433ecb77b0..6229f98863fe 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Linq; using System.Text.Json; using Microsoft.SemanticKernel; using Xunit; From dd57cab533a7923ac65e86e4914dee847c7ee16e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 08:07:12 -0700 Subject: [PATCH 11/26] Clean-up --- .../src/Agents/Abstractions/AgentChannel.cs | 39 -------- dotnet/src/Agents/Abstractions/AgentChat.cs | 70 +------------- .../Agents/Abstractions/AggregatorChannel.cs | 5 - .../Agents/Abstractions/ChatHistoryChannel.cs | 40 ++++---- .../Filters/AssistantMessageContext.cs | 45 --------- .../Abstractions/Filters/ChannelProcessors.cs | 92 ------------------- .../Filters/IAssistantMessageFilter.cs | 17 ---- .../Filters/IManualFunctionCallProcessor.cs | 16 ---- .../ITerminatedFunctionResultProcessor.cs | 16 ---- .../Filters/ManualFunctionCallContext.cs | 49 ---------- .../TerminatedFunctionResultContext.cs | 41 --------- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 6 -- .../src/Agents/UnitTests/AgentChannelTests.cs | 5 - .../UnitTests/Internal/BroadcastQueueTests.cs | 10 -- 14 files changed, 20 insertions(+), 431 deletions(-) delete mode 100644 dotnet/src/Agents/Abstractions/Filters/AssistantMessageContext.cs delete mode 100644 dotnet/src/Agents/Abstractions/Filters/ChannelProcessors.cs delete mode 100644 dotnet/src/Agents/Abstractions/Filters/IAssistantMessageFilter.cs delete mode 100644 dotnet/src/Agents/Abstractions/Filters/IManualFunctionCallProcessor.cs delete mode 100644 dotnet/src/Agents/Abstractions/Filters/ITerminatedFunctionResultProcessor.cs delete mode 100644 dotnet/src/Agents/Abstractions/Filters/ManualFunctionCallContext.cs delete mode 100644 dotnet/src/Agents/Abstractions/Filters/TerminatedFunctionResultContext.cs diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index 53635874b6c1..6abe382cc24c 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Agents.Filters; namespace Microsoft.SemanticKernel.Agents; @@ -42,44 +41,6 @@ protected internal abstract IAsyncEnumerable InvokeAsync( /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. protected internal abstract IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken = default); - - /// - /// %%% - /// - /// - /// - /// - protected internal abstract Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default); - - /// - /// %%% - /// - /// - /// - /// - /// - protected Task OnManualFunctionCallAsync(Agent agent, ChatMessageContent message, CancellationToken cancellationToken) - => ChannelProcessors.ProcessManualFunctionCallAsync(this, agent, message, cancellationToken); - - /// - /// %%% - /// - /// - /// - /// - /// - protected Task OnTerminatedFunctionResultAsync(Agent agent, ChatMessageContent message, CancellationToken cancellationToken) - => ChannelProcessors.ProcessTerminatedFunctionResultAsync(this, agent, message, cancellationToken); - - /// - /// %%% - /// - internal IManualFunctionCallProcessor? ManualFunctionCallProcessor { get; set; } // %%% HACK - - /// - /// %%% - /// - internal ITerminatedFunctionResultProcessor? TerminatedFunctionResultProcessor { get; set; } // %%% HACK } /// diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 37eef900b70c..4b27d7771532 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Agents.Extensions; -using Microsoft.SemanticKernel.Agents.Filters; using Microsoft.SemanticKernel.Agents.Internal; using Microsoft.SemanticKernel.ChatCompletion; @@ -51,21 +50,6 @@ public abstract class AgentChat /// protected ChatHistory History { get; } - /// - /// %%% - /// - public IAssistantMessageFilter? AssistantMessageFilter { get; set; } - - /// - /// %%% - /// - public IManualFunctionCallProcessor? ManualFunctionCallProcessor { get; set; } = new HackFunctionCallProcessor(); // %%% HACK - - /// - /// %%% - /// - public ITerminatedFunctionResultProcessor? TerminatedFunctionResultProcessor { get; set; } // = new HackTerminatedFunctionResultProcessor(); // %%% HACK - /// /// Process a series of interactions between the agents participating in this chat. /// @@ -231,19 +215,16 @@ protected async IAsyncEnumerable InvokeAgentAsync( { this.Logger.LogAgentChatInvokedAgentMessage(nameof(InvokeAgentAsync), agent.GetType(), agent.Id, message); - // Capture potential message replacement - ChatMessageContent effectiveMessage = await this.OnAgentInvokedFilterAsync(agent, this.History, message, cancellationToken).ConfigureAwait(false); - // %%% Broadcast everything - messages.Add(effectiveMessage); + messages.Add(message); // Add to primary history - this.History.Add(effectiveMessage); + this.History.Add(message); - if (!effectiveMessage.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) + if (!message.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) { // Yield message to caller - yield return effectiveMessage; + yield return message; } } @@ -271,8 +252,6 @@ async Task GetOrCreateChannelAsync() this.Logger.LogAgentChatCreatingChannel(nameof(InvokeAgentAsync), agent.GetType(), agent.Id); channel = await agent.CreateChannelAsync(cancellationToken).ConfigureAwait(false); - channel.ManualFunctionCallProcessor = this.ManualFunctionCallProcessor; // %%% HACK - channel.TerminatedFunctionResultProcessor = this.TerminatedFunctionResultProcessor; // %%% HACK this._agentChannels.Add(channelKey, channel); @@ -298,28 +277,6 @@ private void ClearActivitySignal() Interlocked.Exchange(ref this._isActive, 0); } - /// - /// %%% - /// - /// - /// - /// - /// - /// - /// - private async Task OnAgentInvokedFilterAsync(Agent agent, ChatHistory history, ChatMessageContent message, CancellationToken cancellationToken) - { - if (this.AssistantMessageFilter != null) - { - AssistantMessageContext context = new(agent, history, message) { CancellationToken = cancellationToken }; - IEnumerable content = await this.AssistantMessageFilter.OnFilterAssistantMessage(context).ConfigureAwait(false); - - return new(message.Role, [.. content], message.ModelId, message.InnerContent, message.Encoding, message.Metadata); // %%% - } - - return message; - } - /// /// Test to ensure chat is not concurrently active and throw exception if it is. /// If not, activity is signaled. @@ -374,23 +331,4 @@ protected AgentChat() this._channelMap = []; this.History = []; } - - private sealed class HackFunctionCallProcessor : IManualFunctionCallProcessor // %%% HACK - { - /// - public async Task OnProcessFunctionCallAsync(ManualFunctionCallContext context) - { - context.Result = await context.Function.InvokeAsync(context.Kernel, context.Arguments).ConfigureAwait(false); - } - } - - private class HackTerminatedFunctionResultProcessor : ITerminatedFunctionResultProcessor - { - public Task OnProcessTerminatedFunctionResultAsync(TerminatedFunctionResultContext context) - { - context.TransformedContent = context.FunctionResults.Select(r => new TextContent(r.ToString())).ToArray(); - - return Task.CompletedTask; - } - } } diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 199a64f33818..80d7b4250631 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -13,11 +13,6 @@ internal sealed class AggregatorChannel(AgentChat chat) : AgentChannel GetHistoryAsync(CancellationToken cancellationToken = default) { return this._chat.GetChatMessagesAsync(cancellationToken); diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index f4b68ef8c75a..0adb1da9b493 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -62,22 +62,22 @@ protected internal sealed override async IAsyncEnumerable In yield return yieldMessage; } - if (yieldMessage != null) - { - // Process manual Function Invocation - if (yieldMessage.Items.Any(i => i is FunctionCallContent)) - { - ChatMessageContent functionResultContent = await this.OnManualFunctionCallAsync(agent, yieldMessage, cancellationToken).ConfigureAwait(false); - yield return functionResultContent; - didPostFunctionResult = true; - } - - // Autocomplete Function Termination, et al... - if (yieldMessage.Items.Any(i => i is FunctionResultContent)) - { - yield return await this.OnTerminatedFunctionResultAsync(agent, yieldMessage, cancellationToken).ConfigureAwait(false); - } - } + //if (yieldMessage != null) %%% + //{ + // // Process manual Function Invocation + // if (yieldMessage.Items.Any(i => i is FunctionCallContent)) + // { + // ChatMessageContent functionResultContent = await this.OnManualFunctionCallAsync(agent, yieldMessage, cancellationToken).ConfigureAwait(false); + // yield return functionResultContent; + // didPostFunctionResult = true; + // } + + // // Autocomplete Function Termination, et al... + // if (yieldMessage.Items.Any(i => i is FunctionResultContent)) + // { + // yield return await this.OnTerminatedFunctionResultAsync(agent, yieldMessage, cancellationToken).ConfigureAwait(false); + // } + //} } while (didPostFunctionResult); } @@ -96,14 +96,6 @@ protected internal sealed override IAsyncEnumerable GetHisto return this._history.ToDescendingAsync(); } - /// - protected internal override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) - { - this._history.Add(functionResultsMessage); - - return Task.CompletedTask; - } - /// /// Initializes a new instance of the class. /// diff --git a/dotnet/src/Agents/Abstractions/Filters/AssistantMessageContext.cs b/dotnet/src/Agents/Abstractions/Filters/AssistantMessageContext.cs deleted file mode 100644 index 66ffb1751eb4..000000000000 --- a/dotnet/src/Agents/Abstractions/Filters/AssistantMessageContext.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading; - -namespace Microsoft.SemanticKernel.Agents.Filters; - -/// -/// %%% -/// -public class AssistantMessageContext -{ - /// - /// %%% - /// - /// - /// - /// - public AssistantMessageContext(Agent agent, IReadOnlyList history, ChatMessageContent message) - { - this.Agent = agent; - this.History = history; - this.Message = message; - } - - /// - /// %%% - /// - public Agent Agent { get; } // %%% METADATA ONLY - - /// - /// %%% - /// - public IReadOnlyList History { get; } - - /// - /// %%% - /// - public ChatMessageContent Message { get; } - - /// - /// %%% - /// - public CancellationToken CancellationToken { get; internal set; } -} diff --git a/dotnet/src/Agents/Abstractions/Filters/ChannelProcessors.cs b/dotnet/src/Agents/Abstractions/Filters/ChannelProcessors.cs deleted file mode 100644 index 26bb355a2f4c..000000000000 --- a/dotnet/src/Agents/Abstractions/Filters/ChannelProcessors.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Agents.Filters; - -internal static class ChannelProcessors -{ - /// - /// %%% - /// - /// - /// - /// - /// - /// - public static async Task ProcessManualFunctionCallAsync( - AgentChannel channel, - Agent agent, - ChatMessageContent messageContent, - CancellationToken cancellationToken) - { - FunctionResultContent resultContent; - FunctionCallContent? functionCallContent = null; - - try - { - functionCallContent = messageContent.Items.OfType().Single(); // %%% CARDINALITY - - if (channel.ManualFunctionCallProcessor == null) - { - throw new KernelException("Manual function call processor not available"); // %%% INFO - } - - KernelAgent kernelAgent = agent as KernelAgent ?? throw new KernelException("Agent must be a KernelAgent to invoke functions."); // %%% - - if (!kernelAgent.Kernel.Plugins.TryGetFunction(functionCallContent.PluginName, functionCallContent.FunctionName, out KernelFunction? function)) - { - throw new KernelException("Unable to resolve function"); // %%% INFO - } - - ManualFunctionCallContext context = new(kernelAgent.Kernel, function, functionCallContent.Arguments) { CancellationToken = cancellationToken }; - - await channel.ManualFunctionCallProcessor.OnProcessFunctionCallAsync(context).ConfigureAwait(false); - - if (context.Result == null) - { - throw new KernelException("Unknown function result"); // %%% INFO - } - - resultContent = new(functionCallContent, context.Result); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception exception) -#pragma warning restore CA1031 // Do not catch general exception types - { - functionCallContent ??= new("unknown function"); - resultContent = new FunctionResultContent(functionCallContent, exception); - } - - ChatMessageContent resultMessage = resultContent.ToChatMessage(); - - await channel.CaptureFunctionResultAsync(resultMessage, cancellationToken).ConfigureAwait(false); - - return resultMessage; - } - - internal static async Task ProcessTerminatedFunctionResultAsync(AgentChannel agentChannel, Agent agent, ChatMessageContent message, CancellationToken cancellationToken) - { - if (agentChannel.TerminatedFunctionResultProcessor != null) - { - TerminatedFunctionResultContext context = new(message.Items.OfType().ToArray()) { CancellationToken = cancellationToken }; // %%% LINQ - - await agentChannel.TerminatedFunctionResultProcessor.OnProcessTerminatedFunctionResultAsync(context).ConfigureAwait(false); - - return - new(AuthorRole.Assistant, content: null) - { - //Items = context.FunctionResults, %%% TBD - }; - } - // Default logic if no filter - return - new(AuthorRole.Assistant, content: message.Content) - { - Items = [.. message.Items.OfType().Cast()], - }; - } -} diff --git a/dotnet/src/Agents/Abstractions/Filters/IAssistantMessageFilter.cs b/dotnet/src/Agents/Abstractions/Filters/IAssistantMessageFilter.cs deleted file mode 100644 index 7e3bf8bc229d..000000000000 --- a/dotnet/src/Agents/Abstractions/Filters/IAssistantMessageFilter.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.SemanticKernel.Agents.Filters; - -/// -/// %%% -/// -public interface IAssistantMessageFilter -{ - /// - /// %%% - /// - /// The containing the result of the function's invocation. - Task> OnFilterAssistantMessage(AssistantMessageContext context); -} diff --git a/dotnet/src/Agents/Abstractions/Filters/IManualFunctionCallProcessor.cs b/dotnet/src/Agents/Abstractions/Filters/IManualFunctionCallProcessor.cs deleted file mode 100644 index 5e1387786e17..000000000000 --- a/dotnet/src/Agents/Abstractions/Filters/IManualFunctionCallProcessor.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Threading.Tasks; - -namespace Microsoft.SemanticKernel.Agents.Filters; - -/// -/// %%% -/// -public interface IManualFunctionCallProcessor -{ - /// - /// %%% - /// - /// The containing the result of the function's invocation. - Task OnProcessFunctionCallAsync(ManualFunctionCallContext context); -} diff --git a/dotnet/src/Agents/Abstractions/Filters/ITerminatedFunctionResultProcessor.cs b/dotnet/src/Agents/Abstractions/Filters/ITerminatedFunctionResultProcessor.cs deleted file mode 100644 index 1c57e2f4fc45..000000000000 --- a/dotnet/src/Agents/Abstractions/Filters/ITerminatedFunctionResultProcessor.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Threading.Tasks; - -namespace Microsoft.SemanticKernel.Agents.Filters; - -/// -/// %%% -/// -public interface ITerminatedFunctionResultProcessor -{ - /// - /// %%% - /// - /// The containing the result of the terminated function. - Task OnProcessTerminatedFunctionResultAsync(TerminatedFunctionResultContext context); -} diff --git a/dotnet/src/Agents/Abstractions/Filters/ManualFunctionCallContext.cs b/dotnet/src/Agents/Abstractions/Filters/ManualFunctionCallContext.cs deleted file mode 100644 index 6567d4d9bd65..000000000000 --- a/dotnet/src/Agents/Abstractions/Filters/ManualFunctionCallContext.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Threading; - -namespace Microsoft.SemanticKernel.Agents.Filters; - -/// -/// %%% -/// -public class ManualFunctionCallContext -{ - /// - /// Initializes a new instance of the class. - /// - /// The containing services, plugins, and other state for use throughout the operation. - /// The with which this filter is associated. - /// The arguments associated with the operation. - internal ManualFunctionCallContext(Kernel kernel, KernelFunction function, KernelArguments? arguments) - { - this.Kernel = kernel; - this.Function = function; - this.Arguments = arguments ?? []; - } - - /// - /// The to monitor for cancellation requests. - /// The default is . - /// - public CancellationToken CancellationToken { get; init; } - - /// - /// Gets the containing services, plugins, and other state for use throughout the operation. - /// - public Kernel Kernel { get; } - - /// - /// Gets the with which this filter is associated. - /// - public KernelFunction Function { get; } - - /// - /// Gets the arguments associated with the operation. - /// - public KernelArguments Arguments { get; } - - /// - /// Gets or sets the result of the function's invocation. - /// - public FunctionResult? Result { get; set; } -} diff --git a/dotnet/src/Agents/Abstractions/Filters/TerminatedFunctionResultContext.cs b/dotnet/src/Agents/Abstractions/Filters/TerminatedFunctionResultContext.cs deleted file mode 100644 index 9b3655350c96..000000000000 --- a/dotnet/src/Agents/Abstractions/Filters/TerminatedFunctionResultContext.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Threading; - -namespace Microsoft.SemanticKernel.Agents.Filters; - -/// -/// %%% -/// -public class TerminatedFunctionResultContext -{ - /// - /// Initializes a new instance of the class. - /// - /// - internal TerminatedFunctionResultContext(IReadOnlyList functionResults) - { - this.FunctionResults = functionResults; - } - - /// - /// The to monitor for cancellation requests. - /// The default is . - /// - public CancellationToken CancellationToken { get; init; } - - ///// - ///// Gets the containing services, plugins, and other state for use throughout the operation. - ///// - //public Kernel Kernel { get; } // %%% - - /// - /// Gets the with which this filter is associated. - /// - public IReadOnlyList FunctionResults { get; } - - /// - /// %%% - /// - public IEnumerable? TransformedContent { get; set; } -} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index a0602f22a1a3..6bbc33545f46 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -39,10 +39,4 @@ protected override IAsyncEnumerable GetHistoryAsync(Cancella { return AssistantThreadActions.GetMessagesAsync(this._client, this._threadId, cancellationToken); } - - /// - protected override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) - { - throw new System.NotImplementedException(); // %%% TODO - } } diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs index e23c538576a5..c35bd5bc365d 100644 --- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs @@ -61,11 +61,6 @@ protected internal override Task ReceiveAsync(IEnumerable hi { throw new NotImplementedException(); } - - protected internal override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } } private sealed class NextAgent : TestAgent; diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs index 8b1c27ab6c8c..4c90aa9c2cdc 100644 --- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs @@ -131,11 +131,6 @@ private sealed class TestChannel : AgentChannel public List ReceivedMessages { get; } = []; - protected internal override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -159,11 +154,6 @@ private sealed class BadChannel : AgentChannel { public TimeSpan ReceiveDuration { get; set; } = TimeSpan.FromSeconds(0.1); - protected internal override Task CaptureFunctionResultAsync(ChatMessageContent functionResultsMessage, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) { throw new NotImplementedException(); From 58817d89f0a33a780f2b0658f887ec973e2501ee Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 08:13:05 -0700 Subject: [PATCH 12/26] Namespace --- dotnet/src/Agents/Abstractions/AgentChat.cs | 1 - dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 4b27d7771532..6db8929417e8 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 0adb1da9b493..69995f23f509 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; From 3571d53459032cd98aeb3f673dae50bf77b9e6ea Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 09:14:33 -0700 Subject: [PATCH 13/26] Checkpoint --- .../src/Agents/Abstractions/AgentChannel.cs | 6 +- dotnet/src/Agents/Abstractions/AgentChat.cs | 4 +- .../Agents/Abstractions/AggregatorChannel.cs | 6 +- .../Agents/Abstractions/ChatHistoryChannel.cs | 79 ++++++++----------- .../Agents/OpenAI/AssistantThreadActions.cs | 10 ++- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 12 ++- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 2 +- 7 files changed, 58 insertions(+), 61 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index 6abe382cc24c..9788464a2adb 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -31,7 +31,7 @@ public abstract class AgentChannel /// The agent actively interacting with the chat. /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. - protected internal abstract IAsyncEnumerable InvokeAsync( + protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( Agent agent, CancellationToken cancellationToken = default); @@ -59,12 +59,12 @@ public abstract class AgentChannel : AgentChannel where TAgent : Agent /// The agent actively interacting with the chat. /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. - protected internal abstract IAsyncEnumerable InvokeAsync( + protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( TAgent agent, CancellationToken cancellationToken = default); /// - protected internal override IAsyncEnumerable InvokeAsync( + protected internal override IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( Agent agent, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 6db8929417e8..67490a6aab97 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -210,7 +210,7 @@ protected async IAsyncEnumerable InvokeAgentAsync( // Invoke agent & process response List 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); @@ -220,7 +220,7 @@ protected async IAsyncEnumerable InvokeAgentAsync( // Add to primary history this.History.Add(message); - if (!message.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) + if (isVisible) { // Yield message to caller yield return message; diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 80d7b4250631..73561a4eba8b 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -18,7 +18,7 @@ protected internal override IAsyncEnumerable GetHistoryAsync return this._chat.GetChatMessagesAsync(cancellationToken); } - protected internal override async IAsyncEnumerable 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; @@ -27,7 +27,7 @@ protected internal override async IAsyncEnumerable 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; @@ -43,7 +43,7 @@ protected internal override async IAsyncEnumerable InvokeAsy AuthorName = agent.Name }; - yield return message; + yield return (IsVisible: true, message); } } diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 69995f23f509..46ab13799699 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -16,7 +17,7 @@ public class ChatHistoryChannel : AgentChannel private readonly ChatHistory _history; /// - protected internal sealed override async IAsyncEnumerable InvokeAsync( + protected internal sealed override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( Agent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -25,60 +26,48 @@ protected internal sealed override async IAsyncEnumerable In throw new KernelException($"Invalid channel binding for agent: {agent.Id} ({agent.GetType().FullName})"); } - bool didPostFunctionResult; - do - { - didPostFunctionResult = false; // Clear on each iteration + int messageCount = this._history.Count; + HashSet mutatedHistory = []; - int messageCount = this._history.Count; - HashSet mutatedHistory = []; + Queue messageQueue = []; + ChatMessageContent? yieldMessage = null; + await foreach (ChatMessageContent responseMessage in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false)) + { + for (int messageIndex = messageCount; messageIndex < this._history.Count; messageIndex++) + { + ChatMessageContent mutatedMessage = this._history[messageIndex]; + mutatedHistory.Add(mutatedMessage); + messageQueue.Enqueue(mutatedMessage); + } - Queue messageQueue = []; - ChatMessageContent? yieldMessage = null; - await foreach (ChatMessageContent responseMessage in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false)) + if (!mutatedHistory.Contains(responseMessage)) { - for (int messageIndex = messageCount; messageIndex < this._history.Count; messageIndex++) - { - ChatMessageContent mutatedMessage = this._history[messageIndex]; - mutatedHistory.Add(mutatedMessage); - messageQueue.Enqueue(mutatedMessage); - } + this._history.Add(responseMessage); + messageQueue.Enqueue(responseMessage); + } - if (!mutatedHistory.Contains(responseMessage)) - { - this._history.Add(responseMessage); - messageQueue.Enqueue(responseMessage); - } + yieldMessage = messageQueue.Dequeue(); + yield return (IsMessageVisible(yieldMessage), yieldMessage); + } - yieldMessage = messageQueue.Dequeue(); - yield return yieldMessage; - } + while (messageQueue.Count > 0) + { + yieldMessage = messageQueue.Dequeue(); - while (messageQueue.Count > 0) - { - yieldMessage = messageQueue.Dequeue(); + yield return (IsMessageVisible(yieldMessage), yieldMessage); + } - yield return yieldMessage; + bool IsMessageVisible(ChatMessageContent message) + { + // Process manual Function Invocation + if (message.Items.Any(i => i is FunctionCallContent) || + message.Items.Any(i => i is FunctionResultContent) && messageQueue.Count == 0) + { + return false; } - //if (yieldMessage != null) %%% - //{ - // // Process manual Function Invocation - // if (yieldMessage.Items.Any(i => i is FunctionCallContent)) - // { - // ChatMessageContent functionResultContent = await this.OnManualFunctionCallAsync(agent, yieldMessage, cancellationToken).ConfigureAwait(false); - // yield return functionResultContent; - // didPostFunctionResult = true; - // } - - // // Autocomplete Function Termination, et al... - // if (yieldMessage.Items.Any(i => i is FunctionResultContent)) - // { - // yield return await this.OnTerminatedFunctionResultAsync(agent, yieldMessage, cancellationToken).ConfigureAwait(false); - // } - //} + return true; } - while (didPostFunctionResult); } /// diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs index b1be5bb52765..cf77b271dd65 100644 --- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs @@ -136,7 +136,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist /// The logger to utilize (might be agent or channel scoped) /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. - public static async IAsyncEnumerable InvokeAsync( + public static async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( OpenAIAssistantAgent agent, AssistantsClient client, string threadId, @@ -190,7 +190,7 @@ public static async IAsyncEnumerable InvokeAsync( if (activeFunctionSteps.Length > 0) { // Emit function-call content - yield return GenerateFunctionCallContent(agent.GetName(), activeFunctionSteps); + yield return (IsVisible: false, Message: GenerateFunctionCallContent(agent.GetName(), activeFunctionSteps)); // Invoke functions for each tool-step IEnumerable> functionResultTasks = ExecuteFunctionSteps(agent, activeFunctionSteps, cancellationToken); @@ -224,12 +224,14 @@ public static async IAsyncEnumerable InvokeAsync( foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls) { + bool isVisible = false; ChatMessageContent? content = null; // Process code-interpreter content if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter) { content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); + isVisible = true; } // Process function result content else if (toolCall is RunStepFunctionToolCall toolFunction) @@ -242,7 +244,7 @@ public static async IAsyncEnumerable InvokeAsync( { ++messageCount; - yield return content; + yield return (isVisible, Message: content); } } } @@ -276,7 +278,7 @@ public static async IAsyncEnumerable InvokeAsync( { ++messageCount; - yield return content; + yield return (IsVisible: false, Message: content); } } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 31c0bb1c0de7..8e8797fa8885 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -240,13 +240,19 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul /// The thread identifier /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. - public IAsyncEnumerable InvokeAsync( + public async IAsyncEnumerable InvokeAsync( string threadId, - CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { this.ThrowIfDeleted(); - return AssistantThreadActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, cancellationToken); + await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, cancellationToken).ConfigureAwait(false)) + { + if (isVisible) + { + yield return message; + } + } } /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 6bbc33545f46..48fdefa65fe9 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -25,7 +25,7 @@ protected override async Task ReceiveAsync(IEnumerable histo } /// - protected override IAsyncEnumerable InvokeAsync( + protected override IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( OpenAIAssistantAgent agent, CancellationToken cancellationToken) { From 27fead015937b38579ef408804dee4abc462f8b8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 09:16:47 -0700 Subject: [PATCH 14/26] Sync test mocks --- dotnet/src/Agents/UnitTests/AgentChannelTests.cs | 4 ++-- dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs index c35bd5bc365d..2a680614a54f 100644 --- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs @@ -40,11 +40,11 @@ private sealed class TestChannel : AgentChannel { public int InvokeCount { get; private set; } - public IAsyncEnumerable InvokeAgentAsync(Agent agent, CancellationToken cancellationToken = default) + public IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAgentAsync(Agent agent, CancellationToken cancellationToken = default) => base.InvokeAsync(agent, cancellationToken); #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected internal override async IAsyncEnumerable InvokeAsync(TestAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) + protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(TestAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { this.InvokeCount++; diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs index 4c90aa9c2cdc..452a0566e11f 100644 --- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs @@ -136,7 +136,7 @@ protected internal override IAsyncEnumerable GetHistoryAsync throw new NotImplementedException(); } - protected internal override IAsyncEnumerable InvokeAsync(Agent agent, CancellationToken cancellationToken = default) + protected internal override IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(Agent agent, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } @@ -159,7 +159,7 @@ protected internal override IAsyncEnumerable GetHistoryAsync throw new NotImplementedException(); } - protected internal override IAsyncEnumerable InvokeAsync(Agent agent, CancellationToken cancellationToken = default) + protected internal override IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(Agent agent, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } From a1833d0da916c535496a61fd6f7b629faade2a4b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 09:25:38 -0700 Subject: [PATCH 15/26] Logic clean-up --- dotnet/src/Agents/Abstractions/AgentChat.cs | 2 +- .../src/Agents/Abstractions/ChatHistoryChannel.cs | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 67490a6aab97..473b150cca75 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -233,7 +233,7 @@ protected async IAsyncEnumerable InvokeAgentAsync( this._agentChannels .Where(kvp => kvp.Value != channel) .Select(kvp => new ChannelReference(kvp.Value, kvp.Key)); - this._broadcastQueue.Enqueue(channelRefs, messages); // %%% BROADCAST ALL + this._broadcastQueue.Enqueue(channelRefs, messages); this.Logger.LogAgentChatInvokedAgent(nameof(InvokeAgentAsync), agent.GetType(), agent.Id); } diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 46ab13799699..5f1e93892778 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -57,17 +57,10 @@ public class ChatHistoryChannel : AgentChannel yield return (IsMessageVisible(yieldMessage), yieldMessage); } - bool IsMessageVisible(ChatMessageContent message) - { - // Process manual Function Invocation - if (message.Items.Any(i => i is FunctionCallContent) || - message.Items.Any(i => i is FunctionResultContent) && messageQueue.Count == 0) - { - return false; - } - - return true; - } + // Function content not visibile, unless result is the final message. + bool IsMessageVisible(ChatMessageContent message) => + (message.Items.Any(i => i is FunctionCallContent) || + (message.Items.Any(i => i is FunctionResultContent) && messageQueue.Count > 0)); } /// From 0ca0197ce8c9f0d3484936f7144b83e8bfd85f70 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 09:26:11 -0700 Subject: [PATCH 16/26] Comment clean-up --- dotnet/src/Agents/Abstractions/AgentChat.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 473b150cca75..f14dc67369d4 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -214,7 +214,6 @@ protected async IAsyncEnumerable InvokeAgentAsync( { this.Logger.LogAgentChatInvokedAgentMessage(nameof(InvokeAgentAsync), agent.GetType(), agent.Id, message); - // %%% Broadcast everything messages.Add(message); // Add to primary history From 1ed5439d538511c26eb8871c4cbb9f0613f7ef84 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 09:50:37 -0700 Subject: [PATCH 17/26] Assistant logic fix --- dotnet/samples/Concepts/FunctionSanity.cs | 517 ++++++++++++++++++ .../Concepts/FunctionSanity_Streaming.cs | 324 +++++++++++ .../Agents/OpenAI/AssistantThreadActions.cs | 2 +- 3 files changed, 842 insertions(+), 1 deletion(-) create mode 100644 dotnet/samples/Concepts/FunctionSanity.cs create mode 100644 dotnet/samples/Concepts/FunctionSanity_Streaming.cs diff --git a/dotnet/samples/Concepts/FunctionSanity.cs b/dotnet/samples/Concepts/FunctionSanity.cs new file mode 100644 index 000000000000..57ebd996cc18 --- /dev/null +++ b/dotnet/samples/Concepts/FunctionSanity.cs @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace Sanity; + +public class FunctionSanity(ITestOutputHelper output) : BaseTest(output) +{ + private static readonly string[] s_userInput = + [ + //"Hello", + "What is the special soup and what is its price?", + "What is the special drink and what is its price?", + //"Thank you" + ]; + + ////////////////////////////// + // CHAT COMPLETION SERVICE + + [Fact] + public async Task ServiceBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServiceManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunServiceTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task ServiceFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServicePromptFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new PromptFilter()); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServiceAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunServiceTestAsync(kernel); + } + + ////////////////////////////// + // KERNEL PROMPT FUNCTION + + [Fact] + public async Task KernelBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunKernelTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task KernelFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelPromptFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new PromptFilter()); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunKernelTestAsync(kernel); + } + + ////////////////////////////// + // AGENT + + [Fact] + public async Task AgentInvokeBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentChatBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentChatTestAsync(kernel); + } + + [Fact] + public async Task AgentInvokeManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task AgentChatManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentChatTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task AgentInvokeFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentChatFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunAgentChatTestAsync(kernel); + } + + [Fact] + public async Task AgentInvokeAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentChatAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunAgentChatTestAsync(kernel); + } + + ////////////////////////////// + // ASSISTANT + + [Fact] + public async Task AssistantInvokeBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AssistantChatBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentChatTestAsync(kernel, useAssistant: true); + } + + [Fact] + public async Task AssistantInvokeManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); + } + + [Fact] + public async Task AssistantChatManualFunctionTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentChatTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions, useAssistant: true); + } + + [Fact] + public async Task AssistantInvokeFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AssistantChatFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunAgentChatTestAsync(kernel, useAssistant: true); + } + + [Fact] + public async Task AssistantInvokeAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AssistantChatAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunAgentChatTestAsync(kernel, useAssistant: true); + } + + ////////////////////////////// + // KERNEL TEST + private async Task RunKernelTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + async Task InvokeWithInputAsync(string input) + { + Console.WriteLine($"[TextContent] {AuthorRole.User}: '{input}'"); + + KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(input, new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }); + + ChatMessageContent content = (await kernel.InvokeAsync(promptFunction))!; + this.WriteContent(content); + } + } + + ////////////////////////////// + // CHAT COMPLETION SERVICE TEST + private async Task RunServiceTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + IChatCompletionService service = kernel.GetRequiredService(); + + ChatHistory chat = []; + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.Add(userContent); + this.WriteContent(userContent); + + foreach (ChatMessageContent content in await service.GetChatMessageContentsAsync(chat, new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) + { + if (content.Role != AuthorRole.Tool && !content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) + { + chat.Add(content); + } + + this.WriteContent(content); + } + } + } + + ////////////////////////////// + // AGENT TEST + private async Task RunAgentTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) + { + ChatCompletionAgent agent = + new() + { + Instructions = "Answer questions about the menu.", + Kernel = kernel, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, + }; + + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + ChatHistory chat = []; + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.Add(userContent); + this.WriteContent(userContent); + + await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) + { + if (content.Role != AuthorRole.Tool && !content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) + { + chat.Add(content); + } + + this.WriteContent(content); + } + } + } + + ////////////////////////////// + // AGENT CHAT TEST + private async Task RunAgentChatTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null, bool useAssistant = false) + { + Agent agent = + useAssistant ? + await OpenAIAssistantAgent.CreateAsync(kernel, new(this.ApiKey, this.Endpoint), new() { ModelId = this.Model, Instructions = "Answer questions about the menu." }) : + new ChatCompletionAgent() + { + Instructions = "Answer questions about the menu.", + Kernel = kernel, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, + }; + + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + AgentGroupChat chat = new(); + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + Console.WriteLine("================================"); + Console.WriteLine("PRIMARY HISTORY"); + Console.WriteLine("================================"); + IEnumerable history = chat.GetChatMessagesAsync().ToEnumerable().Reverse(); + foreach (ChatMessageContent content in history) + { + this.WriteContent(content); + } + + if (useAssistant) + { + await ((OpenAIAssistantAgent)agent).DeleteAsync(); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeWithInputAsync(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); + } + } + } + + ////////////////////////////// + // UTILITY + private void WriteContent(ChatMessageContent content) + { + Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); + } + + ////////////////////////////// + // PLUGIN + 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 prices of the specials from the menu.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] + public string GetPrices() + { + return @" +Clam Chowder: $9.99 +Cobb Salad: $9.99 +Chai Tea: $9.99 +"; + } + } + + ////////////////////////////// + // FUNCTION FILTER + private sealed class FunctionFilter : IFunctionInvocationFilter + { + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) + { + if (context.Function.PluginName == nameof(MenuPlugin)) + { + context.Result = new FunctionResult(context.Function, "Menu not available."); + return Task.CompletedTask; + } + + return next(context); + } + } + + ////////////////////////////// + // PROMPT FILTER + private sealed class PromptFilter : IFunctionInvocationFilter + { + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) + { + if (context.Function.PluginName != nameof(MenuPlugin)) + { + context.Result = new FunctionResult(context.Function, new ChatMessageContent(AuthorRole.Assistant, "Intercepted message.")); + return Task.CompletedTask; + } + + return next(context); + } + } + + ////////////////////////////// + // AUTO INVOCATION FILTER + private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter + { + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + FunctionCallContent[] functionCalls = FunctionCallContent.GetFunctionCalls(context.ChatHistory.Last()).ToArray(); + + await next(context); + + if (context.Function.PluginName == nameof(MenuPlugin)) + { + //context.Result = new FunctionResult(context.Function, "Menu not available."); + context.Terminate = terminate; + } + } + } +} diff --git a/dotnet/samples/Concepts/FunctionSanity_Streaming.cs b/dotnet/samples/Concepts/FunctionSanity_Streaming.cs new file mode 100644 index 000000000000..4681aecf05e7 --- /dev/null +++ b/dotnet/samples/Concepts/FunctionSanity_Streaming.cs @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace Sanity; + +public class FunctionSanity_Streaming(ITestOutputHelper output) : BaseTest(output) +{ + private static readonly string[] s_userInput = + [ + "Hello", + "What is the special soup and what is its price?", + "What is the special drink and what is its price?", + "Thank you" + ]; + + ////////////////////////////// + // CHAT COMPLETION SERVICE + + [Fact] + public async Task ServiceBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServiceFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServicePromptFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new PromptFilter()); + await RunServiceTestAsync(kernel); + } + + [Fact] + public async Task ServiceAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunServiceTestAsync(kernel); + } + + ////////////////////////////// + // KERNEL PROMPT FUNCTION + + [Fact] + public async Task KernelBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelPromptFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new PromptFilter()); + await RunKernelTestAsync(kernel); + } + + [Fact] + public async Task KernelAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunKernelTestAsync(kernel); + } + + ////////////////////////////// + // AGENT + + [Fact] + public async Task AgentInvokeBasicTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentInvokeFunctionFilterTestAsync() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.FunctionInvocationFilters.Add(new FunctionFilter()); + await RunAgentTestAsync(kernel); + } + + [Fact] + public async Task AgentInvokeAutoFilterTestAsync() + { + IKernelBuilder builder = Kernel.CreateBuilder(); + + builder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + TestConfiguration.AzureOpenAI.ApiKey); + + builder.Services.AddSingleton(new AutoInvocationFilter()); + + Kernel kernel = builder.Build(); + + await RunAgentTestAsync(kernel); + } + + ////////////////////////////// + // KERNEL TEST + private async Task RunKernelTestAsync(Kernel kernel) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + async Task InvokeWithInputAsync(string input) + { + Console.WriteLine($"[TextContent] {AuthorRole.User}: '{input}'"); + + KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(input, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }); + + await foreach (StreamingChatMessageContent content in kernel.InvokeStreamingAsync(promptFunction)) + { + this.WriteContent(content); + } + } + } + + ////////////////////////////// + // CHAT COMPLETION SERVICE TEST + private async Task RunServiceTestAsync(Kernel kernel) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + IChatCompletionService service = kernel.GetRequiredService(); + + ChatHistory chat = []; + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.Add(userContent); + this.WriteContent(userContent); + + await foreach (StreamingChatMessageContent content in service.GetStreamingChatMessageContentsAsync(chat, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) + { + this.WriteContent(content); + } + } + } + + ////////////////////////////// + // AGENT TEST + private async Task RunAgentTestAsync(Kernel kernel) + { + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + ChatCompletionAgent agent = + new() + { + Instructions = "Answer questions about the menu.", + Kernel = kernel, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + }; + + ChatHistory chat = []; + + foreach (string input in s_userInput) + { + await InvokeWithInputAsync(input); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeWithInputAsync(string input) + { + ChatMessageContent userContent = new(AuthorRole.User, input); + chat.Add(userContent); + this.WriteContent(userContent); + + await foreach (StreamingChatMessageContent content in agent.InvokeStreamingAsync(chat)) + { + //if (content.Role != AuthorRole.Tool) // %%% BIG PROBLEM + //{ + // chat.Add(content); // %%% AWKWARD (BUILDING HISTORY) + //} + + this.WriteContent(content); + } + } + } + + ////////////////////////////// + // UTILITY + private void WriteContent(ChatMessageContent content) + { + Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); + } + + private void WriteContent(StreamingChatMessageContent content) + { + Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); + } + + ////////////////////////////// + // PLUGIN + 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"; + } + } + + ////////////////////////////// + // FUNCTION FILTER + private sealed class FunctionFilter : IFunctionInvocationFilter + { + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) + { + if (context.Function.PluginName == nameof(MenuPlugin)) + { + context.Result = new FunctionResult(context.Function, "Menu not available."); + return Task.CompletedTask; + } + + return next(context); + } + } + + ////////////////////////////// + // PROMPT FILTER + private sealed class PromptFilter : IFunctionInvocationFilter + { + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) + { + if (context.Function.PluginName != nameof(MenuPlugin)) + { + StreamingChatMessageContent[] contents = [new StreamingChatMessageContent(AuthorRole.Assistant, "Intercepted message.")]; + IAsyncEnumerable contentsAsync = contents.ToAsyncEnumerable(); + context.Result = new FunctionResult(context.Function, contentsAsync); + return Task.CompletedTask; + } + + return next(context); + } + } + + ////////////////////////////// + // AUTO INVOCATION FILTER + private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter + { + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + FunctionCallContent[] functionCalls = FunctionCallContent.GetFunctionCalls(context.ChatHistory.Last()).ToArray(); + + await next(context); + + if (context.Function.PluginName == nameof(MenuPlugin)) + { + //context.Result = new FunctionResult(context.Function, "Menu not available."); + context.Terminate = terminate; + } + } + } +} diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs index cf77b271dd65..eeab85c00df4 100644 --- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs @@ -278,7 +278,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist { ++messageCount; - yield return (IsVisible: false, Message: content); + yield return (IsVisible: true, Message: content); } } } From af28337a9f0f5a6c1b612125c70160beaf123c11 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 09:51:49 -0700 Subject: [PATCH 18/26] Typo --- dotnet/samples/Concepts/FunctionSanity.cs | 517 ------------------ .../Concepts/FunctionSanity_Streaming.cs | 324 ----------- .../Agents/Abstractions/ChatHistoryChannel.cs | 2 +- 3 files changed, 1 insertion(+), 842 deletions(-) delete mode 100644 dotnet/samples/Concepts/FunctionSanity.cs delete mode 100644 dotnet/samples/Concepts/FunctionSanity_Streaming.cs diff --git a/dotnet/samples/Concepts/FunctionSanity.cs b/dotnet/samples/Concepts/FunctionSanity.cs deleted file mode 100644 index 57ebd996cc18..000000000000 --- a/dotnet/samples/Concepts/FunctionSanity.cs +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.ComponentModel; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Sanity; - -public class FunctionSanity(ITestOutputHelper output) : BaseTest(output) -{ - private static readonly string[] s_userInput = - [ - //"Hello", - "What is the special soup and what is its price?", - "What is the special drink and what is its price?", - //"Thank you" - ]; - - ////////////////////////////// - // CHAT COMPLETION SERVICE - - [Fact] - public async Task ServiceBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServiceManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunServiceTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task ServiceFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServicePromptFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new PromptFilter()); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServiceAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunServiceTestAsync(kernel); - } - - ////////////////////////////// - // KERNEL PROMPT FUNCTION - - [Fact] - public async Task KernelBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunKernelTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task KernelFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelPromptFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new PromptFilter()); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunKernelTestAsync(kernel); - } - - ////////////////////////////// - // AGENT - - [Fact] - public async Task AgentInvokeBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentChatBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentChatTestAsync(kernel); - } - - [Fact] - public async Task AgentInvokeManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task AgentChatManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentChatTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task AgentInvokeFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentChatFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunAgentChatTestAsync(kernel); - } - - [Fact] - public async Task AgentInvokeAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentChatAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunAgentChatTestAsync(kernel); - } - - ////////////////////////////// - // ASSISTANT - - [Fact] - public async Task AssistantInvokeBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AssistantChatBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentChatTestAsync(kernel, useAssistant: true); - } - - [Fact] - public async Task AssistantInvokeManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions); - } - - [Fact] - public async Task AssistantChatManualFunctionTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentChatTestAsync(kernel, ToolCallBehavior.EnableKernelFunctions, useAssistant: true); - } - - [Fact] - public async Task AssistantInvokeFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AssistantChatFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunAgentChatTestAsync(kernel, useAssistant: true); - } - - [Fact] - public async Task AssistantInvokeAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AssistantChatAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunAgentChatTestAsync(kernel, useAssistant: true); - } - - ////////////////////////////// - // KERNEL TEST - private async Task RunKernelTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - async Task InvokeWithInputAsync(string input) - { - Console.WriteLine($"[TextContent] {AuthorRole.User}: '{input}'"); - - KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(input, new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }); - - ChatMessageContent content = (await kernel.InvokeAsync(promptFunction))!; - this.WriteContent(content); - } - } - - ////////////////////////////// - // CHAT COMPLETION SERVICE TEST - private async Task RunServiceTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - IChatCompletionService service = kernel.GetRequiredService(); - - ChatHistory chat = []; - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - this.WriteContent(userContent); - - foreach (ChatMessageContent content in await service.GetChatMessageContentsAsync(chat, new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) - { - if (content.Role != AuthorRole.Tool && !content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) - { - chat.Add(content); - } - - this.WriteContent(content); - } - } - } - - ////////////////////////////// - // AGENT TEST - private async Task RunAgentTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null) - { - ChatCompletionAgent agent = - new() - { - Instructions = "Answer questions about the menu.", - Kernel = kernel, - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, - }; - - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - ChatHistory chat = []; - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - this.WriteContent(userContent); - - await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) - { - if (content.Role != AuthorRole.Tool && !content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) - { - chat.Add(content); - } - - this.WriteContent(content); - } - } - } - - ////////////////////////////// - // AGENT CHAT TEST - private async Task RunAgentChatTestAsync(Kernel kernel, ToolCallBehavior? toolCallBehavior = null, bool useAssistant = false) - { - Agent agent = - useAssistant ? - await OpenAIAssistantAgent.CreateAsync(kernel, new(this.ApiKey, this.Endpoint), new() { ModelId = this.Model, Instructions = "Answer questions about the menu." }) : - new ChatCompletionAgent() - { - Instructions = "Answer questions about the menu.", - Kernel = kernel, - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = toolCallBehavior ?? ToolCallBehavior.AutoInvokeKernelFunctions }, - }; - - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - AgentGroupChat chat = new(); - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - Console.WriteLine("================================"); - Console.WriteLine("PRIMARY HISTORY"); - Console.WriteLine("================================"); - IEnumerable history = chat.GetChatMessagesAsync().ToEnumerable().Reverse(); - foreach (ChatMessageContent content in history) - { - this.WriteContent(content); - } - - if (useAssistant) - { - await ((OpenAIAssistantAgent)agent).DeleteAsync(); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeWithInputAsync(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); - } - } - } - - ////////////////////////////// - // UTILITY - private void WriteContent(ChatMessageContent content) - { - Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); - } - - ////////////////////////////// - // PLUGIN - 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 prices of the specials from the menu.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetPrices() - { - return @" -Clam Chowder: $9.99 -Cobb Salad: $9.99 -Chai Tea: $9.99 -"; - } - } - - ////////////////////////////// - // FUNCTION FILTER - private sealed class FunctionFilter : IFunctionInvocationFilter - { - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - if (context.Function.PluginName == nameof(MenuPlugin)) - { - context.Result = new FunctionResult(context.Function, "Menu not available."); - return Task.CompletedTask; - } - - return next(context); - } - } - - ////////////////////////////// - // PROMPT FILTER - private sealed class PromptFilter : IFunctionInvocationFilter - { - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - if (context.Function.PluginName != nameof(MenuPlugin)) - { - context.Result = new FunctionResult(context.Function, new ChatMessageContent(AuthorRole.Assistant, "Intercepted message.")); - return Task.CompletedTask; - } - - return next(context); - } - } - - ////////////////////////////// - // AUTO INVOCATION FILTER - private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter - { - public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) - { - FunctionCallContent[] functionCalls = FunctionCallContent.GetFunctionCalls(context.ChatHistory.Last()).ToArray(); - - await next(context); - - if (context.Function.PluginName == nameof(MenuPlugin)) - { - //context.Result = new FunctionResult(context.Function, "Menu not available."); - context.Terminate = terminate; - } - } - } -} diff --git a/dotnet/samples/Concepts/FunctionSanity_Streaming.cs b/dotnet/samples/Concepts/FunctionSanity_Streaming.cs deleted file mode 100644 index 4681aecf05e7..000000000000 --- a/dotnet/samples/Concepts/FunctionSanity_Streaming.cs +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.ComponentModel; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Sanity; - -public class FunctionSanity_Streaming(ITestOutputHelper output) : BaseTest(output) -{ - private static readonly string[] s_userInput = - [ - "Hello", - "What is the special soup and what is its price?", - "What is the special drink and what is its price?", - "Thank you" - ]; - - ////////////////////////////// - // CHAT COMPLETION SERVICE - - [Fact] - public async Task ServiceBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServiceFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServicePromptFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new PromptFilter()); - await RunServiceTestAsync(kernel); - } - - [Fact] - public async Task ServiceAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunServiceTestAsync(kernel); - } - - ////////////////////////////// - // KERNEL PROMPT FUNCTION - - [Fact] - public async Task KernelBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelPromptFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new PromptFilter()); - await RunKernelTestAsync(kernel); - } - - [Fact] - public async Task KernelAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunKernelTestAsync(kernel); - } - - ////////////////////////////// - // AGENT - - [Fact] - public async Task AgentInvokeBasicTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentInvokeFunctionFilterTestAsync() - { - Kernel kernel = this.CreateKernelWithChatCompletion(); - kernel.FunctionInvocationFilters.Add(new FunctionFilter()); - await RunAgentTestAsync(kernel); - } - - [Fact] - public async Task AgentInvokeAutoFilterTestAsync() - { - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); - - builder.Services.AddSingleton(new AutoInvocationFilter()); - - Kernel kernel = builder.Build(); - - await RunAgentTestAsync(kernel); - } - - ////////////////////////////// - // KERNEL TEST - private async Task RunKernelTestAsync(Kernel kernel) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - async Task InvokeWithInputAsync(string input) - { - Console.WriteLine($"[TextContent] {AuthorRole.User}: '{input}'"); - - KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(input, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }); - - await foreach (StreamingChatMessageContent content in kernel.InvokeStreamingAsync(promptFunction)) - { - this.WriteContent(content); - } - } - } - - ////////////////////////////// - // CHAT COMPLETION SERVICE TEST - private async Task RunServiceTestAsync(Kernel kernel) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - IChatCompletionService service = kernel.GetRequiredService(); - - ChatHistory chat = []; - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - this.WriteContent(userContent); - - await foreach (StreamingChatMessageContent content in service.GetStreamingChatMessageContentsAsync(chat, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) - { - this.WriteContent(content); - } - } - } - - ////////////////////////////// - // AGENT TEST - private async Task RunAgentTestAsync(Kernel kernel) - { - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - ChatCompletionAgent agent = - new() - { - Instructions = "Answer questions about the menu.", - Kernel = kernel, - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, - }; - - ChatHistory chat = []; - - foreach (string input in s_userInput) - { - await InvokeWithInputAsync(input); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeWithInputAsync(string input) - { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - this.WriteContent(userContent); - - await foreach (StreamingChatMessageContent content in agent.InvokeStreamingAsync(chat)) - { - //if (content.Role != AuthorRole.Tool) // %%% BIG PROBLEM - //{ - // chat.Add(content); // %%% AWKWARD (BUILDING HISTORY) - //} - - this.WriteContent(content); - } - } - } - - ////////////////////////////// - // UTILITY - private void WriteContent(ChatMessageContent content) - { - Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); - } - - private void WriteContent(StreamingChatMessageContent content) - { - Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); - } - - ////////////////////////////// - // PLUGIN - 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"; - } - } - - ////////////////////////////// - // FUNCTION FILTER - private sealed class FunctionFilter : IFunctionInvocationFilter - { - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - if (context.Function.PluginName == nameof(MenuPlugin)) - { - context.Result = new FunctionResult(context.Function, "Menu not available."); - return Task.CompletedTask; - } - - return next(context); - } - } - - ////////////////////////////// - // PROMPT FILTER - private sealed class PromptFilter : IFunctionInvocationFilter - { - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - if (context.Function.PluginName != nameof(MenuPlugin)) - { - StreamingChatMessageContent[] contents = [new StreamingChatMessageContent(AuthorRole.Assistant, "Intercepted message.")]; - IAsyncEnumerable contentsAsync = contents.ToAsyncEnumerable(); - context.Result = new FunctionResult(context.Function, contentsAsync); - return Task.CompletedTask; - } - - return next(context); - } - } - - ////////////////////////////// - // AUTO INVOCATION FILTER - private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter - { - public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) - { - FunctionCallContent[] functionCalls = FunctionCallContent.GetFunctionCalls(context.ChatHistory.Last()).ToArray(); - - await next(context); - - if (context.Function.PluginName == nameof(MenuPlugin)) - { - //context.Result = new FunctionResult(context.Function, "Menu not available."); - context.Terminate = terminate; - } - } - } -} diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 5f1e93892778..24767cb3ec49 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -57,7 +57,7 @@ public class ChatHistoryChannel : AgentChannel yield return (IsMessageVisible(yieldMessage), yieldMessage); } - // Function content not visibile, unless result is the final message. + // Function content not visible, unless result is the final message. bool IsMessageVisible(ChatMessageContent message) => (message.Items.Any(i => i is FunctionCallContent) || (message.Items.Any(i => i is FunctionResultContent) && messageQueue.Count > 0)); From c8b034a1f36f5933796601c4dcb8c48c4b891088 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 12:44:29 -0700 Subject: [PATCH 19/26] Fix logic fix ;) --- dotnet/src/Agents/Abstractions/AgentChat.cs | 2 +- dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index f14dc67369d4..f4654963444e 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -255,7 +255,7 @@ async Task GetOrCreateChannelAsync() if (this.History.Count > 0) { - // Sync channel with existing history (user and assistant messages only / no function content) + // Sync channel with existing history await channel.ReceiveAsync(this.History, cancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 24767cb3ec49..1ebb55ab8daf 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -59,8 +59,8 @@ public class ChatHistoryChannel : AgentChannel // Function content not visible, unless result is the final message. bool IsMessageVisible(ChatMessageContent message) => - (message.Items.Any(i => i is FunctionCallContent) || - (message.Items.Any(i => i is FunctionResultContent) && messageQueue.Count > 0)); + (!message.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent) || + messageQueue.Count > 0); } /// From 07fc67eea59aa1c278190c43cbe9ea6395eac5bf Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 12:54:03 -0700 Subject: [PATCH 20/26] One more --- dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 1ebb55ab8daf..aae211232ad2 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -60,7 +60,7 @@ public class ChatHistoryChannel : AgentChannel // Function content not visible, unless result is the final message. bool IsMessageVisible(ChatMessageContent message) => (!message.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent) || - messageQueue.Count > 0); + messageQueue.Count == 0); } /// From 1fb4ad96d7401f97a2187b320d0a9b9cd6ae41f5 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 15 Jul 2024 15:28:28 -0700 Subject: [PATCH 21/26] Add integration test --- .../Agents/ChatCompletionAgentTests.cs | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs diff --git a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs new file mode 100644 index 000000000000..91796c1970b0 --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; +using Xunit.Abstractions; + +namespace SemanticKernel.IntegrationTests.Agents.OpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +public sealed class ChatCompletionAgentTests(ITestOutputHelper output) : IDisposable +{ + private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + /// + /// Integration test for using function calling + /// and targeting Azure OpenAI services. + /// + [Theory] + [InlineData("What is the special soup?", "Clam Chowder", false)] + [InlineData("What is the special soup?", "Clam Chowder", true)] + public async Task AzureChatCompletionAgentAsync(string input, string expectedAnswerContains, bool useAutoFunctionTermination) + { + // Arrange + AzureOpenAIConfiguration configuration = this._configuration.GetSection("AzureOpenAI").Get()!; + + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + + this._kernelBuilder.Services.AddSingleton(this._logger); + + this._kernelBuilder.AddAzureOpenAIChatCompletion( + configuration.ChatDeploymentName!, + configuration.Endpoint, + configuration.ApiKey); + + if (useAutoFunctionTermination) + { + this._kernelBuilder.Services.AddSingleton(new AutoInvocationFilter()); + } + + this._kernelBuilder.Plugins.Add(plugin); + + Kernel kernel = this._kernelBuilder.Build(); + + ChatCompletionAgent agent = + new() + { + Kernel = kernel, + Instructions = "Answer questions about the menu.", + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + }; + + AgentGroupChat chat = new(); + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + + // Act + ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + ChatMessageContent[] history = await chat.GetChatMessagesAsync().ToArrayAsync(); + + // Assert + Assert.Single(messages); + + ChatMessageContent response = messages.Single(); + + if (useAutoFunctionTermination) + { + Assert.Equal(3, history.Length); + Assert.Single(response.Items.OfType()); + Assert.Single(response.Items.OfType()); + } + else + { + Assert.Equal(4, history.Length); + Assert.Single(response.Items); + Assert.Single(response.Items.OfType()); + } + + Assert.Contains(expectedAnswerContains, messages.Single().Content, StringComparison.OrdinalIgnoreCase); + } + + private readonly XunitLogger _logger = new(output); + private readonly RedirectOutput _testOutputHelper = new(output); + + public void Dispose() + { + this._logger.Dispose(); + this._testOutputHelper.Dispose(); + } + + 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"; + } + } + + private sealed class AutoInvocationFilter(bool terminate = true) : IAutoFunctionInvocationFilter + { + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + await next(context); + + if (context.Function.PluginName == nameof(MenuPlugin)) + { + context.Terminate = terminate; + } + } + } +} From 712f64eb0053e4a94ce76401d37b0f8e28a0e0c8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 16 Jul 2024 07:00:50 -0700 Subject: [PATCH 22/26] Loop execution protection --- dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index aae211232ad2..055e3453ce41 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -40,6 +40,8 @@ public class ChatHistoryChannel : AgentChannel messageQueue.Enqueue(mutatedMessage); } + messageCount = this._history.Count; + if (!mutatedHistory.Contains(responseMessage)) { this._history.Add(responseMessage); From 970ad3d69c64f2d95ef30b51708ebf79e6aadc03 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 16 Jul 2024 11:12:03 -0700 Subject: [PATCH 23/26] Respond to comments --- .../Concepts/Filtering/AutoFunctionInvocationFiltering.cs | 2 +- dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs b/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs index 6b7c8cf26b90..1e56b8f36878 100644 --- a/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs +++ b/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs @@ -138,7 +138,7 @@ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext co var result = context.Result; // Example: override function result value - context.Result = new FunctionResult(context.Function, "Result from auto function invocation filter"); + context.Result = new FunctionResult(context.Result, "Result from auto function invocation filter"); // Example: Terminate function invocation context.Terminate = true; diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index 055e3453ce41..b796affe9c0b 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -29,10 +29,13 @@ public class ChatHistoryChannel : AgentChannel int messageCount = this._history.Count; HashSet mutatedHistory = []; + // Utilize a queue as a "read-ahead" cache to evaluate message sequencing (i.e., which message is final). Queue messageQueue = []; + ChatMessageContent? yieldMessage = null; await foreach (ChatMessageContent responseMessage in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false)) { + // Capture all messages that have been included in the mutated the history. for (int messageIndex = messageCount; messageIndex < this._history.Count; messageIndex++) { ChatMessageContent mutatedMessage = this._history[messageIndex]; @@ -40,18 +43,22 @@ public class ChatHistoryChannel : AgentChannel messageQueue.Enqueue(mutatedMessage); } + // Update the message count pointer to reflect the current history. messageCount = this._history.Count; + // Avoid duplicating any message included in the mutated history and also returned by the enumeration result. if (!mutatedHistory.Contains(responseMessage)) { this._history.Add(responseMessage); messageQueue.Enqueue(responseMessage); } + // Dequeue the next message to yield. yieldMessage = messageQueue.Dequeue(); yield return (IsMessageVisible(yieldMessage), yieldMessage); } + // Dequeue any remaining messages to yield. while (messageQueue.Count > 0) { yieldMessage = messageQueue.Dequeue(); From d72e085f246005b2832f967c12e1ccf577921355 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 16 Jul 2024 11:13:30 -0700 Subject: [PATCH 24/26] Another comment --- dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs index b796affe9c0b..5dcb6b9b0204 100644 --- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs @@ -26,6 +26,7 @@ public class ChatHistoryChannel : AgentChannel throw new KernelException($"Invalid channel binding for agent: {agent.Id} ({agent.GetType().FullName})"); } + // Capture the current message count to evaluate history mutation. int messageCount = this._history.Count; HashSet mutatedHistory = []; From 1f2a328b8d9d457f318fc893b15f3d09ab8e9fd3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 16 Jul 2024 11:34:05 -0700 Subject: [PATCH 25/26] Added sample --- .../ChatCompletion_FunctionTermination.cs | 157 ++++++++++++++++++ .../Agents/OpenAI/AssistantThreadActions.cs | 14 +- 2 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs new file mode 100644 index 000000000000..8f071c6dbda8 --- /dev/null +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs @@ -0,0 +1,157 @@ +// 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; + +/// +/// Demonstrate usage of for both direction invocation +/// of and via . +/// +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(); + 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)) + { + 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(); + 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 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; + } + } + } +} diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs index eeab85c00df4..f768d89a54bb 100644 --- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs @@ -20,12 +20,6 @@ internal static class AssistantThreadActions { private const string FunctionDelimiter = "-"; - private static readonly HashSet s_messageRoles = - [ - AuthorRole.User, - AuthorRole.Assistant, - ]; - private static readonly HashSet s_pollingStatuses = [ RunStatus.Queued, @@ -50,12 +44,8 @@ internal static class AssistantThreadActions /// if a system message is present, without taking any other action public static async Task CreateMessageAsync(AssistantsClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) { - if (!s_messageRoles.Contains(message.Role)) - { - throw new KernelException($"Invalid message role: {message.Role}"); - } - - if (string.IsNullOrWhiteSpace(message.Content)) + if (string.IsNullOrEmpty(message.Content) || + message.Items.Any(i => i is FunctionCallContent)) { return; } From 2118b9ec6eacf4062cefcba4052f087a6a9f772e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 16 Jul 2024 11:35:42 -0700 Subject: [PATCH 26/26] Comment --- .../Concepts/Agents/ChatCompletion_FunctionTermination.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs index 8f071c6dbda8..f344dae432b9 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs @@ -55,6 +55,7 @@ async Task InvokeAgentAsync(string input) 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);