Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fb
  • Loading branch information
christothes committed May 5, 2025
commit 5703fc04c85fd439ea55b5cd9d56c8b822594d78
34 changes: 15 additions & 19 deletions src/Utility/ChatTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public ChatTools(EmbeddingClient client = null)
public ChatTools(params Type[] tools) : this((EmbeddingClient)null)
{
foreach (var t in tools)
AddLocalTool(t);
AddFunctionTool(t);
}

/// <summary>
Expand All @@ -58,27 +58,27 @@ public ChatTools(params Type[] tools) : this((EmbeddingClient)null)
/// Adds local tool implementations from the provided types.
/// </summary>
/// <param name="tools">Types containing static methods to be used as tools.</param>
public void AddLocalTools(params Type[] tools)
public void AddFunctionTools(params Type[] tools)
{
foreach (Type functionHolder in tools)
AddLocalTool(functionHolder);
AddFunctionTool(functionHolder);
}

/// <summary>
/// Adds all public static methods from the specified type as tools.
/// </summary>
/// <param name="tool">The type containing tool methods.</param>
internal void AddLocalTool(Type tool)
internal void AddFunctionTool(Type tool)
{
#pragma warning disable IL2070
foreach (MethodInfo function in tool.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
AddLocalTool(function);
AddFunctionTool(function);
}
#pragma warning restore IL2070
}

internal void AddLocalTool(MethodInfo function)
internal void AddFunctionTool(MethodInfo function)
{
string name = function.Name;
var tool = ChatTool.CreateFunctionTool(name, ToolsUtility.GetMethodDescription(function), ToolsUtility.BuildParametersJson(function.GetParameters()));
Expand All @@ -97,7 +97,7 @@ public async Task AddMcpToolsAsync(McpClient client)
_mcpClientsByEndpoint[client.Endpoint.AbsoluteUri] = client;
await client.StartAsync().ConfigureAwait(false);
BinaryData tools = await client.ListToolsAsync().ConfigureAwait(false);
await AddToolsAsync(tools, client).ConfigureAwait(false);
await AddMcpToolsAsync(tools, client).ConfigureAwait(false);
_mcpClients.Add(client);
}

Expand All @@ -112,7 +112,7 @@ public async Task AddMcpToolsAsync(Uri mcpEndpoint)
await AddMcpToolsAsync(client).ConfigureAwait(false);
}

