Skip to content

Commit cdb9ce4

Browse files
authored
Add IList<AITool>.Add(ResponseTool) extension method (#6813)
For someone that knows they're using an OpenAI Responses IChatClient, they can Add a ResponseTool directly into ChatOptions.Tools, rather than needing to go through RawRepresentationFactory.
1 parent fdcaa2f commit cdb9ce4

File tree

6 files changed

+94
-9
lines changed

6 files changed

+94
-9
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type.
66
- Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content.
7+
- Fixed `MinLength`/`MaxLength`/`Length` attribute mapping in nullable string properties during schema export.
78

89
## 9.9.0
910

src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## NOT YET RELEASED
44

5-
- Added M.E.AI to OpenAI conversions for response format types
5+
- Added M.E.AI to OpenAI conversions for response format types.
6+
- Added `ResponseTool` to `AITool` conversions.
67

78
## 9.9.0-preview.1.25458.4
89

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,46 @@ public static OpenAIResponse AsOpenAIResponse(this ChatResponse response, ChatOp
9393
previousResponseId: options?.ConversationId,
9494
instructions: options?.Instructions);
9595
}
96+
97+
/// <summary>Adds the <see cref="ResponseTool"/> to the list of <see cref="AITool"/>s.</summary>
98+
/// <param name="tools">The list of <see cref="AITool"/>s to which the provided tool should be added.</param>
99+
/// <param name="tool">The <see cref="ResponseTool"/> to add.</param>
100+
/// <remarks>
101+
/// <see cref="ResponseTool"/> does not derive from <see cref="AITool"/>, so it cannot be added directly to a list of <see cref="AITool"/>s.
102+
/// Instead, this method wraps the provided <see cref="ResponseTool"/> in an <see cref="AITool"/> and adds that to the list.
103+
/// The <see cref="IChatClient"/> returned by <see cref="OpenAIClientExtensions.AsIChatClient(OpenAIResponseClient)"/> will
104+
/// be able to unwrap the <see cref="ResponseTool"/> when it processes the list of tools and use the provided <paramref name="tool"/> as-is.
105+
/// </remarks>
106+
public static void Add(this IList<AITool> tools, ResponseTool tool)
107+
{
108+
_ = Throw.IfNull(tools);
109+
110+
tools.Add(AsAITool(tool));
111+
}
112+
113+
/// <summary>Creates an <see cref="AITool"/> to represent a raw <see cref="ResponseTool"/>.</summary>
114+
/// <param name="tool">The tool to wrap as an <see cref="AITool"/>.</param>
115+
/// <returns>The <paramref name="tool"/> wrapped as an <see cref="AITool"/>.</returns>
116+
/// <remarks>
117+
/// <para>
118+
/// The returned tool is only suitable for use with the <see cref="IChatClient"/> returned by
119+
/// <see cref="OpenAIClientExtensions.AsIChatClient(OpenAIResponseClient)"/> (or <see cref="IChatClient"/>s that delegate
120+
/// to such an instance). It is likely to be ignored by any other <see cref="IChatClient"/> implementation.
121+
/// </para>
122+
/// <para>
123+
/// When a tool has a corresponding <see cref="AITool"/>-derived type already defined in Microsoft.Extensions.AI,
124+
/// such as <see cref="AIFunction"/>, <see cref="HostedWebSearchTool"/>, <see cref="HostedMcpServerTool"/>, or
125+
/// <see cref="HostedFileSearchTool"/>, those types should be preferred instead of this method, as they are more portable,
126+
/// capable of being respected by any <see cref="IChatClient"/> implementation. This method does not attempt to
127+
/// map the supplied <see cref="ResponseTool"/> to any of those types, it simply wraps it as-is:
128+
/// the <see cref="IChatClient"/> returned by <see cref="OpenAIClientExtensions.AsIChatClient(OpenAIResponseClient)"/> will
129+
/// be able to unwrap the <see cref="ResponseTool"/> when it processes the list of tools.
130+
/// </para>
131+
/// </remarks>
132+
public static AITool AsAITool(this ResponseTool tool)
133+
{
134+
_ = Throw.IfNull(tool);
135+
136+
return new OpenAIResponsesChatClient.ResponseToolAITool(tool);
137+
}
96138
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
414414
{
415415
switch (tool)
416416
{
417+
case ResponseToolAITool rtat:
418+
result.Tools.Add(rtat.Tool);
419+
break;
420+
417421
case AIFunctionDeclaration aiFunction:
418422
result.Tools.Add(ToResponseTool(aiFunction, options));
419423
break;
@@ -877,4 +881,11 @@ private static void AddAllMcpFilters(IList<string> toolNames, McpToolFilter filt
877881
filter.ToolNames.Add(toolName);
878882
}
879883
}
884+
885+
/// <summary>Provides an <see cref="AITool"/> wrapper for a <see cref="ResponseTool"/>.</summary>
886+
internal sealed class ResponseToolAITool(ResponseTool tool) : AITool
887+
{
888+
public ResponseTool Tool => tool;
889+
public override string Name => Tool.GetType().Name;
890+
}
880891
}

