Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -21,6 +21,13 @@ public static class MicrosoftExtensionsAIResponsesExtensions
public static FunctionTool AsOpenAIResponseTool(this AIFunctionDeclaration function) =>
OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(function));

/// <summary>Creates an OpenAI <see cref="ResponseTool"/> from an <see cref="AITool"/>.</summary>
/// <param name="tool">The tool to convert.</param>
/// <returns>An OpenAI <see cref="ResponseTool"/> representing <paramref name="tool"/> or <see langword="null"/> if there is no mapping.</returns>
/// <exception cref="ArgumentNullException"><paramref name="tool"/> is <see langword="null"/>.</exception>
public static ResponseTool? AsOpenAIResponseTool(this AITool tool)
=> OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(tool));

/// <summary>
/// Creates an OpenAI <see cref="ResponseTextFormat"/> from a <see cref="ChatResponseFormat"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,105 @@ void IDisposable.Dispose()
// Nothing to dispose. Implementation required for the IChatClient interface.
}

internal static ResponseTool? ToResponseTool(AITool tool, ChatOptions? options = null)
{
switch (tool)
{
case ResponseToolAITool rtat:
return rtat.Tool;

case AIFunctionDeclaration aiFunction:
bool? strict =
OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ??
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties);

return ResponseTool.CreateFunctionTool(
aiFunction.Name,
OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict),
strict,
aiFunction.Description);

case HostedWebSearchTool webSearchTool:
WebSearchToolLocation? location = null;
if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation))
{
location = objLocation as WebSearchToolLocation;
}

WebSearchToolContextSize? size = null;
if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) &&
objSize is WebSearchToolContextSize)
{
size = (WebSearchToolContextSize)objSize;
}

return ResponseTool.CreateWebSearchTool(location, size);

case HostedFileSearchTool fileSearchTool:
return ResponseTool.CreateFileSearchTool(
fileSearchTool.Inputs?.OfType<HostedVectorStoreContent>().Select(c => c.VectorStoreId) ?? [],
fileSearchTool.MaximumResultCount);

case HostedCodeInterpreterTool codeTool:
return ResponseTool.CreateCodeInterpreterTool(
new CodeInterpreterToolContainer(codeTool.Inputs?.OfType<HostedFileContent>().Select(f => f.FileId).ToList() is { Count: > 0 } ids ?
CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) :
new()));

case HostedMcpServerTool mcpTool:
McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? url) ?
ResponseTool.CreateMcpTool(
mcpTool.ServerName,
url,
mcpTool.AuthorizationToken,
mcpTool.ServerDescription) :
ResponseTool.CreateMcpTool(
mcpTool.ServerName,
new McpToolConnectorId(mcpTool.ServerAddress),
mcpTool.AuthorizationToken,
mcpTool.ServerDescription);

if (mcpTool.AllowedTools is not null)
{
responsesMcpTool.AllowedTools = new();
AddAllMcpFilters(mcpTool.AllowedTools, responsesMcpTool.AllowedTools);
}

switch (mcpTool.ApprovalMode)
{
case HostedMcpServerToolAlwaysRequireApprovalMode:
responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval);
break;

case HostedMcpServerToolNeverRequireApprovalMode:
responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval);
break;

case HostedMcpServerToolRequireSpecificApprovalMode specificMode:
responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(new CustomMcpToolCallApprovalPolicy());

if (specificMode.AlwaysRequireApprovalToolNames is { Count: > 0 } alwaysRequireToolNames)
{
responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval = new();
AddAllMcpFilters(alwaysRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval);
}

if (specificMode.NeverRequireApprovalToolNames is { Count: > 0 } neverRequireToolNames)
{
responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval = new();
AddAllMcpFilters(neverRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval);
}

break;
}

return responsesMcpTool;

default:
return null;
}
}

