Skip to content
Merged
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 @@ -4,6 +4,7 @@

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

## 9.9.0

Expand Down
3 changes: 2 additions & 1 deletion src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## NOT YET RELEASED

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

## 9.9.0-preview.1.25458.4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,46 @@ public static OpenAIResponse AsOpenAIResponse(this ChatResponse response, ChatOp
previousResponseId: options?.ConversationId,
instructions: options?.Instructions);
}

/// <summary>Adds the <see cref="ResponseTool"/> to the list of <see cref="AITool"/>s.</summary>
/// <param name="tools">The list of <see cref="AITool"/>s to which the provided tool should be added.</param>
/// <param name="tool">The <see cref="ResponseTool"/> to add.</param>
/// <remarks>
/// <see cref="ResponseTool"/> does not derive from <see cref="AITool"/>, so it cannot be added directly to a list of <see cref="AITool"/>s.
/// Instead, this method wraps the provided <see cref="ResponseTool"/> in an <see cref="AITool"/> and adds that to the list.
/// The <see cref="IChatClient"/> returned by <see cref="OpenAIClientExtensions.AsIChatClient(OpenAIResponseClient)"/> will
/// be able to unwrap the <see cref="ResponseTool"/> when it processes the list of tools and use the provided <paramref name="tool"/> as-is.
/// </remarks>
public static void Add(this IList<AITool> tools, ResponseTool tool)
{
_ = Throw.IfNull(tools);

tools.Add(AsAITool(tool));
}

/// <summary>Creates an <see cref="AITool"/> to represent a raw <see cref="ResponseTool"/>.</summary>
/// <param name="tool">The tool to wrap as an <see cref="AITool"/>.</param>
/// <returns>The <paramref name="tool"/> wrapped as an <see cref="AITool"/>.</returns>
/// <remarks>
/// <para>
/// The returned tool is only suitable for use with the <see cref="IChatClient"/> returned by
/// <see cref="OpenAIClientExtensions.AsIChatClient(OpenAIResponseClient)"/> (or <see cref="IChatClient"/>s that delegate
/// to such an instance). It is likely to be ignored by any other <see cref="IChatClient"/> implementation.
/// </para>
/// <para>
/// When a tool has a corresponding <see cref="AITool"/>-derived type already defined in Microsoft.Extensions.AI,
/// such as <see cref="AIFunction"/>, <see cref="HostedWebSearchTool"/>, <see cref="HostedMcpServerTool"/>, or
/// <see cref="HostedFileSearchTool"/>, those types should be preferred instead of this method, as they are more portable,
/// capable of being respected by any <see cref="IChatClient"/> implementation. This method does not attempt to
/// map the supplied <see cref="ResponseTool"/> to any of those types, it simply wraps it as-is:
/// the <see cref="IChatClient"/> returned by <see cref="OpenAIClientExtensions.AsIChatClient(OpenAIResponseClient)"/> will
/// be able to unwrap the <see cref="ResponseTool"/> when it processes the list of tools.
/// </para>
/// </remarks>
public static AITool AsAITool(this ResponseTool tool)
{
_ = Throw.IfNull(tool);

return new OpenAIResponsesChatClient.ResponseToolAITool(tool);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
{
switch (tool)
{
case ResponseToolAITool rtat:
result.Tools.Add(rtat.Tool);
break;

case AIFunctionDeclaration aiFunction:
result.Tools.Add(ToResponseTool(aiFunction, options));
break;
Expand Down Expand Up @@ -877,4 +881,11 @@ private static void AddAllMcpFilters(IList<string> toolNames, McpToolFilter filt
filter.ToolNames.Add(toolName);
}
}

/// <summary>Provides an <see cref="AITool"/> wrapper for a <see cref="ResponseTool"/>.</summary>
internal sealed class ResponseToolAITool(ResponseTool tool) : AITool
{
public ResponseTool Tool => tool;
public override string Name => Tool.GetType().Name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ public async Task AsChatResponse_ConvertsOpenAIStreamingChatCompletionUpdates()
updates.Add(update);
}

ChatResponse response = updates.ToChatResponse();
var response = updates.ToChatResponse();

Assert.Equal("id", response.ResponseId);
Assert.Equal(ChatFinishReason.ToolCalls, response.FinishReason);
Expand Down Expand Up @@ -1176,6 +1176,31 @@ public void AsOpenAIResponse_WithBothModelIds_PrefersChatResponseModelId()
Assert.Equal("response-model-id", openAIResponse.Model);
}

[Fact]
public void ListAddResponseTool_AddsToolCorrectly()
{
Assert.Throws<ArgumentNullException>("tools", () => ((IList<AITool>)null!).Add(ResponseTool.CreateWebSearchTool()));
Assert.Throws<ArgumentNullException>("tool", () => new List<AITool>().Add((ResponseTool)null!));

Assert.Throws<ArgumentNullException>("tool", () => ((ResponseTool)null!).AsAITool());

ChatOptions options;

options = new()
{
Tools = new List<AITool> { ResponseTool.CreateWebSearchTool() },
};
Assert.Single(options.Tools);
Assert.NotNull(options.Tools[0]);

options = new()
{
Tools = [ResponseTool.CreateWebSearchTool().AsAITool()],
};
Assert.Single(options.Tools);
Assert.NotNull(options.Tools[0]);
}

private static async IAsyncEnumerable<T> CreateAsyncEnumerable<T>(IEnumerable<T> source)
{
foreach (var item in source)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,10 @@ public async Task MultipleOutputItems_NonStreaming()
Assert.Equal(36, response.Usage.TotalTokenCount);
}

[Fact]
public async Task McpToolCall_ApprovalNotRequired_NonStreaming()
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
{
const string Input = """
{
Expand Down Expand Up @@ -1031,13 +1033,16 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming()
using HttpClient httpClient = new(handler);
using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");

AITool mcpTool = rawTool ?
ResponseTool.CreateMcpTool("deepwiki", new("https://mcp.deepwiki.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() :
new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
{
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
};

ChatOptions chatOptions = new()
{
Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp")
{
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire,
}
],
Tools = [mcpTool],
};

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);
Expand Down
Loading