diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 20bc87dd9f3..1de5dd79d4a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -306,6 +306,8 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( if (options.Tools is { Count: > 0 } tools) { + HashSet toolsOverride = new(ToolDefinitionNameEqualityComparer.Instance); + // If the caller has provided any tool overrides, we'll assume they don't want to use the assistant's tools. // But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to // just add them. To handle that, we'll get all of the assistant's tools and add them to the override list @@ -318,10 +320,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( _assistantTools = assistant.Value.Tools; } - foreach (var tool in _assistantTools) - { - runOptions.ToolsOverride.Add(tool); - } + toolsOverride.UnionWith(_assistantTools); } // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools. @@ -330,12 +329,12 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( switch (tool) { case AIFunctionDeclaration aiFunction: - runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); + _ = toolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options)); break; case HostedCodeInterpreterTool codeInterpreterTool: var interpreterToolDef = ToolDefinition.CreateCodeInterpreter(); - runOptions.ToolsOverride.Add(interpreterToolDef); + _ = toolsOverride.Add(interpreterToolDef); if (codeInterpreterTool.Inputs?.Count is > 0) { @@ -358,7 +357,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( break; case HostedFileSearchTool fileSearchTool: - runOptions.ToolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount)); + _ = toolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount)); if (fileSearchTool.Inputs is { Count: > 0 } fileSearchInputs) { foreach (var input in fileSearchInputs) @@ -374,6 +373,11 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( break; } } + + foreach (var tool in toolsOverride) + { + runOptions.ToolsOverride.Add(tool); + } } // Store the tool mode, if relevant. @@ -543,4 +547,22 @@ void AppendSystemInstructions(string? toAppend) return runId; } + + /// + /// Provides the same behavior as , except + /// for it compares names so that two function tool definitions with the + /// same name compare equally. + /// + private sealed class ToolDefinitionNameEqualityComparer : IEqualityComparer + { + public static ToolDefinitionNameEqualityComparer Instance { get; } = new(); + + public bool Equals(ToolDefinition? x, ToolDefinition? y) => + x is FunctionToolDefinition xFtd && y is FunctionToolDefinition yFtd ? xFtd.FunctionName.Equals(yFtd.FunctionName, StringComparison.Ordinal) : + EqualityComparer.Default.Equals(x, y); + + public int GetHashCode(ToolDefinition obj) => + obj is FunctionToolDefinition ftd ? ftd.FunctionName.GetHashCode(StringComparison.Ordinal) : + EqualityComparer.Default.GetHashCode(obj); + } }