internal static FunctionTool ToResponseTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null)
{
bool? strict =
Expand Down Expand Up @@ -492,96 +591,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
{
foreach (AITool tool in tools)
{
switch (tool)
var responseTool = ToResponseTool(tool, options);
if (responseTool is not null)
{
case ResponseToolAITool rtat:
result.Tools.Add(rtat.Tool);
break;

case AIFunctionDeclaration aiFunction:
result.Tools.Add(ToResponseTool(aiFunction, options));
break;

case HostedWebSearchTool webSearchTool:
WebSearchToolLocation? location = null;
if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation))
{
location = objLocation as WebSearchToolLocation;
}

WebSearchToolContextSize? size = null;
if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) &&
objSize is WebSearchToolContextSize)
{
size = (WebSearchToolContextSize)objSize;
}

result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size));
break;

case HostedFileSearchTool fileSearchTool:
result.Tools.Add(ResponseTool.CreateFileSearchTool(
fileSearchTool.Inputs?.OfType<HostedVectorStoreContent>().Select(c => c.VectorStoreId) ?? [],
fileSearchTool.MaximumResultCount));
break;

case HostedCodeInterpreterTool codeTool:
result.Tools.Add(
ResponseTool.CreateCodeInterpreterTool(
new CodeInterpreterToolContainer(codeTool.Inputs?.OfType<HostedFileContent>().Select(f => f.FileId).ToList() is { Count: > 0 } ids ?
CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) :
new())));
break;

case HostedMcpServerTool mcpTool:
McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? url) ?
ResponseTool.CreateMcpTool(
mcpTool.ServerName,
url,
mcpTool.AuthorizationToken,
mcpTool.ServerDescription) :
ResponseTool.CreateMcpTool(
mcpTool.ServerName,
new McpToolConnectorId(mcpTool.ServerAddress),
mcpTool.AuthorizationToken,
mcpTool.ServerDescription);

if (mcpTool.AllowedTools is not null)
{
responsesMcpTool.AllowedTools = new();
AddAllMcpFilters(mcpTool.AllowedTools, responsesMcpTool.AllowedTools);
}

switch (mcpTool.ApprovalMode)
{
case HostedMcpServerToolAlwaysRequireApprovalMode:
responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval);
break;

case HostedMcpServerToolNeverRequireApprovalMode:
responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval);
break;

case HostedMcpServerToolRequireSpecificApprovalMode specificMode:
responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(new CustomMcpToolCallApprovalPolicy());

if (specificMode.AlwaysRequireApprovalToolNames is { Count: > 0 } alwaysRequireToolNames)
{
responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval = new();
AddAllMcpFilters(alwaysRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval);
}

if (specificMode.NeverRequireApprovalToolNames is { Count: > 0 } neverRequireToolNames)
{
responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval = new();
AddAllMcpFilters(neverRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval);
}

break;
}

result.Tools.Add(responsesMcpTool);
break;
result.Tools.Add(responseTool);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,168 @@ public void AsOpenAIResponseTool_ProducesValidInstance()
Assert.NotNull(tool);
}

