diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 46e0d61924..a5dfdfba9b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -22,9 +22,9 @@ - - - + + + @@ -188,4 +188,4 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index b3a95ea1f6..dc13d4f0e9 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -162,6 +162,7 @@ + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Agent_Step25_ToolboxServerSideTools.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Agent_Step25_ToolboxServerSideTools.csproj new file mode 100644 index 0000000000..0db6ba9fe6 --- /dev/null +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Agent_Step25_ToolboxServerSideTools.csproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Program.cs new file mode 100644 index 0000000000..12731f4723 --- /dev/null +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Program.cs @@ -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 pipeline, int currentIndex) + { + message.Request.Headers.Add(FeatureHeader, feature); + ProcessNext(message, pipeline, currentIndex); + } + + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + message.Request.Headers.Add(FeatureHeader, feature); + return ProcessNextAsync(message, pipeline, currentIndex); + } +} diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/README.md new file mode 100644 index 0000000000..75c6b3eb9d --- /dev/null +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/README.md @@ -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 +``` diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AIProjectClientToolboxExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AIProjectClientToolboxExtensions.cs new file mode 100644 index 0000000000..48c50eaedb --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AIProjectClientToolboxExtensions.cs @@ -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; + +/// +/// Provides extension methods on for fetching +/// Foundry toolbox definitions as server-side tools. +/// +/// +/// These extensions mirror Python's FoundryChatClient.get_toolbox() pattern, +/// allowing a single call on the project client to retrieve tools ready for use +/// with AsAIAgent(model, instructions, tools: ...). +/// +[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +public static class AIProjectClientToolboxExtensions +{ + /// + /// Fetches a toolbox from the Foundry project and returns its tools as instances + /// ready for use as server-side tools in the Responses API. + /// + /// The to use. Cannot be . + /// The name of the toolbox to fetch. + /// + /// The specific toolbox version to fetch. When , the toolbox's + /// default version is resolved automatically. + /// + /// A token to monitor for cancellation requests. + /// A read-only list of instances from the toolbox. + /// + /// Thrown when or is . + /// + public static async Task> 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(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolbox.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolbox.cs new file mode 100644 index 0000000000..19f6f6823a --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolbox.cs @@ -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 uses reflection; suppressed for Azure SDK model types. +#pragma warning disable IL3050 // ModelReaderWriter.Read requires dynamic code; suppressed for Azure SDK model types. + +namespace Microsoft.Agents.AI.Foundry.Hosting; + +/// +/// Provides methods for fetching Foundry toolbox definitions and converting their tools +/// to instances for use as server-side tools in the Responses API. +/// +/// +/// +/// When tools from a toolbox are passed to a Foundry agent (e.g. via AsAIAgent(model, instructions, tools: ...)), +/// 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. +/// +/// +/// This is the dotnet equivalent of Python's FoundryChatClient.get_toolbox() pattern. +/// +/// +[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +public static class FoundryToolbox +{ + /// + /// Fetches a toolbox version from the Foundry project and returns the raw SDK . + /// + /// The Foundry project endpoint URI. + /// The authentication credential used to access the Foundry project. + /// The name of the toolbox to fetch. + /// + /// The specific toolbox version to fetch. When , the toolbox's + /// default version is resolved automatically (requires an additional API call). + /// + /// A token to monitor for cancellation requests. + /// The containing tool definitions. + /// + /// Thrown when , , or is . + /// + /// Thrown when the Foundry project API returns an error. + public static async Task 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); + } + + /// + /// Fetches a toolbox from the Foundry project and returns its tools as instances + /// ready for use as server-side tools in the Responses API. + /// + /// The Foundry project endpoint URI. + /// The authentication credential used to access the Foundry project. + /// The name of the toolbox to fetch. + /// + /// The specific toolbox version to fetch. When , the toolbox's + /// default version is resolved automatically. + /// + /// A token to monitor for cancellation requests. + /// A read-only list of instances from the toolbox. + /// + /// Thrown when , , or is . + /// + /// Thrown when the Foundry project API returns an error. + public static async Task> 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(); + } + + /// + /// Converts the tools in a to instances + /// suitable for use as server-side tools in the Responses API. + /// + /// The toolbox version whose tools to convert. + /// A read-only list of instances. + /// Thrown when is . + /// + /// + /// Each in the toolbox is cast to + /// and converted via AsAITool(). Non-function hosted tools (MCP, web_search, + /// code_interpreter, etc.) are included as server-side tool definitions — the Foundry + /// platform handles their execution. + /// + /// + /// Non-function tools are sanitized to remove decoration fields (name, description) + /// that the toolbox API returns but the Responses API rejects. + /// + /// + public static IReadOnlyList 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) + + /// + /// Sanitizes a by removing decoration fields that the + /// toolbox API returns but the Responses API rejects, then converts to . + /// + /// + /// The Azure AI Projects toolbox API may return name and description on + /// hosted tool objects (MCP, code_interpreter, file_search, etc.). The Responses API + /// rejects at least name with "Unknown parameter: 'tools[0].name'". We strip + /// these decoration fields for non-function tools. Function tools keep them since + /// name and description are expected parts of the function schema. + /// + 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(); + + // 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(BinaryData.FromString(sanitizedJson))!; + return sanitizedTool.AsAITool(); + } + + internal static async Task 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 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 +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/InputConverter.cs index cc97049ae9..a65b79a747 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/InputConverter.cs @@ -237,7 +237,7 @@ private static ChatMessage ConvertItemFunctionToolCall(ItemFunctionToolCall func { OutputItemMessage msg => ConvertOutputItemMessageToChat(msg), OutputItemFunctionToolCall funcCall => ConvertOutputItemFunctionCall(funcCall), - FunctionToolCallOutputResource funcOutput => ConvertFunctionToolCallOutputResource(funcOutput), + OutputItemFunctionToolCallOutput funcOutput => ConvertFunctionToolCallOutput(funcOutput), OutputItemReasoningItem => null, _ => null }; @@ -332,7 +332,7 @@ private static ChatMessage ConvertOutputItemFunctionCall(OutputItemFunctionToolC [new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments)]); } - private static ChatMessage ConvertFunctionToolCallOutputResource(FunctionToolCallOutputResource funcOutput) + private static ChatMessage ConvertFunctionToolCallOutput(OutputItemFunctionToolCallOutput funcOutput) { return new ChatMessage( ChatRole.Tool, diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs index 58ba989ebf..2d7728361e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs @@ -251,16 +251,25 @@ private static ResponseUsage ConvertUsage(UsageDetails details, ResponseUsage? e 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); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTelemetryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTelemetryTests.cs index 9b17fa9fae..48daf490ee 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTelemetryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTelemetryTests.cs @@ -164,10 +164,8 @@ public async Task CreateAsync_DefaultAgent_SpanDisplayNameContainsAgentNameAsync 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[] { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index 75a495f05b..d7fc8fc884 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -34,7 +34,7 @@ public async Task CreateAsync_WithDefaultAgent_ProducesStreamEventsAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgentAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_NoAgentRegistered_ThrowsInvalidOperationExceptionA var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_ResolvesAgentByModelFieldAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_ResolvesAgentByEntityIdMetadataAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_NamedAgentNotFound_FallsBackToDefaultAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentNameAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_NoAgentNoName_ErrorMessageIsGenericAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEvent var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_WithHistory_PrependsHistoryToMessagesAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_WithInputItems_UsesResolvedInputItemsAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_NoInputItems_FallsBackToRawRequestInputAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_PassesInstructionsToAgentAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessageAsync( var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOneAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_CancellationDuringExecution_PropagatesOperationCan var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 async Task CreateAsync_DefaultAgent_IsAutoWrappedWithOpenTelemetryAsync() var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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", diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxTests.cs new file mode 100644 index 0000000000..3f0a8a54dd --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxTests.cs @@ -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; + +/// +/// Unit tests for the class. +/// +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(() => + FoundryToolbox.GetToolboxVersionAsync( + projectEndpoint: null!, + credential: new FakeAuthenticationTokenProvider(), + name: "test-toolbox")); + } + + [Fact] + public async Task GetToolboxVersionAsync_NullCredential_ThrowsAsync() + { + await Assert.ThrowsAsync(() => + 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(() => + FoundryToolbox.GetToolboxVersionAsync( + projectEndpoint: s_testEndpoint, + credential: new FakeAuthenticationTokenProvider(), + name: name!)); + } + + [Fact] + public async Task GetToolsAsync_NullEndpoint_ThrowsAsync() + { + await Assert.ThrowsAsync(() => + FoundryToolbox.GetToolsAsync( + projectEndpoint: null!, + credential: new FakeAuthenticationTokenProvider(), + name: "test-toolbox")); + } + + [Fact] + public void ToAITools_NullToolboxVersion_Throws() + { + Assert.Throws(() => + 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(), + 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(BinaryData.FromString(json))!; + + var tools = version.ToAITools(); + + Assert.Single(tools); + Assert.IsAssignableFrom(tools[0]); + } + + [Fact] + public void ToAITools_SanitizesDecorationFieldsOnNonFunctionTools() + { + var json = TestDataUtil.GetToolboxVersionWithDecorationFieldsJson(); + var version = ModelReaderWriter.Read(BinaryData.FromString(json))!; + + var tools = version.ToAITools(); + + Assert.Single(tools); + Assert.IsAssignableFrom(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(BinaryData.FromString(ToolJson))!; + + var aiTool = FoundryToolbox.SanitizeAndConvert(tool); + + Assert.NotNull(aiTool); + Assert.IsAssignableFrom(aiTool); + } + + [Fact] + public void SanitizeAndConvert_CodeInterpreterWithExtraFields_StripsDecorationFields() + { + const string ToolJson = @"{""type"":""code_interpreter"",""name"":""code_interpreter"",""description"":""Execute code""}"; + var tool = ModelReaderWriter.Read(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(() => + 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(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(tools[0]); + } + + #endregion +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs index e2f6159a6e..f10a015c4b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs @@ -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 void ConvertInputToMessages_MultipleItems_ReturnsAllMessages() [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 void ConvertOutputItemsToMessages_FunctionToolCall_ReturnsAssistantMessag } [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 void ConvertOutputItemsToMessages_FunctionToolCallOutputResource_ReturnsT [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 void ConvertOutputItemsToMessages_UnknownOutputItemType_IsSkipped() [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); diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs index 876339ea2c..2b8abfefcb 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs @@ -20,7 +20,7 @@ public class OutputConverterTests private static (ResponseEventStream stream, Mock mockContext) CreateTestStream() { var mockContext = new Mock("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); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs index 05be11c3d8..d87406f815 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs @@ -160,9 +160,7 @@ public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectlyAsync() var sp = services.BuildServiceProvider(); var handler = new AgentFrameworkResponseHandler(sp, NullLogger.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 @@ private static (AgentFrameworkResponseHandler handler, CreateResponse request, R var sp = services.BuildServiceProvider(); var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); - var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + var request = new CreateResponse { Model = "test" }; request.Input = CreateUserInput(userMessage); var mockContext = CreateMockContext(); @@ -393,7 +391,7 @@ private static Mock CreateMockContext() private static (ResponseEventStream stream, Mock mockContext) CreateTestStream() { var mockContext = new Mock("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); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 2cead6029a..2265719711 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -10,7 +10,7 @@ - + @@ -34,7 +34,7 @@ - + @@ -50,6 +50,15 @@ Always + + Always + + + Always + + + Always + diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxRecordResponse.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxRecordResponse.json new file mode 100644 index 0000000000..5208a57262 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxRecordResponse.json @@ -0,0 +1,5 @@ +{ + "id": "tbx-123", + "name": "research_tools", + "default_version": "v5" +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionResponse.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionResponse.json new file mode 100644 index 0000000000..13f686ef8d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionResponse.json @@ -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" } + ] +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionWithDecorationFields.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionWithDecorationFields.json new file mode 100644 index 0000000000..b78698bfb0 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionWithDecorationFields.json @@ -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" } + ] +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs index 3460362efd..898e0293c7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs @@ -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 @@ private static string ApplyDescription(string json, string? description) } return json; } + + /// + /// Gets the toolbox record response JSON. + /// + public static string GetToolboxRecordResponseJson() => s_toolboxRecordResponseJson; + + /// + /// Gets the toolbox version response JSON. + /// + public static string GetToolboxVersionResponseJson() => s_toolboxVersionResponseJson; + + /// + /// Gets the toolbox version response JSON with decoration fields on tools. + /// + public static string GetToolboxVersionWithDecorationFieldsJson() => s_toolboxVersionWithDecorationFieldsJson; }