Skip to content

Commit 53ab892

Browse files
authored
Fix duplication with OpenAI Assistants pre-configured tools (#6896)
Assistants lets you configure an agent with function signatures it's implicitly aware of, rather than them needing to be provided per request. That, however, creates complication for callers, as if they provide that function in ChatTools.Options, it leads to the function's signature being sent as part of the request, and the duplication of it with the pre-configured function signature results in an error. It's possible to work around this, by simply not including that function in the request, but it's a natural thing for a developer to do, especially if they want the function to be automatically invoked when the model requests it. You can achieve that by putting the function into the FunctionInvocationChatClient's AdditionalTools, which exists for this kind of purpose, but that's harder to discover. Rather than try something more complicated, this commit simply deduplicates all tools by putting them into a set, deduplicating any duplicates provided.
1 parent 22cbbb7 commit 53ab892

File tree

1 file changed

+29
-7
lines changed

1 file changed

+29
-7
lines changed

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(
306306

307307
if (options.Tools is { Count: > 0 } tools)
308308
{
309+
HashSet<ToolDefinition> toolsOverride = new(ToolDefinitionNameEqualityComparer.Instance);
310+
309311
// If the caller has provided any tool overrides, we'll assume they don't want to use the assistant's tools.
310312
// But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to
311313
// 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(
318320
_assistantTools = assistant.Value.Tools;
319321
}
320322

321-
foreach (var tool in _assistantTools)
322-
{
323-
runOptions.ToolsOverride.Add(tool);
324-
}
323+
toolsOverride.UnionWith(_assistantTools);
325324
}
326325

327326
// 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(
330329
switch (tool)
331330
{
332331
case AIFunctionDeclaration aiFunction:
333-
runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options));
332+
_ = toolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options));
334333
break;
335334

336335
case HostedCodeInterpreterTool codeInterpreterTool:
337336
var interpreterToolDef = ToolDefinition.CreateCodeInterpreter();
338-
runOptions.ToolsOverride.Add(interpreterToolDef);
337+
_ = toolsOverride.Add(interpreterToolDef);
339338

340339
if (codeInterpreterTool.Inputs?.Count is > 0)
341340
{
@@ -358,7 +357,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(
358357
break;
359358

360359
case HostedFileSearchTool fileSearchTool:
361-
runOptions.ToolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount));
360+
_ = toolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount));
362361
if (fileSearchTool.Inputs is { Count: > 0 } fileSearchInputs)
363362
{
364363
foreach (var input in fileSearchInputs)
@@ -374,6 +373,11 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(
374373
break;
375374
}
376375
}
376+
377+
foreach (var tool in toolsOverride)
378+
{
379+
runOptions.ToolsOverride.Add(tool);
380+
}
377381
}
378382

379383
// Store the tool mode, if relevant.
@@ -543,4 +547,22 @@ void AppendSystemInstructions(string? toAppend)
543547

544548
return runId;
545549
}
550+
551+
/// <summary>
552+
/// Provides the same behavior as <see cref="EqualityComparer{ToolDefinition}.Default"/>, except
553+
/// for <see cref="FunctionToolDefinition"/> it compares names so that two function tool definitions with the
554+
/// same name compare equally.
555+
/// </summary>
556+
private sealed class ToolDefinitionNameEqualityComparer : IEqualityComparer<ToolDefinition>
557+
{
558+
public static ToolDefinitionNameEqualityComparer Instance { get; } = new();
559+
560+
public bool Equals(ToolDefinition? x, ToolDefinition? y) =>
561+
x is FunctionToolDefinition xFtd && y is FunctionToolDefinition yFtd ? xFtd.FunctionName.Equals(yFtd.FunctionName, StringComparison.Ordinal) :
562+
EqualityComparer<ToolDefinition?>.Default.Equals(x, y);
563+
564+
public int GetHashCode(ToolDefinition obj) =>
565+
obj is FunctionToolDefinition ftd ? ftd.FunctionName.GetHashCode(StringComparison.Ordinal) :
566+
EqualityComparer<ToolDefinition>.Default.GetHashCode(obj);
567+
}
546568
}

0 commit comments

Comments
 (0)