[Fact]
public void AsOpenAIResponseTool_WithAIFunctionDeclaration_ProducesValidFunctionTool()
{
var tool = _testFunction.AsOpenAIResponseTool();

Assert.NotNull(tool);
Assert.IsType<FunctionTool>(tool);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedWebSearchTool_ProducesValidWebSearchTool()
{
var webSearchTool = new HostedWebSearchTool();

var result = webSearchTool.AsOpenAIResponseTool();

Assert.NotNull(result);
Assert.IsType<WebSearchTool>(result);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedFileSearchTool_ProducesValidFileSearchTool()
{
var fileSearchTool = new HostedFileSearchTool();

var result = fileSearchTool.AsOpenAIResponseTool();

Assert.NotNull(result);
Assert.IsType<FileSearchTool>(result);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedFileSearchToolWithVectorStores_ProducesValidFileSearchTool()
{
var vectorStoreContent = new HostedVectorStoreContent("vector-store-123");
var fileSearchTool = new HostedFileSearchTool
{
Inputs = [vectorStoreContent]
};

var result = fileSearchTool.AsOpenAIResponseTool();

Assert.NotNull(result);
var tool = Assert.IsType<FileSearchTool>(result);
Assert.Single(tool.VectorStoreIds);
Assert.Equal(vectorStoreContent.VectorStoreId, tool.VectorStoreIds[0]);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedFileSearchToolWithMaxResults_ProducesValidFileSearchTool()
{
var fileSearchTool = new HostedFileSearchTool
{
MaximumResultCount = 10
};

var result = fileSearchTool.AsOpenAIResponseTool();

Assert.NotNull(result);
var tool = Assert.IsType<FileSearchTool>(result);
Assert.Equal(10, tool.MaxResultCount);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedCodeInterpreterTool_ProducesValidCodeInterpreterTool()
{
var codeTool = new HostedCodeInterpreterTool();

var result = codeTool.AsOpenAIResponseTool();

Assert.NotNull(result);
var tool = Assert.IsType<CodeInterpreterTool>(result);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedCodeInterpreterToolWithFiles_ProducesValidCodeInterpreterTool()
{
var fileContent = new HostedFileContent("file-123");
var codeTool = new HostedCodeInterpreterTool
{
Inputs = [fileContent]
};

var result = codeTool.AsOpenAIResponseTool();

Assert.NotNull(result);
var tool = Assert.IsType<CodeInterpreterTool>(result);
var autoContainerConfig = Assert.IsType<AutomaticCodeInterpreterToolContainerConfiguration>(tool.Container.ContainerConfiguration);
Assert.Single(autoContainerConfig.FileIds);
Assert.Equal(fileContent.FileId, autoContainerConfig.FileIds[0]);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedMcpServerTool_ProducesValidMcpTool()
{
var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000");

var result = mcpTool.AsOpenAIResponseTool();

Assert.NotNull(result);
var tool = Assert.IsType<McpTool>(result);
Assert.Equal(new Uri("http://localhost:8000"), tool.ServerUri);
Assert.Equal("test-server", tool.ServerLabel);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedMcpServerToolWithDescription_ProducesValidMcpTool()
{
var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000")
{
ServerDescription = "A test MCP server"
};

var result = mcpTool.AsOpenAIResponseTool();

Assert.NotNull(result);
var tool = Assert.IsType<McpTool>(result);
Assert.Equal("A test MCP server", tool.ServerDescription);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthToken_ProducesValidMcpTool()
{
var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000")
{
AuthorizationToken = "test-token"
};

var result = mcpTool.AsOpenAIResponseTool();

Assert.NotNull(result);
var tool = Assert.IsType<McpTool>(result);
Assert.Equal("test-token", tool.AuthorizationToken);
}

[Fact]
public void AsOpenAIResponseTool_WithHostedMcpServerToolWithUri_ProducesValidMcpTool()
{
var mcpTool = new HostedMcpServerTool("test-server", new Uri("http://localhost:8000"));

var result = mcpTool.AsOpenAIResponseTool();

Assert.NotNull(result);
Assert.IsType<McpTool>(result);
}

[Fact]
public void AsOpenAIResponseTool_WithUnknownToolType_ReturnsNull()
{
var unknownTool = new UnknownAITool();

var result = unknownTool.AsOpenAIResponseTool();

Assert.Null(result);
}

[Fact]
public void AsOpenAIResponseTool_WithNullTool_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>("tool", () => ((AITool)null!).AsOpenAIResponseTool());
}

[Fact]
public void AsOpenAIConversationFunctionTool_ProducesValidInstance()
{
Expand Down Expand Up @@ -1256,4 +1418,10 @@ private static async IAsyncEnumerable<T> CreateAsyncEnumerable<T>(IEnumerable<T>
}

private static string RemoveWhitespace(string input) => Regex.Replace(input, @"\s+", "");

/// <summary>Helper class for testing unknown tool types.</summary>
private sealed class UnknownAITool : AITool
{
public override string Name => "unknown_tool";
}
}
Loading