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)