-
Notifications
You must be signed in to change notification settings - Fork 862
Add HostedToolSearchTool and SearchableAIFunctionDeclaration for tool search / deferred loading support #7377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
475d067
8e7c59b
bf6107a
40f3432
5997a6a
7f3893f
4ad7331
74f0c51
2b39eb9
78f7d57
8e24e6f
8ddecb8
f53ca13
1822466
87f81ff
ba60cd3
89d8df4
0579b05
3a28bd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Collections.Generic; | ||
| using Microsoft.Shared.Diagnostics; | ||
|
|
||
| namespace Microsoft.Extensions.AI; | ||
|
|
||
| /// <summary> | ||
| /// Represents an <see cref="AIFunctionDeclaration"/> that signals to supporting AI services that deferred | ||
| /// loading should be used when tool search is enabled. Only the function's name and description are sent initially; | ||
| /// the full JSON schema is loaded on demand by the service when the model selects this tool. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This class is a marker/decorator that signals to a supporting provider that the function should be | ||
| /// sent with deferred loading (only name and description upfront). Use <see cref="CreateToolSet"/> to create | ||
| /// a complete tool list including a <see cref="HostedToolSearchTool"/> and wrapped functions. | ||
| /// </remarks> | ||
| public sealed class SearchableAIFunctionDeclaration : DelegatingAIFunctionDeclaration | ||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="SearchableAIFunctionDeclaration"/> class. | ||
| /// </summary> | ||
| /// <param name="innerFunction">The <see cref="AIFunctionDeclaration"/> represented by this instance.</param> | ||
| /// <param name="namespace">An optional namespace to group this function under for tool search.</param> | ||
| /// <exception cref="System.ArgumentNullException"><paramref name="innerFunction"/> is <see langword="null"/>.</exception> | ||
| public SearchableAIFunctionDeclaration(AIFunctionDeclaration innerFunction, string? @namespace = null) | ||
| : base(innerFunction) | ||
| { | ||
| Namespace = @namespace; | ||
| } | ||
|
|
||
| /// <summary>Gets the namespace this function belongs to, or <see langword="null"/> if it is a standalone deferred function.</summary> | ||
| public string? Namespace { get; } | ||
|
|
||
| /// <summary> | ||
| /// Creates a complete tool list with a <see cref="HostedToolSearchTool"/> and the given functions wrapped as <see cref="SearchableAIFunctionDeclaration"/>. | ||
| /// </summary> | ||
| /// <param name="functions">The functions to include as searchable tools.</param> | ||
| /// <param name="namespace">An optional namespace to group the functions under.</param> | ||
| /// <param name="toolSearchProperties">Any additional properties to pass to the <see cref="HostedToolSearchTool"/>.</param> | ||
| /// <returns>A list of <see cref="AITool"/> instances ready for use in <see cref="ChatOptions.Tools"/>.</returns> | ||
| /// <exception cref="System.ArgumentNullException"><paramref name="functions"/> is <see langword="null"/>.</exception> | ||
| public static IList<AITool> CreateToolSet( | ||
| IEnumerable<AIFunctionDeclaration> functions, | ||
| string? @namespace = null, | ||
| IReadOnlyDictionary<string, object?>? toolSearchProperties = null) | ||
| { | ||
| _ = Throw.IfNull(functions); | ||
|
|
||
| var tools = new List<AITool> { new HostedToolSearchTool(toolSearchProperties) }; | ||
| foreach (var fn in functions) | ||
| { | ||
| tools.Add(new SearchableAIFunctionDeclaration(fn, @namespace)); | ||
| } | ||
|
|
||
| return tools; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Collections.Generic; | ||
|
|
||
|
stephentoub marked this conversation as resolved.
|
||
| namespace Microsoft.Extensions.AI; | ||
|
|
||
| /// <summary>Represents a hosted tool that can be specified to an AI service to enable it to search for and selectively load tool definitions on demand.</summary> | ||
| /// <remarks> | ||
| /// This tool does not itself implement tool search. It is a marker that can be used to inform a service | ||
| /// that tool search should be enabled, reducing token usage by deferring full tool schema loading until the model requests it. | ||
| /// </remarks> | ||
| public class HostedToolSearchTool : AITool | ||
| { | ||
| /// <summary>Any additional properties associated with the tool.</summary> | ||
|
Comment on lines
+13
to
+15
|
||
| private IReadOnlyDictionary<string, object?>? _additionalProperties; | ||
|
|
||
| /// <summary>Initializes a new instance of the <see cref="HostedToolSearchTool"/> class.</summary> | ||
| public HostedToolSearchTool() | ||
| { | ||
| } | ||
|
|
||
| /// <summary>Initializes a new instance of the <see cref="HostedToolSearchTool"/> class.</summary> | ||
| /// <param name="additionalProperties">Any additional properties associated with the tool.</param> | ||
| public HostedToolSearchTool(IReadOnlyDictionary<string, object?>? additionalProperties) | ||
| { | ||
| _additionalProperties = additionalProperties; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public override string Name => "tool_search"; | ||
|
|
||
| /// <inheritdoc /> | ||
| public override IReadOnlyDictionary<string, object?> AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
| using System.Runtime.CompilerServices; | ||
|
|
@@ -17,6 +18,7 @@ | |
| using System.Threading.Tasks; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
| using Microsoft.Shared.Diagnostics; | ||
| using OpenAI; | ||
| using OpenAI.Responses; | ||
|
|
||
| #pragma warning disable S1226 // Method parameters, caught exceptions and foreach variables' initial values should not be ignored | ||
|
|
@@ -709,7 +711,18 @@ private static bool IsStoredOutputDisabled(CreateResponseOptions? options, Respo | |
| return rtat.Tool; | ||
|
|
||
| case AIFunctionDeclaration aiFunction: | ||
| return ToResponseTool(aiFunction, options); | ||
| var functionTool = ToResponseTool(aiFunction, options); | ||
| if (tool.GetService<SearchableAIFunctionDeclaration>() is not null) | ||
| { | ||
| functionTool.Patch.Set("$.defer_loading"u8, "true"u8); | ||
| } | ||
|
Comment on lines
+715
to
+718
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, we can't do anything with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reverted in 0579b05 — |
||
|
|
||
| return functionTool; | ||
|
|
||
| case HostedToolSearchTool: | ||
| // Workaround: The OpenAI .NET SDK doesn't yet expose a ToolSearchTool type. | ||
| // See https://github.com/openai/openai-dotnet/issues/1053 | ||
| return ModelReaderWriter.Read<ResponseTool>(BinaryData.FromString("""{"type": "tool_search"}"""), ModelReaderWriterOptions.Json, OpenAIContext.Default)!; | ||
|
jozkee marked this conversation as resolved.
jozkee marked this conversation as resolved.
|
||
|
|
||
| case HostedWebSearchTool webSearchTool: | ||
| return new WebSearchTool | ||
|
|
@@ -843,6 +856,34 @@ internal static FunctionTool ToResponseTool(AIFunctionDeclaration aiFunction, Ch | |
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Builds a <c>{"type":"namespace"}</c> <see cref="ResponseTool"/> from a name and set of function tools. | ||
| /// The OpenAI .NET SDK doesn't expose a NamespaceTool type, so we construct the JSON manually. | ||
| /// </summary> | ||
| internal static ResponseTool ToNamespaceResponseTool(string name, IEnumerable<FunctionTool> functionTools) | ||
| { | ||
| using var stream = new MemoryStream(); | ||
| using (var writer = new Utf8JsonWriter(stream)) | ||
| { | ||
| writer.WriteStartObject(); | ||
| writer.WriteString("type"u8, "namespace"u8); | ||
| writer.WriteString("name"u8, name); | ||
|
|
||
| writer.WriteStartArray("tools"u8); | ||
| foreach (var functionTool in functionTools) | ||
| { | ||
| var toolData = ModelReaderWriter.Write(functionTool, ModelReaderWriterOptions.Json, OpenAIContext.Default); | ||
| using var doc = JsonDocument.Parse(toolData); | ||
| doc.RootElement.WriteTo(writer); | ||
| } | ||
|
|
||
| writer.WriteEndArray(); | ||
| writer.WriteEndObject(); | ||
| } | ||
|
|
||
| return ModelReaderWriter.Read<ResponseTool>(BinaryData.FromBytes(stream.ToArray()), ModelReaderWriterOptions.Json, OpenAIContext.Default)!; | ||
| } | ||
|
|
||
| /// <summary>Creates a <see cref="ChatRole"/> from a <see cref="MessageRole"/>.</summary> | ||
| private static ChatRole AsChatRole(MessageRole? role) => | ||
| role switch | ||
|
|
@@ -926,14 +967,39 @@ private CreateResponseOptions AsCreateResponseOptions(ChatOptions? options, out | |
| // Populate tools if there are any. | ||
| if (options.Tools is { Count: > 0 } tools) | ||
| { | ||
| // Group SearchableAIFunctionDeclarations that share a namespace into namespace tools. | ||
| Dictionary<string, List<FunctionTool>>? namespaces = null; | ||
| foreach (AITool tool in tools) | ||
| { | ||
| if (tool.GetService<SearchableAIFunctionDeclaration>() is { Namespace: string ns }) | ||
| { | ||
| namespaces ??= new(StringComparer.Ordinal); | ||
| if (!namespaces.TryGetValue(ns, out var entry)) | ||
| { | ||
| entry = new List<FunctionTool>(); | ||
| namespaces[ns] = entry; | ||
| } | ||
|
|
||
| var ft = ToResponseTool((AIFunctionDeclaration)tool, options); | ||
| ft.Patch.Set("$.defer_loading"u8, "true"u8); | ||
| entry.Add(ft); | ||
| continue; | ||
| } | ||
|
|
||
| if (ToResponseTool(tool, options) is { } responseTool) | ||
| { | ||
| result.Tools.Add(responseTool); | ||
| } | ||
| } | ||
|
|
||
| if (namespaces is not null) | ||
| { | ||
| foreach (var kvp in namespaces) | ||
| { | ||
| result.Tools.Add(ToNamespaceResponseTool(kvp.Key, kvp.Value)); | ||
| } | ||
| } | ||
|
|
||
| if (result.Tools.Count > 0) | ||
| { | ||
| result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SearchableAIFunctionDeclaration is described as an experimental API, but it is currently missing an [Experimental(...)] attribute. Without it, the API is treated as Stable (and the generated Abstractions.json reflects that), which is hard to undo later. Mark it [Experimental(DiagnosticIds.Experiments.AIToolSearch, UrlFormat = DiagnosticIds.UrlFormat)] and ensure DiagnosticIds.Experiments.AIToolSearch is defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot revise the PR description. This shouldn't be experimental.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the PR description in 89d8df4 — removed all references to
[Experimental]annotations. The types are stable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description was updated previously to state all types are stable (not experimental). The current description reflects this — no
[Experimental]attributes on the new types.