.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:
Ben Thomas
2026-04-23 11:52:03 -07:00
committed by GitHub
Unverified
parent 0dbcc9fe9d
commit 5fe8941ff9
20 changed files with 925 additions and 57 deletions
+4 -4
View File
@@ -22,9 +22,9 @@
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.22" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.1" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.3" />
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.23" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.3" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
@@ -188,4 +188,4 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
</Project>
+1
View File
@@ -160,6 +160,7 @@
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Agent_Step25_ToolboxServerSideTools.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/Evaluation/">
<Project Path="samples/02-agents/Evaluation/Evaluation_SimpleEval/Evaluation_SimpleEval.csproj" />
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to load a Foundry toolbox and pass its tools as server-side
// tools when creating an agent. The Foundry platform handles tool execution — the agent
// process does not invoke tools locally.
using System.ClientModel;
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
#pragma warning disable OPENAI001 // Experimental API
#pragma warning disable AAIP001 // AgentToolboxes is experimental
#pragma warning disable CS8321 // Local functions may be commented-out alternatives
// Replace with your own Foundry toolbox name.
const string ToolboxName = "research_toolbox";
// Used only by CombineToolboxes — swap in a second toolbox you own.
const string SecondToolboxName = "analysis_toolbox";
// Replace with any question that exercises the tools configured in your toolbox.
const string Query = "Introduce yourself and briefly describe the tools you can use to help me.";
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("Set FOUNDRY_PROJECT_ENDPOINT to your Foundry project endpoint.");
string model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var projectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
await Main(projectClient, model, endpoint);
// await CombineToolboxes(projectClient, model, endpoint);
// ---------------------------------------------------------------------------
// Main: single toolbox
// ---------------------------------------------------------------------------
static async Task Main(AIProjectClient projectClient, string model, string endpoint)
{
Console.WriteLine("=== Foundry Toolbox Server-Side Tools Example ===");
// Comment out if the toolbox already exists in your Foundry project.
await CreateSampleToolboxAsync(ToolboxName, endpoint);
// Omit the version to resolve the toolbox's current default version at runtime.
var tools = await projectClient.GetToolboxToolsAsync(ToolboxName);
AIAgent agent = projectClient
.AsAIAgent(
model: model,
instructions: "You are a research assistant. Use the available tools to answer questions.",
tools: tools.ToList());
Console.WriteLine($"User: {Query}");
Console.WriteLine($"Result: {await agent.RunAsync(Query)}\n");
}
// ---------------------------------------------------------------------------
// Alternative: combine tools from multiple toolboxes
// ---------------------------------------------------------------------------
static async Task CombineToolboxes(AIProjectClient projectClient, string model, string endpoint)
{
Console.WriteLine("=== Combine Toolboxes Example ===");
// Comment out if the toolboxes already exist in your Foundry project.
await CreateSampleToolboxAsync(ToolboxName, endpoint);
await CreateSampleToolboxAsync(SecondToolboxName, endpoint);
var toolboxA = await projectClient.GetToolboxToolsAsync(ToolboxName);
var toolboxB = await projectClient.GetToolboxToolsAsync(SecondToolboxName);
var allTools = toolboxA.Concat(toolboxB).ToList();
AIAgent agent = projectClient
.AsAIAgent(
model: model,
instructions: "You are a research assistant. Use all available tools to answer questions.",
tools: allTools);
Console.WriteLine($"User: {Query}");
Console.WriteLine($"Combined-toolbox result: {await agent.RunAsync(Query)}\n");
}
// ---------------------------------------------------------------------------
// Helper: create (or replace) a sample toolbox so the sample works out-of-the-box
// ---------------------------------------------------------------------------
static async Task CreateSampleToolboxAsync(string name, string endpoint)
{
// Toolboxes are normally configured in the Foundry portal or a deployment
// script, not the application itself. This helper exists so the sample can
// be run end-to-end without first setting a toolbox up by hand.
// The Foundry-Features header is currently required for toolbox CRUD operations.
var options = new AgentAdministrationClientOptions();
options.AddPolicy(new FoundryFeaturesPolicy("Toolboxes=V1Preview"), PipelinePosition.PerCall);
var adminClient = new AgentAdministrationClient(
new Uri(endpoint),
new DefaultAzureCredential(),
options);
var toolboxClient = adminClient.GetAgentToolboxes();
// Delete existing toolbox if present (ignore 404).
try
{
await toolboxClient.DeleteToolboxAsync(name);
Console.WriteLine($"Deleted existing toolbox '{name}'");
}
catch (ClientResultException ex) when (ex.Status == 404)
{
// Toolbox does not exist — nothing to delete.
}
// Create a fresh version with a single MCP tool.
ProjectsAgentTool mcpTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateMcpTool(
serverLabel: "api-specs",
serverUri: new Uri("https://gitmcp.io/Azure/azure-rest-api-specs"),
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)));
var created = (await toolboxClient.CreateToolboxVersionAsync(
name: name,
tools: [mcpTool],
description: "Sample toolbox with an MCP tool — created by Agent_Step25 sample.")).Value;
Console.WriteLine($"Created toolbox '{created.Name}' v{created.Version} ({created.Tools.Count} tool(s))");
}
// ---------------------------------------------------------------------------
// Pipeline policy that adds the Foundry-Features header for toolbox CRUD
// ---------------------------------------------------------------------------
internal sealed class FoundryFeaturesPolicy(string feature) : PipelinePolicy
{
private const string FeatureHeader = "Foundry-Features";
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
message.Request.Headers.Add(FeatureHeader, feature);
ProcessNext(message, pipeline, currentIndex);
}
public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
message.Request.Headers.Add(FeatureHeader, feature);
return ProcessNextAsync(message, pipeline, currentIndex);
}
}
@@ -0,0 +1,46 @@
# Agent_Step25_ToolboxServerSideTools
This sample demonstrates loading a named Foundry toolbox and passing its tools as
**server-side tools** when creating an agent via `AsAIAgent()`.
When tools from a toolbox are passed this way, they are sent as tool definitions in
the Responses API request. The Foundry platform handles tool execution — the agent
process does not invoke tools locally.
This is the dotnet equivalent of the Python sample:
`python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py`
## Prerequisites
- A Microsoft Foundry project
- `AZURE_AI_PROJECT_ENDPOINT` environment variable set to your Foundry project endpoint
- `AZURE_AI_MODEL_DEPLOYMENT_NAME` environment variable set (defaults to `gpt-5.4-mini`)
The sample recreates the toolbox on each run, replacing any existing toolbox with
the same name. Comment out the `CreateSampleToolboxAsync` call if you want to keep
an existing toolbox unchanged.
## How it works
1. `projectClient.GetToolboxVersionAsync(name)` fetches the toolbox definition from the
Foundry project API (resolving the default version if none is specified)
2. `ToolboxVersion.ToAITools()` converts each tool definition to an `AITool` instance
3. The tools are passed to `AsAIAgent(tools: ...)` which includes them in the Responses
API request as server-side tool definitions
For a one-liner, use `projectClient.GetToolboxToolsAsync(name)` to fetch and convert in one call.
## Sample flows
| Flow | Description |
|------|-------------|
| `Main` (default) | Loads a single toolbox and runs an agent with its tools |
| `CombineToolboxes` | Loads two toolboxes and merges their tools into one agent |
Uncomment the desired flow in the top-level statements to try each one.
## Running the sample
```bash
dotnet run
```
@@ -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);
}
@@ -164,10 +164,8 @@ public class AgentFrameworkResponseHandlerTelemetryTests
private static (CreateResponse request, ResponseContext context) BuildRequest(string? agentKey = null)
{
var request = agentKey is null
? AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test")
: AzureAIAgentServerResponsesModelFactory.CreateResponse(
model: "test",
agentReference: new AgentReference(agentKey));
? new CreateResponse { Model = "test" }
: new CreateResponse { Model = "test", AgentReference = new AgentReference(agentKey) };
request.Input = BinaryData.FromObjectAsJson(new[]
{
@@ -34,7 +34,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -72,9 +72,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(
model: "test",
agentReference: new AgentReference("my-agent"));
var request = new CreateResponse { Model = "test", AgentReference = new AgentReference("my-agent") };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -109,7 +107,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -158,7 +156,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "my-agent");
var request = new CreateResponse { Model = "my-agent" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -195,7 +193,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "");
var request = new CreateResponse { Model = "" };
var metadata = new Metadata();
metadata.AdditionalProperties["entity_id"] = "entity-agent";
request.Metadata = metadata;
@@ -235,9 +233,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(
model: "test",
agentReference: new AgentReference("nonexistent-agent"));
var request = new CreateResponse { Model = "test", AgentReference = new AgentReference("nonexistent-agent") };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -272,9 +268,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(
model: "test",
agentReference: new AgentReference("missing-agent"));
var request = new CreateResponse { Model = "test", AgentReference = new AgentReference("missing-agent") };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -308,7 +302,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "");
var request = new CreateResponse { Model = "" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -342,7 +336,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -387,7 +381,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -435,7 +429,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -478,7 +472,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -517,9 +511,11 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(
model: "test",
instructions: "You are a helpful assistant.");
var request = new CreateResponse
{
Model = "test",
Instructions = "You are a helpful assistant.",
};
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -557,7 +553,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -598,9 +594,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(
model: "test",
agentReference: new AgentReference("agent-2"));
var request = new CreateResponse { Model = "test", AgentReference = new AgentReference("agent-2") };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -637,7 +631,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -674,7 +668,7 @@ public class AgentFrameworkResponseHandlerTests
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = BinaryData.FromObjectAsJson(new[]
{
new { type = "message", id = "msg_1", status = "completed", role = "user",
@@ -0,0 +1,329 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
#pragma warning disable OPENAI001
#pragma warning disable AAIP001
namespace Microsoft.Agents.AI.Foundry.UnitTests;
/// <summary>
/// Unit tests for the <see cref="FoundryToolbox"/> class.
/// </summary>
public class FoundryToolboxTests
{
private static readonly Uri s_testEndpoint = new("https://test.services.ai.azure.com/api/projects/test-project");
#region Parameter validation tests
[Fact]
public async Task GetToolboxVersionAsync_NullEndpoint_ThrowsAsync()
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
FoundryToolbox.GetToolboxVersionAsync(
projectEndpoint: null!,
credential: new FakeAuthenticationTokenProvider(),
name: "test-toolbox"));
}
[Fact]
public async Task GetToolboxVersionAsync_NullCredential_ThrowsAsync()
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
FoundryToolbox.GetToolboxVersionAsync(
projectEndpoint: s_testEndpoint,
credential: null!,
name: "test-toolbox"));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task GetToolboxVersionAsync_InvalidName_ThrowsAsync(string? name)
{
await Assert.ThrowsAnyAsync<ArgumentException>(() =>
FoundryToolbox.GetToolboxVersionAsync(
projectEndpoint: s_testEndpoint,
credential: new FakeAuthenticationTokenProvider(),
name: name!));
}
[Fact]
public async Task GetToolsAsync_NullEndpoint_ThrowsAsync()
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
FoundryToolbox.GetToolsAsync(
projectEndpoint: null!,
credential: new FakeAuthenticationTokenProvider(),
name: "test-toolbox"));
}
[Fact]
public void ToAITools_NullToolboxVersion_Throws()
{
Assert.Throws<ArgumentNullException>(() =>
FoundryToolbox.ToAITools(null!));
}
#endregion
#region ToAITools conversion tests
[Fact]
public void ToAITools_EmptyTools_ReturnsEmptyList()
{
var version = ProjectsAgentsModelFactory.ToolboxVersion(
metadata: null,
id: "ver-1",
name: "empty-toolbox",
version: "v1",
description: "Empty",
createdAt: DateTimeOffset.UtcNow,
tools: Array.Empty<ProjectsAgentTool>(),
policies: null);
var tools = version.ToAITools();
Assert.Empty(tools);
}
[Fact]
public void ToAITools_NullTools_ReturnsEmptyList()
{
var version = ProjectsAgentsModelFactory.ToolboxVersion(
metadata: null,
id: "ver-1",
name: "null-tools-toolbox",
version: "v1",
description: "Null tools",
createdAt: DateTimeOffset.UtcNow,
tools: null,
policies: null);
var tools = version.ToAITools();
Assert.Empty(tools);
}
[Fact]
public void ToAITools_WithCodeInterpreterTool_ReturnsAITool()
{
var json = TestDataUtil.GetToolboxVersionResponseJson();
var version = ModelReaderWriter.Read<ToolboxVersion>(BinaryData.FromString(json))!;
var tools = version.ToAITools();
Assert.Single(tools);
Assert.IsAssignableFrom<AITool>(tools[0]);
}
[Fact]
public void ToAITools_SanitizesDecorationFieldsOnNonFunctionTools()
{
var json = TestDataUtil.GetToolboxVersionWithDecorationFieldsJson();
var version = ModelReaderWriter.Read<ToolboxVersion>(BinaryData.FromString(json))!;
var tools = version.ToAITools();
Assert.Single(tools);
Assert.IsAssignableFrom<AITool>(tools[0]);
}
[Fact]
public void SanitizeAndConvert_FunctionTool_PreservesNameAndDescription()
{
const string ToolJson = @"{""type"":""function"",""name"":""get_weather"",""description"":""Get weather"",""parameters"":{""type"":""object"",""properties"":{}}}";
var tool = ModelReaderWriter.Read<ProjectsAgentTool>(BinaryData.FromString(ToolJson))!;
var aiTool = FoundryToolbox.SanitizeAndConvert(tool);
Assert.NotNull(aiTool);
Assert.IsAssignableFrom<AITool>(aiTool);
}
[Fact]
public void SanitizeAndConvert_CodeInterpreterWithExtraFields_StripsDecorationFields()
{
const string ToolJson = @"{""type"":""code_interpreter"",""name"":""code_interpreter"",""description"":""Execute code""}";
var tool = ModelReaderWriter.Read<ProjectsAgentTool>(BinaryData.FromString(ToolJson))!;
var aiTool = FoundryToolbox.SanitizeAndConvert(tool);
Assert.NotNull(aiTool);
}
#endregion
#region Integration tests with mock HTTP
[Fact]
public async Task GetToolboxVersionAsync_WithExplicitVersion_FetchesVersionDirectlyAsync()
{
var versionJson = TestDataUtil.GetToolboxVersionResponseJson();
using var httpHandler = new HttpHandlerAssert((request) =>
{
Assert.Contains("/toolboxes/research_tools/versions/v5", request.RequestUri!.PathAndQuery);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(versionJson, Encoding.UTF8, "application/json")
};
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var clientOptions = new AgentAdministrationClientOptions { Transport = new HttpClientPipelineTransport(httpClient) };
var result = await FoundryToolbox.GetToolboxVersionAsync(
s_testEndpoint,
new FakeAuthenticationTokenProvider(),
"research_tools",
version: "v5",
clientOptions: clientOptions,
cancellationToken: default);
Assert.Equal("research_tools", result.Name);
Assert.Equal("v5", result.Version);
Assert.Single(result.Tools);
}
[Fact]
public async Task GetToolboxVersionAsync_WithoutVersion_ResolvesDefaultThenFetchesAsync()
{
var recordJson = TestDataUtil.GetToolboxRecordResponseJson();
var versionJson = TestDataUtil.GetToolboxVersionResponseJson();
var callCount = 0;
using var httpHandler = new HttpHandlerAssert((request) =>
{
callCount++;
var path = request.RequestUri!.PathAndQuery;
if (!path.Contains("/versions/"))
{
Assert.Contains("/toolboxes/research_tools", path);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(recordJson, Encoding.UTF8, "application/json")
};
}
Assert.Contains("/toolboxes/research_tools/versions/v5", path);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(versionJson, Encoding.UTF8, "application/json")
};
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var clientOptions = new AgentAdministrationClientOptions { Transport = new HttpClientPipelineTransport(httpClient) };
var result = await FoundryToolbox.GetToolboxVersionAsync(
s_testEndpoint,
new FakeAuthenticationTokenProvider(),
"research_tools",
version: null,
clientOptions: clientOptions,
cancellationToken: default);
Assert.Equal(2, callCount);
Assert.Equal("research_tools", result.Name);
Assert.Equal("v5", result.Version);
}
[Fact]
public async Task GetToolboxVersionAsync_ApiError_ThrowsClientResultExceptionAsync()
{
using var httpHandler = new HttpHandlerAssert((_) =>
new HttpResponseMessage(HttpStatusCode.NotFound)
{
Content = new StringContent("{\"error\":\"not found\"}", Encoding.UTF8, "application/json")
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var clientOptions = new AgentAdministrationClientOptions { Transport = new HttpClientPipelineTransport(httpClient) };
await Assert.ThrowsAsync<ClientResultException>(() =>
FoundryToolbox.GetToolboxVersionAsync(
s_testEndpoint,
new FakeAuthenticationTokenProvider(),
"nonexistent-toolbox",
version: "v1",
clientOptions: clientOptions,
cancellationToken: default));
}
[Fact]
public async Task GetToolsAsync_ReturnsConvertedAIToolsAsync()
{
var versionJson = TestDataUtil.GetToolboxVersionResponseJson();
using var httpHandler = new HttpHandlerAssert((_) =>
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(versionJson, Encoding.UTF8, "application/json")
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var clientOptions = new AgentAdministrationClientOptions { Transport = new HttpClientPipelineTransport(httpClient) };
var result = await FoundryToolbox.GetToolboxVersionAsync(
s_testEndpoint,
new FakeAuthenticationTokenProvider(),
"research_tools",
version: "v5",
clientOptions: clientOptions,
cancellationToken: default);
var tools = result.ToAITools();
Assert.Single(tools);
Assert.IsAssignableFrom<AITool>(tools[0]);
}
#endregion
#region AIProjectClient extension tests
[Fact]
public async Task AIProjectClientExtension_GetToolboxToolsAsync_ReturnsAIToolsAsync()
{
var versionJson = TestDataUtil.GetToolboxVersionResponseJson();
using var httpHandler = new HttpHandlerAssert((_) =>
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(versionJson, Encoding.UTF8, "application/json")
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var clientOptions = new AIProjectClientOptions();
clientOptions.Transport = new HttpClientPipelineTransport(httpClient);
var client = new AIProjectClient(s_testEndpoint, new FakeAuthenticationTokenProvider(), clientOptions);
var tools = await client.GetToolboxToolsAsync("research_tools", version: "v5");
Assert.Single(tools);
Assert.IsAssignableFrom<AITool>(tools[0]);
}
#endregion
}
@@ -2,7 +2,6 @@
using System;
using System.Linq;
using Azure.AI.AgentServer.Responses;
using Azure.AI.AgentServer.Responses.Models;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
@@ -146,11 +145,7 @@ public class InputConverterTests
[Fact]
public void ConvertToChatOptions_SetsTemperatureAndTopP()
{
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(
temperature: 0.7,
topP: 0.9,
maxOutputTokens: 1000,
model: "gpt-4o");
var request = new CreateResponse { Temperature = 0.7, TopP = 0.9, MaxOutputTokens = 1000, Model = "gpt-4o" };
var options = InputConverter.ConvertToChatOptions(request);
@@ -211,9 +206,9 @@ public class InputConverterTests
}
[Fact]
public void ConvertOutputItemsToMessages_FunctionToolCallOutputResource_ReturnsToolMessage()
public void ConvertOutputItemsToMessages_FunctionToolCallOutput_ReturnsToolMessage()
{
var funcOutput = new FunctionToolCallOutputResource(
var funcOutput = new OutputItemFunctionToolCallOutput(
callId: "call_def",
output: BinaryData.FromString("result data"));
@@ -229,8 +224,7 @@ public class InputConverterTests
[Fact]
public void ConvertOutputItemsToMessages_ReasoningItem_ReturnsNull()
{
var reasoning = AzureAIAgentServerResponsesModelFactory.OutputItemReasoningItem(
id: "reason_001");
var reasoning = new OutputItemReasoningItem("reason_001", []);
var messages = InputConverter.ConvertOutputItemsToMessages([reasoning]);
@@ -661,7 +655,7 @@ public class InputConverterTests
[Fact]
public void ConvertToChatOptions_ModelId_NotSetFromRequest()
{
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "my-model");
var request = new CreateResponse { Model = "my-model" };
var options = InputConverter.ConvertToChatOptions(request);
@@ -20,7 +20,7 @@ public class OutputConverterTests
private static (ResponseEventStream stream, Mock<ResponseContext> mockContext) CreateTestStream()
{
var mockContext = new Mock<ResponseContext>("resp_" + new string('0', 46)) { CallBase = true };
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test-model");
var request = new CreateResponse { Model = "test-model" };
var stream = new ResponseEventStream(mockContext.Object, request);
return (stream, mockContext);
}
@@ -160,9 +160,7 @@ public class WorkflowIntegrationTests
var sp = services.BuildServiceProvider();
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(
model: "test",
agentReference: new AgentReference("my-workflow"));
var request = new CreateResponse { Model = "test", AgentReference = new AgentReference("my-workflow") };
request.Input = CreateUserInput("Test keyed workflow");
var mockContext = CreateMockContext();
@@ -363,7 +361,7 @@ public class WorkflowIntegrationTests
var sp = services.BuildServiceProvider();
var handler = new AgentFrameworkResponseHandler(sp, NullLogger<AgentFrameworkResponseHandler>.Instance);
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test");
var request = new CreateResponse { Model = "test" };
request.Input = CreateUserInput(userMessage);
var mockContext = CreateMockContext();
@@ -393,7 +391,7 @@ public class WorkflowIntegrationTests
private static (ResponseEventStream stream, Mock<ResponseContext> mockContext) CreateTestStream()
{
var mockContext = new Mock<ResponseContext>("resp_" + new string('0', 46)) { CallBase = true };
var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test-model");
var request = new CreateResponse { Model = "test-model" };
var stream = new ResponseEventStream(mockContext.Object, request);
return (stream, mockContext);
}
@@ -10,7 +10,7 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
@@ -34,7 +34,7 @@
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
</ItemGroup>
<!-- Evaluation tests require net8.0+ (MEAI.Evaluation does not support legacy TFMs) -->
<!-- FoundryEval tests require net8.0+ (MEAI.Evaluation does not support legacy TFMs) -->
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<Compile Remove="FoundryEvalConverterTests.cs" />
<Compile Remove="FoundryEvalsTests.cs" />
@@ -50,6 +50,15 @@
<None Update="TestData\OpenAIDefaultResponse.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\ToolboxRecordResponse.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\ToolboxVersionResponse.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\ToolboxVersionWithDecorationFields.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,5 @@
{
"id": "tbx-123",
"name": "research_tools",
"default_version": "v5"
}
@@ -0,0 +1,11 @@
{
"metadata": {},
"id": "tbv-research_tools-v5",
"name": "research_tools",
"version": "v5",
"description": "Example research toolbox",
"created_at": 1775779200,
"tools": [
{ "type": "code_interpreter" }
]
}
@@ -0,0 +1,11 @@
{
"metadata": {},
"id": "tbv-dirty-v1",
"name": "dirty_toolbox",
"version": "v1",
"description": "Toolbox with decoration fields on tools",
"created_at": 1775779200,
"tools": [
{ "type": "code_interpreter", "name": "code_interpreter", "description": "Execute Python code" }
]
}
@@ -14,6 +14,9 @@ internal static class TestDataUtil
private static readonly string s_agentResponseJson = File.ReadAllText("TestData/AgentResponse.json");
private static readonly string s_agentVersionResponseJson = File.ReadAllText("TestData/AgentVersionResponse.json");
private static readonly string s_openAIDefaultResponseJson = File.ReadAllText("TestData/OpenAIDefaultResponse.json");
private static readonly string s_toolboxRecordResponseJson = File.ReadAllText("TestData/ToolboxRecordResponse.json");
private static readonly string s_toolboxVersionResponseJson = File.ReadAllText("TestData/ToolboxVersionResponse.json");
private static readonly string s_toolboxVersionWithDecorationFieldsJson = File.ReadAllText("TestData/ToolboxVersionWithDecorationFields.json");
private const string AgentDefinitionPlaceholder = "\"agent-definition-placeholder\"";
@@ -162,4 +165,19 @@ internal static class TestDataUtil
}
return json;
}
/// <summary>
/// Gets the toolbox record response JSON.
/// </summary>
public static string GetToolboxRecordResponseJson() => s_toolboxRecordResponseJson;
/// <summary>
/// Gets the toolbox version response JSON.
/// </summary>
public static string GetToolboxVersionResponseJson() => s_toolboxVersionResponseJson;
/// <summary>
/// Gets the toolbox version response JSON with decoration fields on tools.
/// </summary>
public static string GetToolboxVersionWithDecorationFieldsJson() => s_toolboxVersionWithDecorationFieldsJson;
}