From 03fd50b37ee0fa99b4c3a94f13ab0285ef53c56b Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:00:29 +0000 Subject: [PATCH 01/13] Add an experimental AgentKernelFunctionFactory --- .../Step08_AgentHandOff.cs | 164 ++++++++++++++++++ .../Agents/Abstractions/AgentInvokeOptions.cs | 2 +- .../Functions/AgentKernelFunctionFactory.cs | 88 ++++++++++ .../FunctionCalling/FunctionCallsProcessor.cs | 6 + 4 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs create mode 100644 dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs b/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs new file mode 100644 index 000000000000..50420b2fddd9 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted; + +/// +/// Demonstrate creation of and +/// eliciting its response to three explicit user messages. +/// +public class Step08_AgentHandOff(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task SalesAssistantAgentAsync() + { + this.ForceOpenAI = true; + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.Plugins.AddFromType(); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output)); + + // Define the agent + ChatCompletionAgent agent = + new() + { + Name = "SalesAssistant", + Instructions = "You are a sales assistant. Place orders for items the user requests.", + Kernel = kernel, + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + + // Invoke the agent and display the responses + var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, "Place an order for a black boot.")); + await foreach (ChatMessageContent responseItem in responseItems) + { + this.WriteAgentChatMessage(responseItem); + } + } + + [Fact] + public async Task RefundAgentAsync() + { + this.ForceOpenAI = true; + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.Plugins.AddFromType(); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output)); + + // Define the agent + ChatCompletionAgent agent = + new() + { + Name = "RefundAgent", + Instructions = "You are a refund agent. Help the user with refunds.", + Kernel = kernel, + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + + // Invoke the agent and display the responses + var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, "I want a refund for a black boot.")); + await foreach (ChatMessageContent responseItem in responseItems) + { + this.WriteAgentChatMessage(responseItem); + } + } + + [Fact] + public async Task MultipleAgentsAsync() + { + this.ForceOpenAI = true; + Kernel kernel = this.CreateKernelWithChatCompletion(); + var agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin", + [ + AgentKernelFunctionFactory.CreateFromAgent(this.CreateSalesAssistant()), + AgentKernelFunctionFactory.CreateFromAgent(this.CreateRefundAgent()) + ]); + kernel.Plugins.Add(agentPlugin); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output)); + + // Define the agent + ChatCompletionAgent agent = + new() + { + Name = "ShoppingAssistant", + Instructions = "You are a sales assistant. Delegate to the provided agents to help the user with placing orders and requesting refunds.", + Kernel = kernel, + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + + // Invoke the agent and display the responses + string[] messages = + [ + "Place an order for a black boot.", + "Now I want a refund for the black boot." + ]; + + AgentThread? agentThread = null; + foreach (var message in messages) + { + var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, message), agentThread); + await foreach (var responseItem in responseItems) + { + agentThread = responseItem.Thread; + this.WriteAgentChatMessage(responseItem.Message); + } + } + } + + private ChatCompletionAgent CreateSalesAssistant() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.Plugins.AddFromType(); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output)); + + // Define the agent + return new() + { + Name = "SalesAssistant", + Instructions = "You are a sales assistant. Place orders for items the user requests.", + Description = "Agent to invoke to place orders for items the user requests.", + Kernel = kernel, + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + } + + private ChatCompletionAgent CreateRefundAgent() + { + Kernel kernel = this.CreateKernelWithChatCompletion(); + kernel.Plugins.AddFromType(); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output)); + + // Define the agent + return new() + { + Name = "RefundAgent", + Instructions = "You are a refund agent. Help the user with refunds.", + Description = "Agent to invoke to execute a refund an item on behalf of the user.", + Kernel = kernel, + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + } +} + +public sealed class OrderPlugin +{ + [KernelFunction, Description("Place an order for the specified item.")] + public string PlaceOrder([Description("The name of the item to be ordered.")] string itemName) => "success"; +} + +public sealed class RefundPlugin +{ + [KernelFunction, Description("Execute a refund for the specified item.")] + public string ExecuteRefund(string itemName) => "success"; +} + +public class AutoFunctionInvocationFilter(ITestOutputHelper output) : IAutoFunctionInvocationFilter +{ + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + output.WriteLine($"Invoke: {context.Function.Name}"); + + await next(context); + } +} diff --git a/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs b/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs index 57d27652ab02..62eaf386d15a 100644 --- a/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs +++ b/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs @@ -21,5 +21,5 @@ public sealed class AgentInvokeOptions /// Gets or sets any instructions, in addition to those that were provided to the agent /// initially, that need to be added to the prompt for this invocation only. /// - public string AdditionalInstructions { get; init; } = string.Empty; + public string AdditionalInstructions { get; set; } = string.Empty; } diff --git a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs new file mode 100644 index 000000000000..e2c2f69d24f7 --- /dev/null +++ b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Extensions; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents; + +/// +/// Provides factory methods for creating implementations of backed by an . +/// +[Experimental("SKEXP0110")] +public static class AgentKernelFunctionFactory +{ + /// + /// Creates a instance for a method, specified via a delegate. + /// + /// The to be represented via the created . + /// The name to use for the function. If null, it will default to the agent name. + /// The description to use for the function. If null, it will default to agent description. + /// Optional parameter descriptions. If null, it will default to one derived from the method represented by . + /// The to use for logging. If null, no logging will be performed. + /// The created for invoking . + [RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")] + [RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")] + public static KernelFunction CreateFromAgent( + Agent agent, + string? functionName = null, + string? description = null, + IEnumerable? parameters = null, + KernelReturnParameterMetadata? returnParameter = null, + ILoggerFactory? loggerFactory = null) + { + Verify.NotNull(agent, nameof(agent)); + + async Task InvokeAgentAsync(Kernel kernel, KernelFunction function, KernelArguments arguments, CancellationToken cancellationToken) + { + arguments.TryGetValue("query", out var query); + var queryString = query?.ToString() ?? string.Empty; + + AgentInvokeOptions? options = null; + + if (arguments.TryGetValue("instructions", out var instructions) && instructions is not null) + { + options = new() + { + AdditionalInstructions = instructions?.ToString() ?? string.Empty + }; + } + + var response = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, queryString), null, options, cancellationToken); + var responseItems = await response.ToArrayAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + var chatMessages = responseItems.Select(i => i.Message).ToArray(); + return new FunctionResult(function, chatMessages, kernel.Culture); + } + + KernelFunctionFromMethodOptions options = new() + { + FunctionName = functionName ?? agent.GetName(), + Description = description ?? agent.Description, + Parameters = parameters ?? GetDefaultKernelParameterMetadata(), + ReturnParameter = new() { ParameterType = typeof(IAsyncEnumerable>) }, + }; + + return KernelFunctionFactory.CreateFromMethod( + InvokeAgentAsync, + options); + } + + #region private + [RequiresUnreferencedCode("Uses reflection for generating JSON schema for method parameters and return type, making it incompatible with AOT scenarios.")] + [RequiresDynamicCode("Uses reflection for generating JSON schema for method parameters and return type, making it incompatible with AOT scenarios.")] + private static IEnumerable GetDefaultKernelParameterMetadata() + { + return s_kernelParameterMetadata ??= [ + new KernelParameterMetadata("query") { Description = "Available information that will guide in performing this operation.", ParameterType = typeof(string), IsRequired = true }, + //new KernelParameterMetadata("instructions") { Description = "Additional instructions for the agent.", ParameterType = typeof(string), IsRequired = true }, + ]; + } + + private static IEnumerable? s_kernelParameterMetadata; + #endregion +} diff --git a/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs b/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs index a9f3a79874ef..1bd3c95b0876 100644 --- a/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs +++ b/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs @@ -494,6 +494,12 @@ public static string ProcessFunctionResult(object functionResult) return chatMessageContent.ToString(); } + // Same optimization but for a enumerable of CjatMessageContent + if (functionResult is IEnumerable chatMessageContents) + { + return string.Join(",", chatMessageContents.Select(c => c.ToString())); + } + return JsonSerializer.Serialize(functionResult, s_functionResultSerializerOptions); } From 298aabc234acb49dcf4c60f33ea2a19a116c4ff6 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:08:26 +0000 Subject: [PATCH 02/13] Clean-up --- .../Step08_AgentHandOff.cs | 14 +++++++++----- .../Core/Functions/AgentKernelFunctionFactory.cs | 2 +- .../samples/InternalUtilities/BaseTest.cs | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs b/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs index 50420b2fddd9..c3f1545d2ec4 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs @@ -10,12 +10,16 @@ namespace GettingStarted; /// Demonstrate creation of and /// eliciting its response to three explicit user messages. /// -public class Step08_AgentHandOff(ITestOutputHelper output) : BaseAgentsTest(output) +public class Step08_AgentHandOff : BaseAgentsTest { + public Step08_AgentHandOff(ITestOutputHelper output) : base(output) + { + this.ForceOpenAI = true; + } + [Fact] public async Task SalesAssistantAgentAsync() { - this.ForceOpenAI = true; Kernel kernel = this.CreateKernelWithChatCompletion(); kernel.Plugins.AddFromType(); kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output)); @@ -41,7 +45,6 @@ public async Task SalesAssistantAgentAsync() [Fact] public async Task RefundAgentAsync() { - this.ForceOpenAI = true; Kernel kernel = this.CreateKernelWithChatCompletion(); kernel.Plugins.AddFromType(); kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(this.Output)); @@ -67,7 +70,6 @@ public async Task RefundAgentAsync() [Fact] public async Task MultipleAgentsAsync() { - this.ForceOpenAI = true; Kernel kernel = this.CreateKernelWithChatCompletion(); var agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin", [ @@ -106,6 +108,7 @@ public async Task MultipleAgentsAsync() } } + #region private private ChatCompletionAgent CreateSalesAssistant() { Kernel kernel = this.CreateKernelWithChatCompletion(); @@ -139,6 +142,7 @@ private ChatCompletionAgent CreateRefundAgent() Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), }; } + #endregion } public sealed class OrderPlugin @@ -153,7 +157,7 @@ public sealed class RefundPlugin public string ExecuteRefund(string itemName) => "success"; } -public class AutoFunctionInvocationFilter(ITestOutputHelper output) : IAutoFunctionInvocationFilter +public sealed class AutoFunctionInvocationFilter(ITestOutputHelper output) : IAutoFunctionInvocationFilter { public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) { diff --git a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs index e2c2f69d24f7..e78d37251447 100644 --- a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs +++ b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs @@ -79,7 +79,7 @@ private static IEnumerable GetDefaultKernelParameterMet { return s_kernelParameterMetadata ??= [ new KernelParameterMetadata("query") { Description = "Available information that will guide in performing this operation.", ParameterType = typeof(string), IsRequired = true }, - //new KernelParameterMetadata("instructions") { Description = "Additional instructions for the agent.", ParameterType = typeof(string), IsRequired = true }, + new KernelParameterMetadata("instructions") { Description = "Additional instructions for the agent.", ParameterType = typeof(string), IsRequired = true }, ]; } diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index 78816c97e2e2..82bdf19252e9 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -15,7 +15,7 @@ public abstract class BaseTest : TextWriter /// and are defined. /// If 'false', Azure takes precedence. /// - protected virtual bool ForceOpenAI { get; } = false; + protected virtual bool ForceOpenAI { get; set; } = false; protected ITestOutputHelper Output { get; } From f8cf99649cb01cc6e54715cac3fb9e08e4fc8df9 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:13:32 +0000 Subject: [PATCH 03/13] Clean-up --- .../samples/GettingStartedWithAgents/Step08_AgentHandOff.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs b/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs index c3f1545d2ec4..45375d33c346 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs @@ -92,8 +92,8 @@ public async Task MultipleAgentsAsync() // Invoke the agent and display the responses string[] messages = [ - "Place an order for a black boot.", - "Now I want a refund for the black boot." + "Place an order for a black boot.", + "Now I want a refund for the black boot." ]; AgentThread? agentThread = null; From a05fcafb8de1c5eb1d64720d95a3715a9cec2575 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:16:14 +0000 Subject: [PATCH 04/13] Clean-up --- dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs b/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs index 62eaf386d15a..57d27652ab02 100644 --- a/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs +++ b/dotnet/src/Agents/Abstractions/AgentInvokeOptions.cs @@ -21,5 +21,5 @@ public sealed class AgentInvokeOptions /// Gets or sets any instructions, in addition to those that were provided to the agent /// initially, that need to be added to the prompt for this invocation only. /// - public string AdditionalInstructions { get; set; } = string.Empty; + public string AdditionalInstructions { get; init; } = string.Empty; } From 7bbcc0a178ca529a38942f69bb69c6abf4940575 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 07:13:16 +0000 Subject: [PATCH 05/13] Update dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> --- dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs index e78d37251447..93d580f17de7 100644 --- a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs +++ b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs @@ -18,7 +18,7 @@ namespace Microsoft.SemanticKernel.Agents; public static class AgentKernelFunctionFactory { /// - /// Creates a instance for a method, specified via a delegate. + /// Creates a that will invoke the provided Agent. /// /// The to be represented via the created . /// The name to use for the function. If null, it will default to the agent name. From d8e11e31a99b17dfac2cdb71acb41fd7e63bbd5c Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 07:13:26 +0000 Subject: [PATCH 06/13] Update dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> --- dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs index 93d580f17de7..769d88ebffe9 100644 --- a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs +++ b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs @@ -36,7 +36,7 @@ public static KernelFunction CreateFromAgent( KernelReturnParameterMetadata? returnParameter = null, ILoggerFactory? loggerFactory = null) { - Verify.NotNull(agent, nameof(agent)); + Verify.NotNull(agent); async Task InvokeAgentAsync(Kernel kernel, KernelFunction function, KernelArguments arguments, CancellationToken cancellationToken) { From baa8922ca41ae42f9838cf630ae5ff3596f39612 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 07:13:47 +0000 Subject: [PATCH 07/13] Update dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> --- .../connectors/AI/FunctionCalling/FunctionCallsProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs b/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs index 1bd3c95b0876..97cb426c307d 100644 --- a/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs +++ b/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs @@ -494,7 +494,7 @@ public static string ProcessFunctionResult(object functionResult) return chatMessageContent.ToString(); } - // Same optimization but for a enumerable of CjatMessageContent + // Same optimization but for a enumerable of ChatMessageContent if (functionResult is IEnumerable chatMessageContents) { return string.Join(",", chatMessageContents.Select(c => c.ToString())); From 46582afdb10cd86c2ad41d29960b1beb439ac602 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 09:41:59 +0000 Subject: [PATCH 08/13] Address some code review feedback --- ...p08_AgentHandOff.cs => Step08_AgentAsKernelFunction.cs} | 7 ++----- .../samples/InternalUtilities/BaseTest.cs | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) rename dotnet/samples/GettingStartedWithAgents/{Step08_AgentHandOff.cs => Step08_AgentAsKernelFunction.cs} (97%) diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs b/dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs similarity index 97% rename from dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs rename to dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs index 45375d33c346..e4775f4555d1 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step08_AgentHandOff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs @@ -10,12 +10,9 @@ namespace GettingStarted; /// Demonstrate creation of and /// eliciting its response to three explicit user messages. /// -public class Step08_AgentHandOff : BaseAgentsTest +public class Step08_AgentAsKernelFunction(ITestOutputHelper output) : BaseAgentsTest(output) { - public Step08_AgentHandOff(ITestOutputHelper output) : base(output) - { - this.ForceOpenAI = true; - } + protected override bool ForceOpenAI { get; } = true; [Fact] public async Task SalesAssistantAgentAsync() diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index 82bdf19252e9..78816c97e2e2 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -15,7 +15,7 @@ public abstract class BaseTest : TextWriter /// and are defined. /// If 'false', Azure takes precedence. /// - protected virtual bool ForceOpenAI { get; set; } = false; + protected virtual bool ForceOpenAI { get; } = false; protected ITestOutputHelper Output { get; } From 8f8dfa2d924d99420f2b22f296a77587eb7594f6 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 09:58:33 +0000 Subject: [PATCH 09/13] Fix typo --- dotnet/src/Agents/Bedrock/BedrockAgentThread.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Bedrock/BedrockAgentThread.cs b/dotnet/src/Agents/Bedrock/BedrockAgentThread.cs index f94fe18ce6aa..ed890d37d5b0 100644 --- a/dotnet/src/Agents/Bedrock/BedrockAgentThread.cs +++ b/dotnet/src/Agents/Bedrock/BedrockAgentThread.cs @@ -22,7 +22,7 @@ public sealed class BedrockAgentThread : AgentThread /// Initializes a new instance of the class. /// /// A client used to interact with the Bedrock Agent runtime service. - /// An optional session Id to continue an exsting session. + /// An optional session Id to continue an existing session. /// public BedrockAgentThread(AmazonBedrockAgentRuntimeClient runtimeClient, string? sessionId = null) { From 67eb3745e4d7982f0f2c267f1fdd61acee8de237 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 10:11:59 +0000 Subject: [PATCH 10/13] Fix API comments --- .../Agents/Core/Functions/AgentKernelFunctionFactory.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs index 769d88ebffe9..5c45d2a8d41f 100644 --- a/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs +++ b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs @@ -20,12 +20,12 @@ public static class AgentKernelFunctionFactory /// /// Creates a that will invoke the provided Agent. /// - /// The to be represented via the created . + /// The to be represented via the created . /// The name to use for the function. If null, it will default to the agent name. /// The description to use for the function. If null, it will default to agent description. - /// Optional parameter descriptions. If null, it will default to one derived from the method represented by . + /// Optional parameter descriptions. If null, it will default to query and additional instructions parameters. /// The to use for logging. If null, no logging will be performed. - /// The created for invoking . + /// The created for invoking the . [RequiresUnreferencedCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")] [RequiresDynamicCode("Uses reflection to handle various aspects of the function creation and invocation, making it incompatible with AOT scenarios.")] public static KernelFunction CreateFromAgent( @@ -33,7 +33,6 @@ public static KernelFunction CreateFromAgent( string? functionName = null, string? description = null, IEnumerable? parameters = null, - KernelReturnParameterMetadata? returnParameter = null, ILoggerFactory? loggerFactory = null) { Verify.NotNull(agent); @@ -64,7 +63,7 @@ async Task InvokeAgentAsync(Kernel kernel, KernelFunction functi FunctionName = functionName ?? agent.GetName(), Description = description ?? agent.Description, Parameters = parameters ?? GetDefaultKernelParameterMetadata(), - ReturnParameter = new() { ParameterType = typeof(IAsyncEnumerable>) }, + ReturnParameter = new() { ParameterType = typeof(FunctionResult) }, }; return KernelFunctionFactory.CreateFromMethod( From 3f086a8890af3bc073e088391aa5232c6ad60352 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:17:54 +0000 Subject: [PATCH 11/13] Add unit tests --- .../AgentKernelFunctionFactoryTests.cs | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs diff --git a/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs new file mode 100644 index 000000000000..d4ac9d0c26f2 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs @@ -0,0 +1,210 @@ +// 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; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Core.Functions; + +/// +/// Unit testing of . +/// +public class AgentKernelFunctionFactoryTests +{ + /// + /// Verify calling AgentKernelFunctionFactory.CreateFromAgent. + /// + [Fact] + public void VerifyCreateFromAgent() + { + // Arrange + var agent = new MockAgent() + { + Name = "MyAgent", + Description = "Description for MyAgent" + }; + + // Act + var function = AgentKernelFunctionFactory.CreateFromAgent(agent); + + // Assert + Assert.NotNull(function); + Assert.Equal(agent.Name, function.Name); + Assert.Equal(agent.Description, function.Description); + } + + /// + /// Verify calling AgentKernelFunctionFactory.CreateFromAgent with overrides. + /// + [Fact] + public void VerifyCreateFromAgentWithOverrides() + { + // Arrange + var agent = new MockAgent() + { + Name = "MyAgent", + Description = "Description for MyAgent" + }; + + // Act + var function = AgentKernelFunctionFactory.CreateFromAgent( + agent, + "MyAgentFunction", + "Description for MyAgentFunction" + ); + + // Assert + Assert.NotNull(function); + Assert.Equal("MyAgentFunction", function.Name); + Assert.Equal("Description for MyAgentFunction", function.Description); + } + + /// + /// Verify invoking function returned by AgentKernelFunctionFactory.CreateFromAgent. + /// + [Fact] + public async Task VerifyInvokeAgentAsKernelFunctionAsync() + { + // Arrange + var agent = new MockAgent() + { + Name = "MyAgent", + Description = "Description for MyAgent" + }; + var function = AgentKernelFunctionFactory.CreateFromAgent(agent); + + // Act + var arguments = new KernelArguments + { + { "query", "Mock query" } + }; + var result = await function.InvokeAsync(new(), arguments); + + // Assert + Assert.NotNull(result); + var items = result.GetValue>(); + Assert.NotNull(items); + Assert.NotEmpty(items); + Assert.Equal("Response to: 'Mock query' with instructions: ''", items.First().ToString()); + } + + /// + /// Verify invoking function returned by AgentKernelFunctionFactory.CreateFromAgent. + /// + [Fact] + public async Task VerifyInvokeAgentAsKernelFunctionWithNoQueryAsync() + { + // Arrange + var agent = new MockAgent() + { + Name = "MyAgent", + Description = "Description for MyAgent" + }; + var function = AgentKernelFunctionFactory.CreateFromAgent(agent); + + // Act + var result = await function.InvokeAsync(new()); + + // Assert + Assert.NotNull(result); + var items = result.GetValue>(); + Assert.NotNull(items); + Assert.NotEmpty(items); + Assert.Equal("Response to: '' with instructions: ''", items.First().ToString()); + } + + /// + /// Verify invoking function returned by AgentKernelFunctionFactory.CreateFromAgent. + /// + [Fact] + public async Task VerifyInvokeAgentAsKernelFunctionWithInstructionsAsync() + { + // Arrange + var agent = new MockAgent() + { + Name = "MyAgent", + Description = "Description for MyAgent" + }; + var function = AgentKernelFunctionFactory.CreateFromAgent(agent); + + // Act + var arguments = new KernelArguments + { + { "query", "Mock query" }, + { "instructions", "Mock instructions" } + }; + var result = await function.InvokeAsync(new(), arguments); + + // Assert + Assert.NotNull(result); + var items = result.GetValue>(); + Assert.NotNull(items); + Assert.NotEmpty(items); + Assert.Equal("Response to: 'Mock query' with instructions: 'Mock instructions'", items.First().ToString()); + } + + + /// + /// Mock implementation of . + /// + private class MockAgent : Agent + { + public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var agentThread = thread ?? new MockAgentThread(); + foreach (var message in messages) + { + await Task.Delay(100, cancellationToken); + yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, $"Response to: '{message.Content}' with instructions: '{options?.AdditionalInstructions}'"), agentThread); + } + } + + public override IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected internal override IEnumerable GetChannelKeys() + { + throw new NotImplementedException(); + } + + protected internal override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + /// + /// Mock implementation of + /// + private class MockAgentThread : AgentThread + { + protected override Task CreateInternalAsync(CancellationToken cancellationToken) + { + return Task.FromResult("mock_thread_id"); + } + + protected override Task DeleteInternalAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + protected override Task OnNewMessageInternalAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + } +} From 62d4bf85a1d284cbdedf8ec56eea26ad1f3325b1 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:23:30 +0000 Subject: [PATCH 12/13] Fix format issues --- .../Core/Functions/AgentKernelFunctionFactoryTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs index d4ac9d0c26f2..52e57706feee 100644 --- a/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs @@ -150,11 +150,10 @@ public async Task VerifyInvokeAgentAsKernelFunctionWithInstructionsAsync() Assert.Equal("Response to: 'Mock query' with instructions: 'Mock instructions'", items.First().ToString()); } - /// /// Mock implementation of . /// - private class MockAgent : Agent + private sealed class MockAgent : Agent { public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -190,7 +189,7 @@ protected internal override Task RestoreChannelAsync(string channe /// /// Mock implementation of /// - private class MockAgentThread : AgentThread + private sealed class MockAgentThread : AgentThread { protected override Task CreateInternalAsync(CancellationToken cancellationToken) { From a39f4010f9e523c7f8989c11f883b5a3dd4fe680 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:34:52 +0000 Subject: [PATCH 13/13] Update latest from the feature branch --- .../Core/Functions/AgentKernelFunctionFactoryTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs index 52e57706feee..7bbefbb8652e 100644 --- a/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs @@ -191,9 +191,9 @@ protected internal override Task RestoreChannelAsync(string channe /// private sealed class MockAgentThread : AgentThread { - protected override Task CreateInternalAsync(CancellationToken cancellationToken) + protected override Task CreateInternalAsync(CancellationToken cancellationToken) { - return Task.FromResult("mock_thread_id"); + return Task.FromResult("mock_thread_id"); } protected override Task DeleteInternalAsync(CancellationToken cancellationToken)