mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: dotnet: Add server-side Foundry Toolbox support and fix SDK beta.4 br… (#5450)
* dotnet: Add server-side Foundry Toolbox support and fix SDK beta.4 breaking changes Add FoundryToolbox and AIProjectClient extensions to Microsoft.Agents.AI.Foundry.Hosting for server-side toolbox tool integration matching Python's FoundryChatClient.get_toolbox() pattern. Tools are fetched from the Foundry project SDK and passed as server-side tools in the Responses API request. New files: - FoundryToolbox.cs: Core implementation using AgentAdministrationClient SDK - AIProjectClientToolboxExtensions.cs: Extension methods on AIProjectClient - Agent_Step25_ToolboxServerSideTools sample with create helper and combine flow - 19 unit tests covering param validation, conversion, sanitization, and extensions SDK breaking changes (Azure.AI.AgentServer.Responses beta.3 -> beta.4): - FunctionToolCallOutputResource renamed to OutputItemFunctionToolCallOutput - AzureAIAgentServerResponsesModelFactory made internal, replaced with direct constructors - ResponseUsage constructor now requires non-null token details parameters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: reuse endpoint variable in CreateSampleToolboxAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: pass endpoint through static local functions to avoid capture Static local functions cannot capture top-level variables. Thread the endpoint parameter through Main, CombineToolboxes, and CreateSampleToolboxAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: remove unused projectClient param from CreateSampleToolboxAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Program.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> * Update dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Program.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> * Removing GetToolbocVersion. * Removing tests for GetToolboxVersion * fix: map cached/reasoning token counts in ConvertUsage instead of hardcoding zeros Extract InputTokenDetails.CachedTokenCount and OutputTokenDetails.ReasoningTokenCount from UsageDetails.AdditionalCounts, matching the pattern in AgentResponseExtensions. Also accumulate detail counts when merging with existing usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
0dbcc9fe9d
commit
5fe8941ff9
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Foundry.Hosting;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
#pragma warning disable OPENAI001
|
||||
#pragma warning disable AAIP001 // AgentToolboxes is experimental in Azure.AI.Projects.Agents
|
||||
|
||||
namespace Azure.AI.Projects;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods on <see cref="AIProjectClient"/> for fetching
|
||||
/// Foundry toolbox definitions as server-side tools.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These extensions mirror Python's <c>FoundryChatClient.get_toolbox()</c> pattern,
|
||||
/// allowing a single call on the project client to retrieve tools ready for use
|
||||
/// with <c>AsAIAgent(model, instructions, tools: ...)</c>.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class AIProjectClientToolboxExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches a toolbox from the Foundry project and returns its tools as <see cref="AITool"/> instances
|
||||
/// ready for use as server-side tools in the Responses API.
|
||||
/// </summary>
|
||||
/// <param name="projectClient">The <see cref="AIProjectClient"/> to use. Cannot be <see langword="null"/>.</param>
|
||||
/// <param name="name">The name of the toolbox to fetch.</param>
|
||||
/// <param name="version">
|
||||
/// The specific toolbox version to fetch. When <see langword="null"/>, the toolbox's
|
||||
/// default version is resolved automatically.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <returns>A read-only list of <see cref="AITool"/> instances from the toolbox.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">
|
||||
/// Thrown when <paramref name="projectClient"/> or <paramref name="name"/> is <see langword="null"/>.
|
||||
/// </exception>
|
||||
public static async Task<IReadOnlyList<AITool>> GetToolboxToolsAsync(
|
||||
this AIProjectClient projectClient,
|
||||
string name,
|
||||
string? version = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(projectClient);
|
||||
Throw.IfNullOrWhitespace(name);
|
||||
|
||||
var toolboxClient = projectClient.AgentAdministrationClient.GetAgentToolboxes();
|
||||
var toolboxVersion = await FoundryToolbox.GetToolboxVersionCoreAsync(toolboxClient, name, version, cancellationToken).ConfigureAwait(false);
|
||||
return toolboxVersion.ToAITools();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Responses;
|
||||
|
||||
#pragma warning disable OPENAI001
|
||||
#pragma warning disable AAIP001 // AgentToolboxes is experimental in Azure.AI.Projects.Agents
|
||||
#pragma warning disable IL2026 // ModelReaderWriter.Read<ResponseTool> uses reflection; suppressed for Azure SDK model types.
|
||||
#pragma warning disable IL3050 // ModelReaderWriter.Read<ResponseTool> requires dynamic code; suppressed for Azure SDK model types.
|
||||
|
||||
namespace Microsoft.Agents.AI.Foundry.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for fetching Foundry toolbox definitions and converting their tools
|
||||
/// to <see cref="AITool"/> instances for use as server-side tools in the Responses API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When tools from a toolbox are passed to a Foundry agent (e.g. via <c>AsAIAgent(model, instructions, tools: ...)</c>),
|
||||
/// they are sent as server-side tool definitions in the Responses API request. The Foundry platform
|
||||
/// handles tool execution — the agent process does not invoke tools locally.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is the dotnet equivalent of Python's <c>FoundryChatClient.get_toolbox()</c> pattern.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class FoundryToolbox
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches a toolbox version from the Foundry project and returns the raw SDK <see cref="ToolboxVersion"/>.
|
||||
/// </summary>
|
||||
/// <param name="projectEndpoint">The Foundry project endpoint URI.</param>
|
||||
/// <param name="credential">The authentication credential used to access the Foundry project.</param>
|
||||
/// <param name="name">The name of the toolbox to fetch.</param>
|
||||
/// <param name="version">
|
||||
/// The specific toolbox version to fetch. When <see langword="null"/>, the toolbox's
|
||||
/// default version is resolved automatically (requires an additional API call).
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <returns>The <see cref="ToolboxVersion"/> containing tool definitions.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="projectEndpoint"/>, <paramref name="credential"/>, or <paramref name="name"/> is <see langword="null"/>.
|
||||
/// </exception>
|
||||
/// <exception cref="ClientResultException">Thrown when the Foundry project API returns an error.</exception>
|
||||
public static async Task<ToolboxVersion> GetToolboxVersionAsync(
|
||||
Uri projectEndpoint,
|
||||
AuthenticationTokenProvider credential,
|
||||
string name,
|
||||
string? version = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(projectEndpoint);
|
||||
Throw.IfNull(credential);
|
||||
Throw.IfNullOrWhitespace(name);
|
||||
|
||||
var toolboxClient = CreateToolboxClient(projectEndpoint, credential);
|
||||
return await GetToolboxVersionCoreAsync(toolboxClient, name, version, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a toolbox from the Foundry project and returns its tools as <see cref="AITool"/> instances
|
||||
/// ready for use as server-side tools in the Responses API.
|
||||
/// </summary>
|
||||
/// <param name="projectEndpoint">The Foundry project endpoint URI.</param>
|
||||
/// <param name="credential">The authentication credential used to access the Foundry project.</param>
|
||||
/// <param name="name">The name of the toolbox to fetch.</param>
|
||||
/// <param name="version">
|
||||
/// The specific toolbox version to fetch. When <see langword="null"/>, the toolbox's
|
||||
/// default version is resolved automatically.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <returns>A read-only list of <see cref="AITool"/> instances from the toolbox.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="projectEndpoint"/>, <paramref name="credential"/>, or <paramref name="name"/> is <see langword="null"/>.
|
||||
/// </exception>
|
||||
/// <exception cref="ClientResultException">Thrown when the Foundry project API returns an error.</exception>
|
||||
public static async Task<IReadOnlyList<AITool>> GetToolsAsync(
|
||||
Uri projectEndpoint,
|
||||
AuthenticationTokenProvider credential,
|
||||
string name,
|
||||
string? version = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var toolboxVersion = await GetToolboxVersionAsync(projectEndpoint, credential, name, version, cancellationToken).ConfigureAwait(false);
|
||||
return toolboxVersion.ToAITools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the tools in a <see cref="ToolboxVersion"/> to <see cref="AITool"/> instances
|
||||
/// suitable for use as server-side tools in the Responses API.
|
||||
/// </summary>
|
||||
/// <param name="toolboxVersion">The toolbox version whose tools to convert.</param>
|
||||
/// <returns>A read-only list of <see cref="AITool"/> instances.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="toolboxVersion"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Each <see cref="ProjectsAgentTool"/> in the toolbox is cast to <see cref="ResponseTool"/>
|
||||
/// and converted via <c>AsAITool()</c>. Non-function hosted tools (MCP, web_search,
|
||||
/// code_interpreter, etc.) are included as server-side tool definitions — the Foundry
|
||||
/// platform handles their execution.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Non-function tools are sanitized to remove decoration fields (<c>name</c>, <c>description</c>)
|
||||
/// that the toolbox API returns but the Responses API rejects.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static IReadOnlyList<AITool> ToAITools(this ToolboxVersion toolboxVersion)
|
||||
{
|
||||
Throw.IfNull(toolboxVersion);
|
||||
|
||||
if (toolboxVersion.Tools?.Any() != true)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return toolboxVersion.Tools
|
||||
.Select(SanitizeAndConvert)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
#region Internal helpers (visible to unit tests via InternalsVisibleTo)
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a <see cref="ProjectsAgentTool"/> by removing decoration fields that the
|
||||
/// toolbox API returns but the Responses API rejects, then converts to <see cref="AITool"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Azure AI Projects toolbox API may return <c>name</c> and <c>description</c> on
|
||||
/// hosted tool objects (MCP, code_interpreter, file_search, etc.). The Responses API
|
||||
/// rejects at least <c>name</c> with "Unknown parameter: 'tools[0].name'". We strip
|
||||
/// these decoration fields for non-function tools. Function tools keep them since
|
||||
/// <c>name</c> and <c>description</c> are expected parts of the function schema.
|
||||
/// </remarks>
|
||||
internal static AITool SanitizeAndConvert(ProjectsAgentTool tool)
|
||||
{
|
||||
var toolJson = ModelReaderWriter.Write(tool, new ModelReaderWriterOptions("J"));
|
||||
var node = JsonNode.Parse(toolJson.ToString());
|
||||
if (node is not JsonObject obj)
|
||||
{
|
||||
return ((ResponseTool)tool).AsAITool();
|
||||
}
|
||||
|
||||
var toolType = obj["type"]?.GetValue<string>();
|
||||
|
||||
// Function tools need name/description — don't strip
|
||||
if (toolType is "function" or "custom")
|
||||
{
|
||||
return ((ResponseTool)tool).AsAITool();
|
||||
}
|
||||
|
||||
// Strip decoration fields that the Responses API rejects
|
||||
bool modified = false;
|
||||
modified |= obj.Remove("name");
|
||||
modified |= obj.Remove("description");
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
return ((ResponseTool)tool).AsAITool();
|
||||
}
|
||||
|
||||
var sanitizedJson = obj.ToJsonString();
|
||||
var sanitizedTool = ModelReaderWriter.Read<ResponseTool>(BinaryData.FromString(sanitizedJson))!;
|
||||
return sanitizedTool.AsAITool();
|
||||
}
|
||||
|
||||
internal static async Task<ToolboxVersion> GetToolboxVersionAsync(
|
||||
Uri projectEndpoint,
|
||||
AuthenticationTokenProvider credential,
|
||||
string name,
|
||||
string? version,
|
||||
AgentAdministrationClientOptions? clientOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Throw.IfNull(projectEndpoint);
|
||||
Throw.IfNull(credential);
|
||||
Throw.IfNullOrWhitespace(name);
|
||||
|
||||
var toolboxClient = CreateToolboxClient(projectEndpoint, credential, clientOptions);
|
||||
return await GetToolboxVersionCoreAsync(toolboxClient, name, version, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static AgentToolboxes CreateToolboxClient(
|
||||
Uri projectEndpoint,
|
||||
AuthenticationTokenProvider credential,
|
||||
AgentAdministrationClientOptions? clientOptions = null)
|
||||
{
|
||||
clientOptions ??= new AgentAdministrationClientOptions();
|
||||
var adminClient = new AgentAdministrationClient(projectEndpoint, credential, clientOptions);
|
||||
return adminClient.GetAgentToolboxes();
|
||||
}
|
||||
|
||||
internal static async Task<ToolboxVersion> GetToolboxVersionCoreAsync(
|
||||
AgentToolboxes toolboxClient,
|
||||
string name,
|
||||
string? version,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (version is null)
|
||||
{
|
||||
var record = await toolboxClient.GetToolboxAsync(name, cancellationToken).ConfigureAwait(false);
|
||||
version = record.Value.DefaultVersion
|
||||
?? throw new InvalidOperationException($"Toolbox '{name}' does not have a default version. Specify an explicit version.");
|
||||
}
|
||||
|
||||
var result = await toolboxClient.GetToolboxVersionAsync(name, version, cancellationToken).ConfigureAwait(false);
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -237,7 +237,7 @@ internal static class InputConverter
|
||||
{
|
||||
OutputItemMessage msg => ConvertOutputItemMessageToChat(msg),
|
||||
OutputItemFunctionToolCall funcCall => ConvertOutputItemFunctionCall(funcCall),
|
||||
FunctionToolCallOutputResource funcOutput => ConvertFunctionToolCallOutputResource(funcOutput),
|
||||
OutputItemFunctionToolCallOutput funcOutput => ConvertFunctionToolCallOutput(funcOutput),
|
||||
OutputItemReasoningItem => null,
|
||||
_ => null
|
||||
};
|
||||
@@ -332,7 +332,7 @@ internal static class InputConverter
|
||||
[new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments)]);
|
||||
}
|
||||
|
||||
private static ChatMessage ConvertFunctionToolCallOutputResource(FunctionToolCallOutputResource funcOutput)
|
||||
private static ChatMessage ConvertFunctionToolCallOutput(OutputItemFunctionToolCallOutput funcOutput)
|
||||
{
|
||||
return new ChatMessage(
|
||||
ChatRole.Tool,
|
||||
|
||||
@@ -251,16 +251,25 @@ internal static class OutputConverter
|
||||
var outputTokens = details.OutputTokenCount ?? 0;
|
||||
var totalTokens = details.TotalTokenCount ?? 0;
|
||||
|
||||
var cachedTokens = details.AdditionalCounts?.TryGetValue("InputTokenDetails.CachedTokenCount", out var cached) ?? false
|
||||
? cached : 0;
|
||||
var reasoningTokens = details.AdditionalCounts?.TryGetValue("OutputTokenDetails.ReasoningTokenCount", out var reasoning) ?? false
|
||||
? reasoning : 0;
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
inputTokens += existing.InputTokens;
|
||||
outputTokens += existing.OutputTokens;
|
||||
totalTokens += existing.TotalTokens;
|
||||
cachedTokens += existing.InputTokensDetails?.CachedTokens ?? 0;
|
||||
reasoningTokens += existing.OutputTokensDetails?.ReasoningTokens ?? 0;
|
||||
}
|
||||
|
||||
return AzureAIAgentServerResponsesModelFactory.ResponseUsage(
|
||||
return new ResponseUsage(
|
||||
inputTokens: inputTokens,
|
||||
inputTokensDetails: new ResponseUsageInputTokensDetails(cachedTokens),
|
||||
outputTokens: outputTokens,
|
||||
outputTokensDetails: new ResponseUsageOutputTokensDetails(reasoningTokens),
|
||||
totalTokens: totalTokens);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user