Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,23 @@ private protected ChatToolMode()
/// </summary>
/// <param name="functionName">The name of the required function.</param>
/// <returns>An instance of <see cref="RequiredChatToolMode"/> for the specified function name.</returns>
/// <remarks>
/// Specifying a <paramref name="functionName"/> in a <see cref="RequiredChatToolMode"/> stored
/// into <see cref="ChatOptions.ToolMode"/> does not automatically include that tool in <see cref="ChatOptions.Tools"/>.
/// The tool must still be provided separately from the <see cref="ChatOptions.ToolMode"/>.
/// </remarks>
public static RequiredChatToolMode RequireSpecific(string functionName) => new(functionName);

/// <summary>
/// Instantiates a <see cref="ChatToolMode"/> indicating that tool usage is required,
/// and that the specified tool must be selected.
/// </summary>
/// <param name="tool">The required tool.</param>
/// <returns>An instance of <see cref="RequiredChatToolMode"/> for the specified tool.</returns>
/// <remarks>
/// Specifying a <paramref name="tool"/> in a <see cref="RequiredChatToolMode"/> stored
/// into <see cref="ChatOptions.ToolMode"/> does not automatically include that tool in <see cref="ChatOptions.Tools"/>.
/// The tool must still be provided separately from the <see cref="ChatOptions.ToolMode"/>.
/// </remarks>
public static RequiredChatToolMode RequireSpecific(AITool tool) => new(tool);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,59 @@

