openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
jsquire-patch-1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/Utility/ResponseTools.cs

282lines · modeblame

9308b2b1Christopher Scott1 years ago1using System;
2using System.ClientModel.Primitives;
3using System.Collections.Generic;
4using System.IO;
5using System.Linq;
6using System.Reflection;
7using System.Text.Json;
8using System.Threading.Tasks;
9using OpenAI.Agents;
10using OpenAI.Embeddings;
11using OpenAI.Responses;
12
13namespace OpenAI.Responses;
14
15/// <summary>
16/// Provides functionality to manage and execute OpenAI function tools for responses.
17/// </summary>
18//[Experimental("OPENAIMCP001")
19public class ResponseTools
20{
21private readonly Dictionary<string, MethodInfo> _methods = [];
22private readonly Dictionary<string, Func<string, BinaryData, Task<BinaryData>>> _mcpMethods = [];
23private readonly List<ResponseTool> _tools = [];
24private readonly EmbeddingClient _client;
25private readonly List<VectorDatabaseEntry> _entries = [];
26private readonly List<McpClient> _mcpClients = [];
27private readonly Dictionary<string, McpClient> _mcpClientsByEndpoint = [];
28
29/// <summary>
30/// Initializes a new instance of the ResponseTools class with an optional embedding client.
31/// </summary>
32/// <param name="client">The embedding client used for tool vectorization, or null to disable vectorization.</param>
33public ResponseTools(EmbeddingClient client = null)
34{
35_client = client;
36}
37
38/// <summary>
39/// Initializes a new instance of the ResponseTools class with the specified tool types.
40/// </summary>
41/// <param name="tools">Additional tool types to add.</param>
42public ResponseTools(params Type[] tools) : this((EmbeddingClient)null)
43{
44foreach (var t in tools)
45AddFunctionTool(t);
46}
47
48/// <summary>
49/// Gets the list of defined tools.
50/// </summary>
51public IList<ResponseTool> Tools => _tools;
52
53/// <summary>
54/// Gets whether tools can be filtered using embeddings provided by the provided <see cref="EmbeddingClient"/> .
55/// </summary>
56public bool CanFilterTools => _client != null;
57
58/// <summary>
59/// Adds local tool implementations from the provided types.
60/// </summary>
61/// <param name="tools">Types containing static methods to be used as tools.</param>
62public void AddFunctionTools(params Type[] tools)
63{
64foreach (Type functionHolder in tools)
65AddFunctionTool(functionHolder);
66}
67
68/// <summary>
69/// Adds all public static methods from the specified type as tools.
70/// </summary>
71/// <param name="tool">The type containing tool methods.</param>
72internal void AddFunctionTool(Type tool)
73{
74#pragma warning disable IL2070
75foreach (MethodInfo function in tool.GetMethods(BindingFlags.Public | BindingFlags.Static))
76{
77AddFunctionTool(function);
78}
79#pragma warning restore IL2070
80}
81
82internal void AddFunctionTool(MethodInfo function)
83{
84string name = function.Name;
85var tool = ResponseTool.CreateFunctionTool(name, ToolsUtility.GetMethodDescription(function), ToolsUtility.BuildParametersJson(function.GetParameters()));
86_tools.Add(tool);
87_methods[name] = function;
88}
89
90/// <summary>
91/// Adds a remote MCP server as a tool provider.
92/// </summary>
93/// <param name="client">The MCP client instance.</param>
94/// <returns>A task representing the asynchronous operation.</returns>
95internal async Task AddMcpToolsAsync(McpClient client)
96{
97if (client == null) throw new ArgumentNullException(nameof(client));
98_mcpClientsByEndpoint[client.Endpoint.AbsoluteUri] = client;
99await client.StartAsync().ConfigureAwait(false);
100BinaryData tools = await client.ListToolsAsync().ConfigureAwait(false);
101await AddMcpToolsAsync(tools, client).ConfigureAwait(false);
102_mcpClients.Add(client);
103}
104
105/// <summary>
106/// Adds a remote MCP server as a tool provider.
107/// </summary>
108/// <param name="mcpEndpoint">The URI endpoint of the MCP server.</param>
109/// <returns>A task representing the asynchronous operation.</returns>
110public async Task AddMcpToolsAsync(Uri mcpEndpoint)
111{
112var client = new McpClient(mcpEndpoint);
113await AddMcpToolsAsync(client).ConfigureAwait(false);
114}
115
116private async Task AddMcpToolsAsync(BinaryData toolDefinitions, McpClient client)
117{
118List<ResponseTool> toolsToVectorize = new();
119var parsedTools = ToolsUtility.ParseMcpToolDefinitions(toolDefinitions, client);
120
121foreach (var (name, description, inputSchema) in parsedTools)
122{
123var responseTool = ResponseTool.CreateFunctionTool(name, description, BinaryData.FromString(inputSchema));
124_tools.Add(responseTool);
125toolsToVectorize.Add(responseTool);
126_mcpMethods[name] = client.CallToolAsync;
127}
128
129if (_client != null)
130{
131var embeddings = await _client.GenerateEmbeddingsAsync(toolsToVectorize.ConvertAll(GetDescription)).ConfigureAwait(false);
132foreach (var embedding in embeddings.Value)
133{
134var vector = embedding.ToFloats();
135var item = toolsToVectorize[embedding.Index];
136var toolDefinition = SerializeTool(item);
137_entries.Add(new VectorDatabaseEntry(vector, toolDefinition));
138}
139}
140}
141
142private string GetDescription(ResponseTool tool) => (tool as InternalResponsesFunctionTool)?.Description ?? "";
143
144private BinaryData SerializeTool(ResponseTool tool)
145{
146var functionTool = tool as InternalResponsesFunctionTool;
147return ToolsUtility.SerializeTool(functionTool?.Name, functionTool?.Description, functionTool?.Parameters ?? BinaryData.FromString("{}"));
148}
149
150private ResponseTool ParseToolDefinition(BinaryData data)
151{
152using var document = JsonDocument.Parse(data);
153var root = document.RootElement;
154
155return ResponseTool.CreateFunctionTool(
156root.GetProperty("name").GetString()!,
157root.GetProperty("description").GetString()!,
158BinaryData.FromString(root.GetProperty("inputSchema").GetRawText()));
159}
160
161/// <summary>
162/// Converts the tools collection to <see cref="ResponseCreationOptions"> configured with the tools contained in this instance..
163/// </summary>
164/// <returns>A new ResponseCreationOptions containing all defined tools.</returns>
165public ResponseCreationOptions ToResponseCreationOptions()
166{
167var options = new ResponseCreationOptions();
168foreach (var tool in _tools)
169options.Tools.Add(tool);
170return options;
171}
172
173/// <summary>
174/// Converts the tools collection to <see cref="ResponseCreationOptions">, filtered by relevance to the given prompt.
175/// </summary>
176/// <param name="prompt">The prompt to find relevant tools for.</param>
177/// <param name="maxTools">The maximum number of tools to return. Default is 5.</param>
178/// <param name="minVectorDistance">The similarity threshold for including tools. Default is 0.29.</param>
179/// <returns>A new ResponseCreationOptions containing the most relevant tools.</returns>
180public ResponseCreationOptions ToResponseCreationOptions(string prompt, int maxTools = 5, float minVectorDistance = 0.29f)
181{
182if (!CanFilterTools)
183return ToResponseCreationOptions();
184
185var completionOptions = new ResponseCreationOptions();
186foreach (var tool in FindRelatedTools(false, prompt, maxTools, minVectorDistance).GetAwaiter().GetResult())
187completionOptions.Tools.Add(tool);
188return completionOptions;
189}
190
191/// <summary>
192/// Converts the tools collection to <see cref="ResponseCreationOptions">, filtered by relevance to the given prompt.
193/// </summary>
194/// <param name="prompt">The prompt to find relevant tools for.</param>
195/// <param name="maxTools">The maximum number of tools to return. Default is 5.</param>
196/// <param name="minVectorDistance">The similarity threshold for including tools. Default is 0.29.</param>
197/// <returns>A new ResponseCreationOptions containing the most relevant tools.</returns>
198public async Task<ResponseCreationOptions> ToResponseCreationOptionsAsync(string prompt, int maxTools = 5, float minVectorDistance = 0.29f)
199{
200if (!CanFilterTools)
201return ToResponseCreationOptions();
202
203var completionOptions = new ResponseCreationOptions();
204foreach (var tool in await FindRelatedTools(true, prompt, maxTools, minVectorDistance).ConfigureAwait(false))
205completionOptions.Tools.Add(tool);
206return completionOptions;
207}
208
209private async Task<IEnumerable<ResponseTool>> FindRelatedTools(bool async, string prompt, int maxTools, float minVectorDistance)
210{
211if (!CanFilterTools)
212return _tools;
213
214return (await FindVectorMatches(async, prompt, maxTools, minVectorDistance).ConfigureAwait(false))
215.Select(e => ParseToolDefinition(e.Data));
216}
217
218private async Task<IEnumerable<VectorDatabaseEntry>> FindVectorMatches(bool async, string prompt, int maxTools, float minVectorDistance)
219{
220var vector = async ?
221await ToolsUtility.GetEmbeddingAsync(_client, prompt).ConfigureAwait(false) :
222ToolsUtility.GetEmbedding(_client, prompt);
223lock (_entries)
224{
225return ToolsUtility.GetClosestEntries(_entries, maxTools, minVectorDistance, vector);
226}
227}
228
229internal async Task<string> CallFunctionToolAsync(FunctionCallResponseItem call)
230{
231List<object> arguments = new();
232if (call.FunctionArguments != null)
233{
234if (!_methods.TryGetValue(call.FunctionName, out MethodInfo method))
235return $"I don't have a tool called {call.FunctionName}";
236
237ToolsUtility.ParseFunctionCallArgs(method, call.FunctionArguments, out arguments);
238}
239
240return await ToolsUtility.CallFunctionToolAsync(_methods, call.FunctionName, [.. arguments]);
241}
242
243internal async Task<string> CallMcpAsync(FunctionCallResponseItem call)
244{
245if (!_mcpMethods.TryGetValue(call.FunctionName, out var method))
246throw new NotImplementedException($"MCP tool {call.FunctionName} not found.");
247
248#if !NETSTANDARD2_0
249var actualFunctionName = call.FunctionName.Split(ToolsUtility.McpToolSeparator, 2)[1];
250#else
251var index = call.FunctionName.IndexOf(ToolsUtility.McpToolSeparator);
252var actualFunctionName = call.FunctionName.Substring(index + ToolsUtility.McpToolSeparator.Length);
253#endif
254var result = await method(actualFunctionName, call.FunctionArguments).ConfigureAwait(false);
255return result.ToString();
256}
257
258/// <summary>
259/// Executes a function call and returns its result as a FunctionCallOutputResponseItem.
260/// </summary>
261/// <param name="toolCall">The function call to execute.</param>
262/// <returns>A task that represents the asynchronous operation and contains the function call result.</returns>
263public async Task<FunctionCallOutputResponseItem> CallAsync(FunctionCallResponseItem toolCall)
264{
265bool isMcpTool = false;
266if (!_methods.ContainsKey(toolCall.FunctionName))
267{
268if (_mcpMethods.ContainsKey(toolCall.FunctionName))
269{
270isMcpTool = true;
271}
272else
273{
274return new FunctionCallOutputResponseItem(toolCall.CallId, $"I don't have a tool called {toolCall.FunctionName}");
275}
276}
277
278var result = isMcpTool ? await CallMcpAsync(toolCall).ConfigureAwait(false) : await CallFunctionToolAsync(toolCall);
279return new FunctionCallOutputResponseItem(toolCall.CallId, result);
280}
281}
282