Skip to content

.Net: Add an experimental AgentKernelFunctionFactory #11136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Demonstrate creation of <see cref="ChatCompletionAgent"/> and
/// eliciting its response to three explicit user messages.
/// </summary>
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<OrderPlugin>();
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<RefundPlugin>();
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<OrderPlugin>();
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<RefundPlugin>();
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<AutoFunctionInvocationContext, Task> next)
{
output.WriteLine($"Invoke: {context.Function.Name}");

await next(context);
}
}
87 changes: 87 additions & 0 deletions dotnet/src/Agents/Core/Functions/AgentKernelFunctionFactory.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides factory methods for creating implementations of <see cref="KernelFunction"/> backed by an <see cref="Agent" />.
/// </summary>
[Experimental("SKEXP0110")]
public static class AgentKernelFunctionFactory
{
/// <summary>
/// Creates a <see cref="KernelFunction"/> that will invoke the provided Agent.
/// </summary>
/// <param name="agent">The <see cref="Agent" /> to be represented via the created <see cref="KernelFunction"/>.</param>
/// <param name="functionName">The name to use for the function. If null, it will default to the agent name.</param>
/// <param name="description">The description to use for the function. If null, it will default to agent description.</param>
/// <param name="parameters">Optional parameter descriptions. If null, it will default to query and additional instructions parameters.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
/// <returns>The created <see cref="KernelFunction"/> for invoking the <see cref="Agent"/>.</returns>
[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<KernelParameterMetadata>? parameters = null,
ILoggerFactory? loggerFactory = null)
{
Verify.NotNull(agent);

async Task<FunctionResult> 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<KernelParameterMetadata> 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<KernelParameterMetadata>? s_kernelParameterMetadata;
#endregion
}
Loading
Loading