using System;
using System.Diagnostics;
using System.Text.Json.Serialization;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents a mode where a chat tool must be called. This class can optionally nominate a specific function
/// or indicate that any of the functions can be selected.
/// Represents a mode where a chat tool must be called. This class can optionally nominate a specific tool
/// or indicate that any of the tools can be selected.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class RequiredChatToolMode : ChatToolMode
{
/// <summary>
/// Gets the name of a specific tool that must be called.
/// Gets the name of a specific function tool that must be called.
/// </summary>
/// <remarks>
/// If the value is <see langword="null"/>, any available tool can be selected (but at least one must be).
/// If both <see cref="RequiredFunctionName"/> and <see cref="RequiredTool"/> are <see langword="null"/>,
/// any available tool can be selected (but at least one must be).
/// </remarks>
public string? RequiredFunctionName { get; }

/// <summary>Gets the specific tool that must be called.</summary>
/// <remarks>
/// <para>
/// If both <see cref="RequiredFunctionName"/> and <see cref="RequiredTool"/> are <see langword="null"/>,
/// any available tool can be selected (but at least one must be).
/// </para>
/// <para>
/// Note that <see cref="RequiredTool"/> will not serialize to JSON as part of serializing
/// the <see cref="RequiredChatToolMode"/> instance, just as <see cref="ChatOptions.Tools"/> doesn't serialize. As such, attempting to
/// roundtrip a <see cref="RequiredChatToolMode"/> through JSON serialization may lead to the deserialized instance having <see cref="RequiredTool"/>
/// set to <see langword="null"/>.
/// </para>
/// </remarks>
[JsonIgnore]
public AITool? RequiredTool { get; }
Copy link
Member

@eiriktsarpalis eiriktsarpalis Oct 9, 2025

Choose a reason for hiding this comment

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

If we end up roundtripping this value the resultant object is going to have a subtly different equality semantic, and will have impact in how the resultant object gets processed in the OpenAI provider. Is there a way we could potentially preserve its intended representation in such cases?

Copy link
Member Author

Choose a reason for hiding this comment

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

If we end up roundtripping this value the resultant object is going to have a subtly different equality semantic

Will it? Do you just mean it'll no longer be equal to another instance that contains the tool since this one is null? I mean, yeah :) That's not just equality, it's no longer the same requirement. If you set it to an AIFunctionDeclaration, though, it should behave the same, as the name gets extracted.

Is there a way we could potentially preserve its intended representation in such cases?

What did you have in mind?

Copy link
Member

Choose a reason for hiding this comment

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

What did you have in mind?

Nothing specific, just asking if serializing the value could inherently change how it gets handled by the leaf clients.

Copy link
Member Author

Choose a reason for hiding this comment

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

just asking if serializing the value could inherently change how it gets handled by the leaf clients.

It could, yes, as the AITool will be null after deserialization. That's already the case for ChatOptions.Tools, too. So if you serialize and deserialize a ChatOptions, you're going to lose both on the resulting instance.

We could revisit that. We could allow AITool to be serialized, and in most cases we can persist most data. The main (and important) thing we can't is the actual function implementation for AIFunction.

Copy link
Member Author

Choose a reason for hiding this comment

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

@eiriktsarpalis, alternative suggestions or approaches? What do you think about enabling AITools to be serialized, except an AIFunction would roundtrip as an AIFunctionDeclaration (i.e. it would lose its invocation ability)?

Copy link
Member

Choose a reason for hiding this comment

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

The only alternative I can think of is requiring tools by name only.

What do you think about enabling AITools to be serialized, except an AIFunction would roundtrip as an AIFunctionDeclaration (i.e. it would lose its invocation ability)?

Seems reasonable, although that scheme would only work in the context of a JsonConverter<AITool> but not for a JsonConverter<AIFunction> due to the lack of type relationship with AIFunctionDefinition.


/// <summary>
/// Initializes a new instance of the <see cref="RequiredChatToolMode"/> class that requires a specific tool to be called.
/// </summary>
/// <param name="requiredFunctionName">The name of the tool that must be called.</param>
/// <param name="requiredFunctionName">The name of the function that must be called.</param>
/// <exception cref="ArgumentException"><paramref name="requiredFunctionName"/> is empty or composed entirely of whitespace.</exception>
/// <remarks>
/// <para>
/// <paramref name="requiredFunctionName"/> can be <see langword="null"/>. However, it's preferable to use
/// <see cref="ChatToolMode.RequireAny"/> when any function can be selected.
/// </para>
/// <para>
/// The specified tool must also be included in the list of tools provided in the request,
/// such as via <see cref="ChatOptions.Tools"/>.
/// </para>
/// </remarks>
[JsonConstructor]
public RequiredChatToolMode(string? requiredFunctionName)
{
if (requiredFunctionName is not null)
Expand All @@ -41,17 +66,42 @@ public RequiredChatToolMode(string? requiredFunctionName)
RequiredFunctionName = requiredFunctionName;
}

/// <summary>
/// Initializes a new instance of the <see cref="RequiredChatToolMode"/> class that requires a specific tool to be called.
/// </summary>
/// <param name="requiredTool">The specific tool that must be called.</param>
/// <para>
/// <paramref name="requiredTool"/> can be <see langword="null"/>. However, it's preferable to use
/// <see cref="ChatToolMode.RequireAny"/> when any function can be selected.
/// </para>
/// <para>
/// Specifying a <paramref name="requiredTool"/> in a <see cref="RequiredChatToolMode"/> stored
/// into <see cref="ChatOptions.ToolMode"/> does not automatically include that tool in <see cref="ChatOptions.Tools"/>.
/// The tool must still be provided separately from the <see cref="ChatOptions.ToolMode"/>.
/// </para>
public RequiredChatToolMode(AITool? requiredTool)
{
if (requiredTool is not null)
{
RequiredTool = requiredTool;
RequiredFunctionName = requiredTool is AIFunctionDeclaration af ? af.Name : null;
}
}

/// <summary>Gets a string representing this instance to display in the debugger.</summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay => $"Required: {RequiredFunctionName ?? "Any"}";
private string DebuggerDisplay => $"Required: {RequiredFunctionName ?? RequiredTool?.Name ?? "Any"}";

/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is RequiredChatToolMode other &&
RequiredFunctionName == other.RequiredFunctionName;
(RequiredFunctionName is not null || other.RequiredFunctionName is not null ?
RequiredFunctionName == other.RequiredFunctionName :
Equals(RequiredTool, other.RequiredTool));

/// <inheritdoc/>
public override int GetHashCode() =>
RequiredFunctionName?.GetHashCode(StringComparison.Ordinal) ??
RequiredTool?.GetHashCode() ??
typeof(RequiredChatToolMode).GetHashCode();
}
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,10 @@
{
"Member": "static Microsoft.Extensions.AI.RequiredChatToolMode Microsoft.Extensions.AI.ChatToolMode.RequireSpecific(string functionName);",
"Stage": "Stable"
},
{
"Member": "static Microsoft.Extensions.AI.RequiredChatToolMode Microsoft.Extensions.AI.ChatToolMode.RequireSpecific(Microsoft.Extensions.AI.AITool tool);",
"Stage": "Stable"
}
],
"Properties": [
Expand Down Expand Up @@ -2099,6 +2103,10 @@
"Member": "Microsoft.Extensions.AI.RequiredChatToolMode.RequiredChatToolMode(string? requiredFunctionName);",
"Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.RequiredChatToolMode.RequiredChatToolMode(Microsoft.Extensions.AI.AITool? requiredTool);",
"Stage": "Stable"
},
{
"Member": "override bool Microsoft.Extensions.AI.RequiredChatToolMode.Equals(object? obj);",
"Stage": "Stable"
Expand All @@ -2112,6 +2120,10 @@
{
"Member": "string? Microsoft.Extensions.AI.RequiredChatToolMode.RequiredFunctionName { get; }",
"Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.AITool? Microsoft.Extensions.AI.RequiredChatToolMode.RequiredTool { get; }",
"Stage": "Stable"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(
runOptions.ToolConstraint = new ToolConstraint(ToolDefinition.CreateFunction(functionName));
break;

case RequiredChatToolMode required when required.RequiredTool is HostedCodeInterpreterTool:
runOptions.ToolConstraint = new ToolConstraint(ToolDefinition.CreateCodeInterpreter());
break;

case RequiredChatToolMode required when required.RequiredTool is HostedFileSearchTool:
runOptions.ToolConstraint = new ToolConstraint(ToolDefinition.CreateFileSearch());
break;

case RequiredChatToolMode required:
runOptions.ToolConstraint = ToolConstraint.Required;
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
break;

case RequiredChatToolMode required:
result.ToolChoice = required.RequiredFunctionName is not null ?
ResponseToolChoice.CreateFunctionChoice(required.RequiredFunctionName) :
result.ToolChoice =
required.RequiredFunctionName is not null ? ResponseToolChoice.CreateFunctionChoice(required.RequiredFunctionName) :
required.RequiredTool is HostedWebSearchTool || required.RequiredTool is ResponseToolAITool { Tool: WebSearchTool } ? ResponseToolChoice.CreateWebSearchChoice() :
required.RequiredTool is HostedFileSearchTool || required.RequiredTool is ResponseToolAITool { Tool: FileSearchTool } ? ResponseToolChoice.CreateFileSearchChoice() :
ResponseToolChoice.CreateRequiredChoice();
break;
}
Expand Down
Loading
Loading