test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ public async Task AsChatResponse_ConvertsOpenAIStreamingChatCompletionUpdates()
316316
updates.Add(update);
317317
}
318318

319-
ChatResponse response = updates.ToChatResponse();
319+
var response = updates.ToChatResponse();
320320

321321
Assert.Equal("id", response.ResponseId);
322322
Assert.Equal(ChatFinishReason.ToolCalls, response.FinishReason);
@@ -1176,6 +1176,31 @@ public void AsOpenAIResponse_WithBothModelIds_PrefersChatResponseModelId()
11761176
Assert.Equal("response-model-id", openAIResponse.Model);
11771177
}
11781178

1179+
[Fact]
1180+
public void ListAddResponseTool_AddsToolCorrectly()
1181+
{
1182+
Assert.Throws<ArgumentNullException>("tools", () => ((IList<AITool>)null!).Add(ResponseTool.CreateWebSearchTool()));
1183+
Assert.Throws<ArgumentNullException>("tool", () => new List<AITool>().Add((ResponseTool)null!));
1184+
1185+
Assert.Throws<ArgumentNullException>("tool", () => ((ResponseTool)null!).AsAITool());
1186+
1187+
ChatOptions options;
1188+
1189+
options = new()
1190+
{
1191+
Tools = new List<AITool> { ResponseTool.CreateWebSearchTool() },
1192+
};
1193+
Assert.Single(options.Tools);
1194+
Assert.NotNull(options.Tools[0]);
1195+
1196+
options = new()
1197+
{
1198+
Tools = [ResponseTool.CreateWebSearchTool().AsAITool()],
1199+
};
1200+
Assert.Single(options.Tools);
1201+
Assert.NotNull(options.Tools[0]);
1202+
}
1203+
11791204
private static async IAsyncEnumerable<T> CreateAsyncEnumerable<T>(IEnumerable<T> source)
11801205
{
11811206
foreach (var item in source)

test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -825,8 +825,10 @@ public async Task MultipleOutputItems_NonStreaming()
825825
Assert.Equal(36, response.Usage.TotalTokenCount);
826826
}
827827

828-
[Fact]
829-
public async Task McpToolCall_ApprovalNotRequired_NonStreaming()
828+
[Theory]
829+
[InlineData(false)]
830+
[InlineData(true)]
831+
public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
830832
{
831833
const string Input = """
832834
{
@@ -1031,13 +1033,16 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming()
10311033
using HttpClient httpClient = new(handler);
10321034
using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
10331035

1036+
AITool mcpTool = rawTool ?
1037+
ResponseTool.CreateMcpTool("deepwiki", new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() :
1038+
new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
1039+
{
1040+
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
1041+
};
1042+
10341043
ChatOptions chatOptions = new()
10351044
{
1036-
Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
1037-
{
1038-
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
1039-
}
1040-
],
1045+
Tools = [mcpTool],
10411046
};
10421047

10431048
var response = await client.GetResponseAsync("Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions);

0 commit comments

Comments
 (0)