diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs b/dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs new file mode 100644 index 000000000000..e4775f4555d1 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Step08_AgentAsKernelFunction.cs @@ -0,0 +1,165 @@ +// 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_AgentAsKernelFunction(ITestOutputHelper output) : BaseAgentsTest(output) +{ + protected override bool ForceOpenAI { get; } = true; + + [Fact] + public async Task SalesAssistantAgentAsync() + { + 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() + { + 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() + { + 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); + } + } + } + + #region private + 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() }), + }; + } + #endregion +} + +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 sealed 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/Core/Functions/AgentKernelFunctionFactory.cs b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs new file mode 100644 index 000000000000..5c45d2a8d41f --- /dev/null +++ b/dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs @@ -0,0 +1,87 @@ +// 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 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. + /// The description to use for the function. If null, it will default to agent description. + /// 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 . + [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, + ILoggerFactory? loggerFactory = null) + { + Verify.NotNull(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(FunctionResult) }, + }; + + 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/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs new file mode 100644 index 000000000000..7bbefbb8652e --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Core/Functions/AgentKernelFunctionFactoryTests.cs @@ -0,0 +1,209 @@ +// 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 sealed 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 sealed 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; + } + } +} diff --git a/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs b/dotnet/src/InternalUtilities/connectors/AI/FunctionCalling/FunctionCallsProcessor.cs index a9f3a79874ef..97cb426c307d 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 ChatMessageContent + if (functionResult is IEnumerable chatMessageContents) + { + return string.Join(",", chatMessageContents.Select(c => c.ToString())); + } + return JsonSerializer.Serialize(functionResult, s_functionResultSerializerOptions); }