private async Task AddToolsAsync(BinaryData toolDefinitions, McpClient client)
private async Task AddMcpToolsAsync(BinaryData toolDefinitions, McpClient client)
{
List<ChatTool> toolsToVectorize = new();
var parsedTools = ToolsUtility.ParseMcpToolDefinitions(toolDefinitions, client);
Expand Down Expand Up @@ -158,7 +158,7 @@ private ChatTool ParseToolDefinition(BinaryData data)
/// Converts the tools collection to chat completion options.
/// </summary>
/// <returns>A new ChatCompletionOptions containing all defined tools.</returns>
public ChatCompletionOptions CreateCompletionOptions()
public ChatCompletionOptions ToChatCompletionOptions()
{
var options = new ChatCompletionOptions();
foreach (var tool in _tools)
Expand All @@ -173,10 +173,10 @@ public ChatCompletionOptions CreateCompletionOptions()
/// <param name="maxTools">The maximum number of tools to return. Default is 3.</param>
/// <param name="minVectorDistance">The similarity threshold for including tools. Default is 0.29.</param>
/// <returns>A new <see cref="ChatCompletionOptions"/> containing the most relevant tools.</returns>
public ChatCompletionOptions CreateCompletionOptions(string prompt, int maxTools = 3, float minVectorDistance = 0.29f)
public ChatCompletionOptions CreateCompletionOptions(string prompt, int maxTools = 5, float minVectorDistance = 0.29f)
{
if (!CanFilterTools)
return CreateCompletionOptions();
return ToChatCompletionOptions();

var completionOptions = new ChatCompletionOptions();
foreach (var tool in FindRelatedTools(false, prompt, maxTools, minVectorDistance).GetAwaiter().GetResult())
Expand All @@ -191,10 +191,10 @@ public ChatCompletionOptions CreateCompletionOptions(string prompt, int maxTools
/// <param name="maxTools">The maximum number of tools to return. Default is 3.</param>
/// <param name="minVectorDistance">The similarity threshold for including tools. Default is 0.29.</param>
/// <returns>A new <see cref="ChatCompletionOptions"/> containing the most relevant tools.</returns>
public async Task<ChatCompletionOptions> CreateCompletionOptionsAsync(string prompt, int maxTools = 3, float minVectorDistance = 0.29f)
public async Task<ChatCompletionOptions> ToChatCompletionOptions(string prompt, int maxTools = 5, float minVectorDistance = 0.29f)
{
if (!CanFilterTools)
return CreateCompletionOptions();
return ToChatCompletionOptions();

var completionOptions = new ChatCompletionOptions();
foreach (var tool in await FindRelatedTools(true, prompt, maxTools, minVectorDistance).ConfigureAwait(false))
Expand Down Expand Up @@ -223,12 +223,6 @@ await ToolsUtility.GetEmbeddingAsync(_client, prompt).ConfigureAwait(false) :
}
}

/// <summary>
/// Implicitly converts ChatTools to <see cref="ChatCompletionOptions"/>.
/// </summary>
/// <param name="tools">The ChatTools instance to convert.</param>
public static implicit operator ChatCompletionOptions(ChatTools tools) => tools.CreateCompletionOptions();

internal string CallLocal(ChatToolCall call)
{
var arguments = new List<object>();
Expand Down Expand Up @@ -260,6 +254,8 @@ internal async Task<string> CallMcpAsync(ChatToolCall call)
var actualFunctionName = call.FunctionName.Substring(index + ToolsUtility.McpToolSeparator.Length);
#endif
var result = await method(actualFunctionName, call.FunctionArguments).ConfigureAwait(false);
if (result == null)
throw new InvalidOperationException($"MCP tool {call.FunctionName} returned null. Function tools should always return a value.");
return result.ToString();
}

Expand Down
32 changes: 13 additions & 19 deletions src/Utility/ResponseTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public ResponseTools(EmbeddingClient client = null)
public ResponseTools(params Type[] tools) : this((EmbeddingClient)null)
{
foreach (var t in tools)
AddLocalTool(t);
AddFunctionTool(t);
}

/// <summary>
Expand All @@ -58,27 +58,27 @@ public ResponseTools(params Type[] tools) : this((EmbeddingClient)null)
/// Adds local tool implementations from the provided types.
/// </summary>
/// <param name="tools">Types containing static methods to be used as tools.</param>
public void AddLocalTools(params Type[] tools)
public void AddFunctionTools(params Type[] tools)
{
foreach (Type functionHolder in tools)
AddLocalTool(functionHolder);
AddFunctionTool(functionHolder);
}

/// <summary>
/// Adds all public static methods from the specified type as tools.
/// </summary>
/// <param name="tool">The type containing tool methods.</param>
internal void AddLocalTool(Type tool)
internal void AddFunctionTool(Type tool)
{
#pragma warning disable IL2070
foreach (MethodInfo function in tool.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
AddLocalTool(function);
AddFunctionTool(function);
}
#pragma warning restore IL2070
}

internal void AddLocalTool(MethodInfo function)
internal void AddFunctionTool(MethodInfo function)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is reinventing AIFunctionFactory from Microsoft.Extensions.AI, albeit with fewer features developers need. It'd be better to just use AIFunctionFactory. Static methods alone are insufficient to address real-world need. Developers need support instance methods, both on supplied instances and those manufactured from DI. Developers need to be able to inject state, such as satisfying parameters from dependency injection. Developers need to support flowing cancellation. Developers need support for complex parameters and return types, and with proper NativeAOT support. And so on.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would love to use some reusable library for this. But currently, we are not ready take a hard dependency on MEAI. Let's keep discussing how to solve the problem offline (not in this PR)

{
string name = function.Name;
var tool = ResponseTool.CreateFunctionTool(name, ToolsUtility.GetMethodDescription(function), ToolsUtility.BuildParametersJson(function.GetParameters()));
Expand All @@ -97,7 +97,7 @@ public async Task AddMcpToolsAsync(McpClient client)
_mcpClientsByEndpoint[client.Endpoint.AbsoluteUri] = client;
await client.StartAsync().ConfigureAwait(false);
BinaryData tools = await client.ListToolsAsync().ConfigureAwait(false);
await AddToolsAsync(tools, client).ConfigureAwait(false);
await AddMcpToolsAsync(tools, client).ConfigureAwait(false);
_mcpClients.Add(client);
}

Expand All @@ -112,7 +112,7 @@ public async Task AddMcpToolsAsync(Uri mcpEndpoint)
await AddMcpToolsAsync(client).ConfigureAwait(false);
}

private async Task AddToolsAsync(BinaryData toolDefinitions, McpClient client)
private async Task AddMcpToolsAsync(BinaryData toolDefinitions, McpClient client)
{
List<ResponseTool> toolsToVectorize = new();
var parsedTools = ToolsUtility.ParseMcpToolDefinitions(toolDefinitions, client);
Expand Down Expand Up @@ -161,7 +161,7 @@ private ResponseTool ParseToolDefinition(BinaryData data)
/// Converts the tools collection to <see cref="ResponseCreationOptions"> configured with the tools contained in this instance..
/// </summary>
/// <returns>A new ResponseCreationOptions containing all defined tools.</returns>
public ResponseCreationOptions CreateResponseOptions()
public ResponseCreationOptions ToResponseCreationOptions()
{
var options = new ResponseCreationOptions();
foreach (var tool in _tools)
Expand All @@ -176,10 +176,10 @@ public ResponseCreationOptions CreateResponseOptions()
/// <param name="maxTools">The maximum number of tools to return. Default is 5.</param>
/// <param name="minVectorDistance">The similarity threshold for including tools. Default is 0.29.</param>
/// <returns>A new ResponseCreationOptions containing the most relevant tools.</returns>
public ResponseCreationOptions CreateResponseOptions(string prompt, int maxTools = 5, float minVectorDistance = 0.29f)
public ResponseCreationOptions ToResponseCreationOptions(string prompt, int maxTools = 5, float minVectorDistance = 0.29f)
{
if (!CanFilterTools)
return CreateResponseOptions();
return ToResponseCreationOptions();

var completionOptions = new ResponseCreationOptions();
foreach (var tool in FindRelatedTools(false, prompt, maxTools, minVectorDistance).GetAwaiter().GetResult())
Expand All @@ -194,10 +194,10 @@ public ResponseCreationOptions CreateResponseOptions(string prompt, int maxTools
/// <param name="maxTools">The maximum number of tools to return. Default is 5.</param>
/// <param name="minVectorDistance">The similarity threshold for including tools. Default is 0.29.</param>
/// <returns>A new ResponseCreationOptions containing the most relevant tools.</returns>
public async Task<ResponseCreationOptions> CreateResponseOptionsAsync(string prompt, int maxTools = 5, float minVectorDistance = 0.29f)
public async Task<ResponseCreationOptions> ToResponseCreationOptionsAsync(string prompt, int maxTools = 5, float minVectorDistance = 0.29f)
{
if (!CanFilterTools)
return CreateResponseOptions();
return ToResponseCreationOptions();

var completionOptions = new ResponseCreationOptions();
foreach (var tool in await FindRelatedTools(true, prompt, maxTools, minVectorDistance).ConfigureAwait(false))
Expand Down Expand Up @@ -225,12 +225,6 @@ await ToolsUtility.GetEmbeddingAsync(_client, prompt).ConfigureAwait(false) :
}
}

/// <summary>
/// Implicitly converts ResponseTools to ResponseCreationOptions.
/// </summary>
/// <param name="tools">The ResponseTools instance to convert.</param>
public static implicit operator ResponseCreationOptions(ResponseTools tools) => tools.CreateResponseOptions();

internal string CallLocal(FunctionCallResponseItem call)
{
List<object> arguments = new();
Expand Down
39 changes: 13 additions & 26 deletions tests/Utility/ChatToolsTests.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
using Moq;
using NUnit.Framework;
using OpenAI.Chat;
using OpenAI.Embeddings;
using System;
using System.Collections.Generic;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using OpenAI.Agents;
using OpenAI.Chat;
using OpenAI.Embeddings;

namespace OpenAI.Tests.Utility;

Expand All @@ -37,7 +37,7 @@ public void Setup()
public void CanAddLocalTools()
{
var tools = new ChatTools();
tools.AddLocalTools(typeof(TestTools));
tools.AddFunctionTools(typeof(TestTools));

Assert.That(tools.Tools, Has.Count.EqualTo(2));
Assert.That(tools.Tools.Any(t => t.FunctionName == "Echo"));
Expand All @@ -48,7 +48,7 @@ public void CanAddLocalTools()
public async Task CanCallToolsAsync()
{
var tools = new ChatTools();
tools.AddLocalTools(typeof(TestTools));
tools.AddFunctionTools(typeof(TestTools));

var toolCalls = new[]
{
Expand All @@ -70,9 +70,9 @@ public async Task CanCallToolsAsync()
public void CreatesCompletionOptionsWithTools()
{
var tools = new ChatTools();
tools.AddLocalTools(typeof(TestTools));
tools.AddFunctionTools(typeof(TestTools));

var options = tools.CreateCompletionOptions();
var options = tools.ToChatCompletionOptions();

Assert.That(options.Tools, Has.Count.EqualTo(2));
Assert.That(options.Tools.Any(t => t.FunctionName == "Echo"));
Expand All @@ -98,26 +98,13 @@ public async Task CanFilterToolsByRelevance()
.ReturnsAsync(ClientResult.FromValue(embedding, mockResponse));

var tools = new ChatTools(mockEmbeddingClient.Object);
tools.AddLocalTools(typeof(TestTools));
tools.AddFunctionTools(typeof(TestTools));

var options = await tools.CreateCompletionOptionsAsync("Need to add two numbers", 1, 0.5f);
var options = await tools.ToChatCompletionOptions("Need to add two numbers", 1, 0.5f);

Assert.That(options.Tools, Has.Count.LessThanOrEqualTo(1));
}

[Test]
public void ImplicitConversionToCompletionOptions()
{
var tools = new ChatTools();
tools.AddLocalTools(typeof(TestTools));

ChatCompletionOptions options = tools;

Assert.That(options.Tools, Has.Count.EqualTo(2));
Assert.That(options.Tools.Any(t => t.FunctionName == "Echo"));
Assert.That(options.Tools.Any(t => t.FunctionName == "Add"));
}

[Test]
public void ThrowsWhenCallingNonExistentTool()
{
Expand Down Expand Up @@ -306,11 +293,11 @@ public async Task CreateCompletionOptions_WithMaxToolsParameter_FiltersTools()

// Act & Assert
// Test with maxTools = 1
var options1 = await tools.CreateCompletionOptionsAsync("calculate 2+2", 1, 0.5f);
var options1 = await tools.ToChatCompletionOptions("calculate 2+2", 1, 0.5f);
Assert.That(options1.Tools, Has.Count.EqualTo(1));

// Test with maxTools = 2
var options2 = await tools.CreateCompletionOptionsAsync("calculate 2+2", 2, 0.5f);
var options2 = await tools.ToChatCompletionOptions("calculate 2+2", 2, 0.5f);
Assert.That(options2.Tools, Has.Count.EqualTo(2));

// Test that we can call the tools after filtering
Expand Down
Loading