diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln
index 291cb484649..4f82713b5ad 100644
--- a/dotnet/AutoGen.sln
+++ b/dotnet/AutoGen.sln
@@ -68,6 +68,13 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{243E768F-EA7D-4AF1-B625-0398440BB1AB}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
+ .gitattributes = .gitattributes
+ .gitignore = .gitignore
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ Directory.Packages.props = Directory.Packages.props
+ global.json = global.json
+ NuGet.config = NuGet.config
spelling.dic = spelling.dic
EndProjectSection
EndProject
@@ -123,7 +130,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloAgent", "samples\Hello
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AIModelClientHostingExtensions", "src\Microsoft.AutoGen\Extensions\AIModelClientHostingExtensions\AIModelClientHostingExtensions.csproj", "{97550E87-48C6-4EBF-85E1-413ABAE9DBFD}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Agents.Tests", "Microsoft.AutoGen.Agents.Tests\Microsoft.AutoGen.Agents.Tests.csproj", "{CF4C92BD-28AE-4B8F-B173-601004AEC9BF}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AutoGen.Agents.Tests", "Microsoft.AutoGen.Agents.Tests\Microsoft.AutoGen.Agents.Tests.csproj", "{CF4C92BD-28AE-4B8F-B173-601004AEC9BF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{686480D7-8FEC-4ED3-9C5D-CEBE1057A7ED}"
EndProject
diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props
index bb78c84d14f..ae30d2c48a5 100644
--- a/dotnet/Directory.Build.props
+++ b/dotnet/Directory.Build.props
@@ -32,10 +32,6 @@
$(NoWarn);CA1829
-
-
-
-
diff --git a/dotnet/samples/AutoGen.BasicSamples/AutoGen.BasicSample.csproj b/dotnet/samples/AutoGen.BasicSamples/AutoGen.BasicSample.csproj
index 460c95f3743..8d73ca96679 100644
--- a/dotnet/samples/AutoGen.BasicSamples/AutoGen.BasicSample.csproj
+++ b/dotnet/samples/AutoGen.BasicSamples/AutoGen.BasicSample.csproj
@@ -15,5 +15,7 @@
+
+
diff --git a/dotnet/samples/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs b/dotnet/samples/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs
index ca05a42ca36..f09b5645dc2 100644
--- a/dotnet/samples/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs
+++ b/dotnet/samples/AutoGen.BasicSamples/Example03_Agent_FunctionCall.cs
@@ -6,6 +6,7 @@
using AutoGen.OpenAI;
using AutoGen.OpenAI.Extension;
using FluentAssertions;
+using Microsoft.Extensions.AI;
///
/// This example shows how to add type-safe function call to an agent.
@@ -37,13 +38,20 @@ public async Task ConcatString(string[] strings)
///
/// price, should be an integer
/// tax rate, should be in range (0, 1)
- [FunctionAttribute]
+ [Function]
public async Task CalculateTax(int price, float taxRate)
{
return $"tax is {price * taxRate}";
}
- public static async Task RunAsync()
+ ///
+ /// This example shows how to add type-safe function call using AutoGen.SourceGenerator.
+ /// The SourceGenerator will automatically generate FunctionDefinition and FunctionCallWrapper during compiling time.
+ ///
+ /// For adding type-safe function call from M.E.A.I tools, please refer to .
+ ///
+ ///
+ public static async Task ToolCallWithSourceGenerator()
{
var instance = new Example03_Agent_FunctionCall();
var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini();
@@ -101,4 +109,60 @@ public static async Task RunAsync()
// send aggregate message back to llm to get the final result
var finalResult = await agent.SendAsync(calculateTaxes);
}
+
+ ///
+ /// This example shows how to add type-safe function call from M.E.A.I tools.
+ ///
+ /// For adding type-safe function call from source generator, please refer to .
+ ///
+ public static async Task ToolCallWithMEAITools()
+ {
+ var gpt4o = LLMConfiguration.GetOpenAIGPT4o_mini();
+ var instance = new Example03_Agent_FunctionCall();
+
+ AIFunction[] tools = [
+ AIFunctionFactory.Create(instance.UpperCase),
+ AIFunctionFactory.Create(instance.ConcatString),
+ AIFunctionFactory.Create(instance.CalculateTax),
+ ];
+
+ var toolCallMiddleware = new FunctionCallMiddleware(tools);
+
+ var agent = new OpenAIChatAgent(
+ chatClient: gpt4o,
+ name: "agent",
+ systemMessage: "You are a helpful AI assistant")
+ .RegisterMessageConnector()
+ .RegisterStreamingMiddleware(toolCallMiddleware)
+ .RegisterPrintMessage();
+
+ // talk to the assistant agent
+ var upperCase = await agent.SendAsync("convert to upper case: hello world");
+ upperCase.GetContent()?.Should().Be("HELLO WORLD");
+ upperCase.Should().BeOfType();
+ upperCase.GetToolCalls().Should().HaveCount(1);
+ upperCase.GetToolCalls().First().FunctionName.Should().Be(nameof(UpperCase));
+
+ var concatString = await agent.SendAsync("concatenate strings: a, b, c, d, e");
+ concatString.GetContent()?.Should().Be("a b c d e");
+ concatString.Should().BeOfType();
+ concatString.GetToolCalls().Should().HaveCount(1);
+ concatString.GetToolCalls().First().FunctionName.Should().Be(nameof(ConcatString));
+
+ var calculateTax = await agent.SendAsync("calculate tax: 100, 0.1");
+ calculateTax.GetContent().Should().Be("tax is 10");
+ calculateTax.Should().BeOfType();
+ calculateTax.GetToolCalls().Should().HaveCount(1);
+ calculateTax.GetToolCalls().First().FunctionName.Should().Be(nameof(CalculateTax));
+
+ // parallel function calls
+ var calculateTaxes = await agent.SendAsync("calculate tax: 100, 0.1; calculate tax: 200, 0.2");
+ calculateTaxes.GetContent().Should().Be("tax is 10\ntax is 40"); // "tax is 10\n tax is 40
+ calculateTaxes.Should().BeOfType();
+ calculateTaxes.GetToolCalls().Should().HaveCount(2);
+ calculateTaxes.GetToolCalls().First().FunctionName.Should().Be(nameof(CalculateTax));
+
+ // send aggregate message back to llm to get the final result
+ var finalResult = await agent.SendAsync(calculateTaxes);
+ }
}
diff --git a/dotnet/samples/AutoGen.BasicSamples/Program.cs b/dotnet/samples/AutoGen.BasicSamples/Program.cs
index 3a2edbb585f..16a79e75cff 100644
--- a/dotnet/samples/AutoGen.BasicSamples/Program.cs
+++ b/dotnet/samples/AutoGen.BasicSamples/Program.cs
@@ -11,7 +11,8 @@
// When a new sample is created please add them to the allSamples collection
("Assistant Agent", Example01_AssistantAgent.RunAsync),
("Two-agent Math Chat", Example02_TwoAgent_MathChat.RunAsync),
- ("Agent Function Call", Example03_Agent_FunctionCall.RunAsync),
+ ("Agent Function Call With Source Generator", Example03_Agent_FunctionCall.ToolCallWithSourceGenerator),
+ ("Agent Function Call With M.E.A.I AI Functions", Example03_Agent_FunctionCall.ToolCallWithMEAITools),
("Dynamic Group Chat Coding Task", Example04_Dynamic_GroupChat_Coding_Task.RunAsync),
("DALL-E and GPT4v", Example05_Dalle_And_GPT4V.RunAsync),
("User Proxy Agent", Example06_UserProxyAgent.RunAsync),
diff --git a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj
index c34c03af2b5..f46d48dc844 100644
--- a/dotnet/src/AutoGen.Core/AutoGen.Core.csproj
+++ b/dotnet/src/AutoGen.Core/AutoGen.Core.csproj
@@ -17,6 +17,7 @@
+
diff --git a/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs b/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs
index bb37f1cb25d..9418dc7fd6a 100644
--- a/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs
+++ b/dotnet/src/AutoGen.Core/Function/FunctionAttribute.cs
@@ -3,6 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.AI;
namespace AutoGen.Core;
@@ -22,6 +25,10 @@ public FunctionAttribute(string? functionName = null, string? description = null
public class FunctionContract
{
+ private const string NamespaceKey = nameof(Namespace);
+
+ private const string ClassNameKey = nameof(ClassName);
+
///
/// The namespace of the function.
///
@@ -52,6 +59,7 @@ public class FunctionContract
///
/// The return type of the function.
///
+ [JsonIgnore]
public Type? ReturnType { get; set; }
///
@@ -60,6 +68,39 @@ public class FunctionContract
/// Otherwise, the description will be null.
///
public string? ReturnDescription { get; set; }
+
+ public static implicit operator FunctionContract(AIFunctionMetadata metadata)
+ {
+ return new FunctionContract
+ {
+ Namespace = metadata.AdditionalProperties.ContainsKey(NamespaceKey) ? metadata.AdditionalProperties[NamespaceKey] as string : null,
+ ClassName = metadata.AdditionalProperties.ContainsKey(ClassNameKey) ? metadata.AdditionalProperties[ClassNameKey] as string : null,
+ Name = metadata.Name,
+ Description = metadata.Description,
+ Parameters = metadata.Parameters?.Select(p => (FunctionParameterContract)p).ToList(),
+ ReturnType = metadata.ReturnParameter.ParameterType,
+ ReturnDescription = metadata.ReturnParameter.Description,
+ };
+ }
+
+ public static implicit operator AIFunctionMetadata(FunctionContract contract)
+ {
+ return new AIFunctionMetadata(contract.Name)
+ {
+ Description = contract.Description,
+ ReturnParameter = new AIFunctionReturnParameterMetadata()
+ {
+ Description = contract.ReturnDescription,
+ ParameterType = contract.ReturnType,
+ },
+ AdditionalProperties = new Dictionary
+ {
+ [NamespaceKey] = contract.Namespace,
+ [ClassNameKey] = contract.ClassName,
+ },
+ Parameters = [.. contract.Parameters?.Select(p => (AIFunctionParameterMetadata)p)],
+ };
+ }
}
public class FunctionParameterContract
@@ -79,6 +120,7 @@ public class FunctionParameterContract
///
/// The type of the parameter.
///
+ [JsonIgnore]
public Type? ParameterType { get; set; }
///
@@ -90,4 +132,29 @@ public class FunctionParameterContract
/// The default value of the parameter.
///
public object? DefaultValue { get; set; }
+
+ // convert to/from FunctionParameterMetadata
+ public static implicit operator FunctionParameterContract(AIFunctionParameterMetadata metadata)
+ {
+ return new FunctionParameterContract
+ {
+ Name = metadata.Name,
+ Description = metadata.Description,
+ ParameterType = metadata.ParameterType,
+ IsRequired = metadata.IsRequired,
+ DefaultValue = metadata.DefaultValue,
+ };
+ }
+
+ public static implicit operator AIFunctionParameterMetadata(FunctionParameterContract contract)
+ {
+ return new AIFunctionParameterMetadata(contract.Name!)
+ {
+ DefaultValue = contract.DefaultValue,
+ Description = contract.Description,
+ IsRequired = contract.IsRequired,
+ ParameterType = contract.ParameterType,
+ HasDefaultValue = contract.DefaultValue != null,
+ };
+ }
}
diff --git a/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs b/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs
index 21461834dc8..266155316c8 100644
--- a/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs
+++ b/dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs
@@ -5,8 +5,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
namespace AutoGen.Core;
@@ -43,6 +45,19 @@ public FunctionCallMiddleware(
this.functionMap = functionMap;
}
+ ///
+ /// Create a new instance of with a list of .
+ ///
+ /// function list
+ /// optional middleware name. If not provided, the class name will be used.
+ public FunctionCallMiddleware(IEnumerable functions, string? name = null)
+ {
+ this.Name = name ?? nameof(FunctionCallMiddleware);
+ this.functions = functions.Select(f => (FunctionContract)f.Metadata).ToArray();
+
+ this.functionMap = functions.Select(f => (f.Metadata.Name, this.AIToolInvokeWrapper(f.InvokeAsync))).ToDictionary(f => f.Name, f => f.Item2);
+ }
+
public string? Name { get; }
public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default)
@@ -173,4 +188,20 @@ private async Task InvokeToolCallMessagesAfterInvokingAgentAsync(ToolC
return toolCallMsg;
}
}
+
+ private Func> AIToolInvokeWrapper(Func>?, CancellationToken, Task
diff --git a/dotnet/test/AutoGen.Tests/BasicSampleTest.cs b/dotnet/test/AutoGen.Tests/BasicSampleTest.cs
index df02bb3dcd0..5cf4704037f 100644
--- a/dotnet/test/AutoGen.Tests/BasicSampleTest.cs
+++ b/dotnet/test/AutoGen.Tests/BasicSampleTest.cs
@@ -34,7 +34,8 @@ public async Task TwoAgentMathClassTestAsync()
[ApiKeyFact("OPENAI_API_KEY")]
public async Task AgentFunctionCallTestAsync()
{
- await Example03_Agent_FunctionCall.RunAsync();
+ await Example03_Agent_FunctionCall.ToolCallWithSourceGenerator();
+ await Example03_Agent_FunctionCall.ToolCallWithMEAITools();
}
[ApiKeyFact("MISTRAL_API_KEY")]
diff --git a/dotnet/test/AutoGen.Tests/Function/ApprovalTests/FunctionTests.CreateGetWeatherFunctionFromAIFunctionFactoryAsync.approved.txt b/dotnet/test/AutoGen.Tests/Function/ApprovalTests/FunctionTests.CreateGetWeatherFunctionFromAIFunctionFactoryAsync.approved.txt
new file mode 100644
index 00000000000..f57e0203e35
--- /dev/null
+++ b/dotnet/test/AutoGen.Tests/Function/ApprovalTests/FunctionTests.CreateGetWeatherFunctionFromAIFunctionFactoryAsync.approved.txt
@@ -0,0 +1,76 @@
+[
+ {
+ "Kind": 0,
+ "FunctionName": "GetWeather",
+ "FunctionDescription": "get weather",
+ "FunctionParameters": {
+ "type": "object",
+ "properties": {
+ "city": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "city"
+ ]
+ }
+ },
+ {
+ "Kind": 0,
+ "FunctionName": "GetWeatherStatic",
+ "FunctionDescription": "get weather from static method",
+ "FunctionParameters": {
+ "type": "object",
+ "properties": {
+ "city": {
+ "type": "string"
+ },
+ "date": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "city",
+ "date"
+ ]
+ }
+ },
+ {
+ "Kind": 0,
+ "FunctionName": "GetWeather",
+ "FunctionDescription": "get weather from async method",
+ "FunctionParameters": {
+ "type": "object",
+ "properties": {
+ "city": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "city"
+ ]
+ }
+ },
+ {
+ "Kind": 0,
+ "FunctionName": "GetWeatherAsyncStatic",
+ "FunctionDescription": "get weather from async static method",
+ "FunctionParameters": {
+ "type": "object",
+ "properties": {
+ "city": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "city"
+ ]
+ }
+ }
+]
\ No newline at end of file
diff --git a/dotnet/test/AutoGen.Tests/Function/FunctionTests.cs b/dotnet/test/AutoGen.Tests/Function/FunctionTests.cs
new file mode 100644
index 00000000000..768558d35ba
--- /dev/null
+++ b/dotnet/test/AutoGen.Tests/Function/FunctionTests.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// FunctionTests.cs
+
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using ApprovalTests;
+using ApprovalTests.Namers;
+using ApprovalTests.Reporters;
+using AutoGen.OpenAI.Extension;
+using FluentAssertions;
+using Microsoft.Extensions.AI;
+using Xunit;
+
+namespace AutoGen.Tests.Function;
+public class FunctionTests
+{
+ private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
+ [Description("get weather")]
+ public string GetWeather(string city, string date = "today")
+ {
+ return $"The weather in {city} is sunny.";
+ }
+
+ [Description("get weather from static method")]
+ [return: Description("weather information")]
+ public static string GetWeatherStatic(string city, string[] date)
+ {
+ return $"The weather in {city} is sunny.";
+ }
+
+ [Description("get weather from async method")]
+ public async Task GetWeatherAsync(string city)
+ {
+ await Task.Delay(100);
+ return $"The weather in {city} is sunny.";
+ }
+
+ [Description("get weather from async static method")]
+ public static async Task GetWeatherAsyncStatic(string city)
+ {
+ await Task.Delay(100);
+ return $"The weather in {city} is sunny.";
+ }
+
+ [Fact]
+ [UseReporter(typeof(DiffReporter))]
+ [UseApprovalSubdirectory("ApprovalTests")]
+ public async Task CreateGetWeatherFunctionFromAIFunctionFactoryAsync()
+ {
+ Delegate[] availableDelegates = [
+ GetWeather,
+ GetWeatherStatic,
+ GetWeatherAsync,
+ GetWeatherAsyncStatic,
+ ];
+
+ var functionContracts = availableDelegates.Select(function => (FunctionContract)AIFunctionFactory.Create(function).Metadata).ToList();
+
+ // Verify the function contracts
+ functionContracts.Should().HaveCount(4);
+
+ var openAIToolContracts = functionContracts.Select(f =>
+ {
+ var tool = f.ToChatTool();
+
+ return new
+ {
+ tool.Kind,
+ tool.FunctionName,
+ tool.FunctionDescription,
+ FunctionParameters = tool.FunctionParameters.ToObjectFromJson