diff --git a/sdk/ai/azure-ai-agents/CHANGELOG.md b/sdk/ai/azure-ai-agents/CHANGELOG.md index 5e275d86ca04..d250b5b2057e 100644 --- a/sdk/ai/azure-ai-agents/CHANGELOG.md +++ b/sdk/ai/azure-ai-agents/CHANGELOG.md @@ -7,12 +7,18 @@ - Added `readSpecFromFile(Path)` static convenience method to `OpenApiFunctionDefinition` for loading OpenAPI specification JSON files as the `Map` required by the constructor, eliminating the need for manual `JsonReader`/`BinaryData` wiring. - Added new `OpenApiSync`/`OpenApiAsync` samples demonstrating end-to-end OpenAPI tool integration: loading a spec file, creating an agent with an `OpenApiTool`, and invoking an external API via conversation. - Added new tool samples for parity with the Python SDK: `AzureFunctionSync`/`AzureFunctionAsync`, `BingCustomSearchSync`/`BingCustomSearchAsync`, `MemorySearchSync`/`MemorySearchAsync`, `McpWithConnectionSync`/`McpWithConnectionAsync`, and `OpenApiWithConnectionSync`/`OpenApiWithConnectionAsync`. +- Added type-safe accessors on `CodeInterpreterTool` for the `container` property: `setContainer(String)`, `setContainer(AutoCodeInterpreterToolParam)`, `getContainerAsString()`, and `getContainerAsAutoCodeInterpreterToolParam()`. +- Added type-safe accessors on `McpTool` for the `allowedTools` property: `setAllowedTools(List)`, `setAllowedTools(McpToolFilter)`, `getAllowedToolsAsStringList()`, and `getAllowedToolsAsMcpToolFilter()`. +- Added type-safe accessors on `McpTool` for the `requireApproval` property: `setRequireApproval(String)`, `setRequireApproval(McpToolRequireApproval)`, `getRequireApprovalAsString()`, and `getRequireApprovalAsMcpToolRequireApproval()`. - Added `setComparisonFilter(ComparisonFilter)` and `setCompoundFilter(CompoundFilter)` convenience methods on `FileSearchTool`, accepting the openai-java filter types directly. ### Breaking Changes - `AgentDefinitionOptInKeys` and `FoundryFeaturesOptInKeys` changed from `ExpandableStringEnum`-based classes to standard Java `enum` types. The `values()` method now returns an array instead of a `Collection`, and the deprecated no-arg constructor is removed. - The `timezone` property in `ApproximateLocation` and `WebSearchApproximateLocation` changed from `String` to `java.util.TimeZone`. +- The `container` property on `CodeInterpreterTool` no longer exposes `BinaryData` getter/setter publicly. Use the new typed accessors instead (e.g., `setContainer("container-id")` or `setContainer(new AutoCodeInterpreterToolParam())`). +- The `allowedTools` and `requireApproval` properties on `McpTool` no longer expose `BinaryData` getter/setter publicly. Use the new typed accessors instead (e.g., `setRequireApproval("always")` or `setAllowedTools(List.of("tool_a", "tool_b"))`). +- The `filters` property on `FileSearchTool` no longer exposes `BinaryData` getter/setter publicly. - The `reasoning` property on `PromptAgentDefinition` now uses `com.openai.models.Reasoning` from the openai-java library instead of the previously generated `Reasoning` class. Use `Reasoning.builder().effort(ReasoningEffort.HIGH).build()` to construct values. - Removed `ComparisonFilter`, `ComparisonFilterType`, `CompoundFilter`, `CompoundFilterType`, `Reasoning`, `ReasoningEffort`, `ReasoningSummary`, and `ReasoningGenerateSummary` from `com.azure.ai.agents.models`. Use the equivalent types from `com.openai.models` instead (e.g., `com.openai.models.ComparisonFilter`, `com.openai.models.Reasoning`). diff --git a/sdk/ai/azure-ai-agents/README.md b/sdk/ai/azure-ai-agents/README.md index 79421f811241..726d25ee1a06 100644 --- a/sdk/ai/azure-ai-agents/README.md +++ b/sdk/ai/azure-ai-agents/README.md @@ -301,7 +301,7 @@ Connect agents to external MCP servers: // Uses gitmcp.io to expose a GitHub repository as an MCP-compatible server McpTool tool = new McpTool("api-specs") .setServerUrl("https://gitmcp.io/Azure/azure-rest-api-specs") - .setRequireApproval(BinaryData.fromObject("always")); + .setRequireApproval("always"); ``` See the full sample in [McpSync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpSync.java). @@ -536,7 +536,7 @@ MCP integration using project-specific connections for accessing connected MCP s McpTool mcpTool = new McpTool("api-specs") .setServerUrl("https://api.githubcopilot.com/mcp") .setProjectConnectionId(mcpConnectionId) - .setRequireApproval(BinaryData.fromObject("always")); + .setRequireApproval("always"); ``` See the full sample in [McpWithConnectionSync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionSync.java). diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/CodeInterpreterTool.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/CodeInterpreterTool.java index 81d52a57312b..0e9b5d7152d7 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/CodeInterpreterTool.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/CodeInterpreterTool.java @@ -53,8 +53,8 @@ public ToolType getType() { * * @return the container value. */ - @Generated - public BinaryData getContainer() { + BinaryData getContainer() { + // AI Tooling: union type return this.container; } @@ -117,9 +117,71 @@ public CodeInterpreterTool() { * @param container the container value to set. * @return the CodeInterpreterTool object itself. */ - @Generated - public CodeInterpreterTool setContainer(BinaryData container) { + CodeInterpreterTool setContainer(BinaryData container) { + // AI Tooling: union type this.container = container; return this; } + + /** + * Set the container property: The code interpreter container. Can be a container ID or an object that + * specifies uploaded file IDs to make available to your code, along with an + * optional `memory_limit` setting. + * If not provided, the service assumes auto. + * + * @param containerId the container ID string to set. + * @return the CodeInterpreterTool object itself. + */ + public CodeInterpreterTool setContainer(String containerId) { + // AI Tooling: union type + this.container = BinaryData.fromString(containerId); + return this; + } + + /** + * Set the container property: The code interpreter container. Can be a container ID or an object that + * specifies uploaded file IDs to make available to your code, along with an + * optional `memory_limit` setting. + * If not provided, the service assumes auto. + * + * @param container the {@link AutoCodeInterpreterToolParam} to set. + * @return the CodeInterpreterTool object itself. + */ + public CodeInterpreterTool setContainer(AutoCodeInterpreterToolParam container) { + // AI Tooling: union type + this.container = BinaryData.fromObject(container); + return this; + } + + /** + * Get the container property: The code interpreter container. Can be a container ID or an object that + * specifies uploaded file IDs to make available to your code, along with an + * optional `memory_limit` setting. + * If not provided, the service assumes auto. + * + * @return the container value as a String. + */ + public String getContainerAsString() { + // AI Tooling: union type + if (this.container == null) { + return null; + } + return this.container.toObject(String.class); + } + + /** + * Get the container property: The code interpreter container. Can be a container ID or an object that + * specifies uploaded file IDs to make available to your code, along with an + * optional `memory_limit` setting. + * If not provided, the service assumes auto. + * + * @return the container value as an {@link AutoCodeInterpreterToolParam}. + */ + public AutoCodeInterpreterToolParam getContainerAsAutoCodeInterpreterToolParam() { + // AI Tooling: union type + if (this.container == null) { + return null; + } + return this.container.toObject(AutoCodeInterpreterToolParam.class); + } } diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/FileSearchTool.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/FileSearchTool.java index cabbf172678e..8a99bcbd8e9a 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/FileSearchTool.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/FileSearchTool.java @@ -100,28 +100,6 @@ public FileSearchTool setRankingOptions(RankingOptions rankingOptions) { return this; } - /** - * Get the filters property: The filters property. - * - * @return the filters value. - */ - @Generated - public BinaryData getFilters() { - return this.filters; - } - - /** - * Set the filters property: The filters property. - * - * @param filters the filters value to set. - * @return the FileSearchTool object itself. - */ - @Generated - public FileSearchTool setFilters(BinaryData filters) { - this.filters = filters; - return this; - } - /** * {@inheritDoc} */ @@ -214,6 +192,28 @@ public FileSearchTool setMaxResults(Long maxResults) { return this; } + /** + * Get the filters property: The filters property. + * + * @return the filters value. + */ + BinaryData getFilters() { + // AI Tooling: union type + return this.filters; + } + + /** + * Set the filters property: The filters property. + * + * @param filters the filters value to set. + * @return the FileSearchTool object itself. + */ + FileSearchTool setFilters(BinaryData filters) { + // AI Tooling: union type + this.filters = filters; + return this; + } + /** * Sets the file search filters using an openai-java {@link ComparisonFilter}. *

diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/McpTool.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/McpTool.java index b8e97717ed66..178d2bbf85d8 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/McpTool.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/McpTool.java @@ -10,6 +10,7 @@ import com.azure.json.JsonToken; import com.azure.json.JsonWriter; import java.io.IOException; +import java.util.List; import java.util.Map; /** @@ -270,8 +271,8 @@ public McpTool setHeaders(Map headers) { * * @return the allowedTools value. */ - @Generated - public BinaryData getAllowedTools() { + BinaryData getAllowedTools() { + // AI Tooling: union type return this.allowedTools; } @@ -281,19 +282,70 @@ public BinaryData getAllowedTools() { * @param allowedTools the allowedTools value to set. * @return the McpTool object itself. */ - @Generated - public McpTool setAllowedTools(BinaryData allowedTools) { + McpTool setAllowedTools(BinaryData allowedTools) { + // AI Tooling: union type this.allowedTools = allowedTools; return this; } + /** + * Set the allowedTools property: The allowed_tools property. + * + * @param allowedTools the list of tool name strings to set. + * @return the McpTool object itself. + */ + public McpTool setAllowedTools(List allowedTools) { + // AI Tooling: union type + this.allowedTools = BinaryData.fromObject(allowedTools); + return this; + } + + /** + * Set the allowedTools property: The allowed_tools property. + * + * @param allowedTools the {@link McpToolFilter} to set. + * @return the McpTool object itself. + */ + public McpTool setAllowedTools(McpToolFilter allowedTools) { + // AI Tooling: union type + this.allowedTools = BinaryData.fromObject(allowedTools); + return this; + } + + /** + * Get the allowedTools property: The allowed_tools property. + * + * @return the allowedTools value as a list of Strings. + */ + @SuppressWarnings("unchecked") + public List getAllowedToolsAsStringList() { + // AI Tooling: union type + if (this.allowedTools == null) { + return null; + } + return this.allowedTools.toObject(List.class); + } + + /** + * Get the allowedTools property: The allowed_tools property. + * + * @return the allowedTools value as a {@link McpToolFilter}. + */ + public McpToolFilter getAllowedToolsAsMcpToolFilter() { + // AI Tooling: union type + if (this.allowedTools == null) { + return null; + } + return this.allowedTools.toObject(McpToolFilter.class); + } + /** * Get the requireApproval property: The require_approval property. * * @return the requireApproval value. */ - @Generated - public BinaryData getRequireApproval() { + BinaryData getRequireApproval() { + // AI Tooling: union type return this.requireApproval; } @@ -303,12 +355,62 @@ public BinaryData getRequireApproval() { * @param requireApproval the requireApproval value to set. * @return the McpTool object itself. */ - @Generated - public McpTool setRequireApproval(BinaryData requireApproval) { + McpTool setRequireApproval(BinaryData requireApproval) { + // AI Tooling: union type this.requireApproval = requireApproval; return this; } + /** + * Set the requireApproval property: The require_approval property. + * + * @param requireApproval the approval setting string to set (e.g., "always" or "never"). + * @return the McpTool object itself. + */ + public McpTool setRequireApproval(String requireApproval) { + // AI Tooling: union type + this.requireApproval = BinaryData.fromString(requireApproval); + return this; + } + + /** + * Set the requireApproval property: The require_approval property. + * + * @param requireApproval the {@link McpToolRequireApproval} filter to set. + * @return the McpTool object itself. + */ + public McpTool setRequireApproval(McpToolRequireApproval requireApproval) { + // AI Tooling: union type + this.requireApproval = BinaryData.fromObject(requireApproval); + return this; + } + + /** + * Get the requireApproval property: The require_approval property. + * + * @return the requireApproval value as a String. + */ + public String getRequireApprovalAsString() { + // AI Tooling: union type + if (this.requireApproval == null) { + return null; + } + return this.requireApproval.toObject(String.class); + } + + /** + * Get the requireApproval property: The require_approval property. + * + * @return the requireApproval value as a {@link McpToolRequireApproval}. + */ + public McpToolRequireApproval getRequireApprovalAsMcpToolRequireApproval() { + // AI Tooling: union type + if (this.requireApproval == null) { + return null; + } + return this.requireApproval.toObject(McpToolRequireApproval.class); + } + /** * Get the projectConnectionId property: The connection ID in the project for the MCP server. The connection stores * authentication and other connection details needed to connect to the MCP server. diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/CustomCodeInterpreterAsync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/CustomCodeInterpreterAsync.java index c29f56fa4ec8..1d2d25b7ea45 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/CustomCodeInterpreterAsync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/CustomCodeInterpreterAsync.java @@ -10,7 +10,6 @@ import com.azure.ai.agents.models.AgentVersionDetails; import com.azure.ai.agents.models.McpTool; import com.azure.ai.agents.models.PromptAgentDefinition; -import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; import com.openai.models.responses.ResponseCreateParams; @@ -51,7 +50,7 @@ public static void main(String[] args) { McpTool customCodeInterpreter = new McpTool("custom-code-interpreter") .setServerUrl(mcpServerUrl) .setProjectConnectionId(connectionId) - .setRequireApproval(BinaryData.fromObject("never")); + .setRequireApproval("never"); PromptAgentDefinition agentDefinition = new PromptAgentDefinition(model) .setInstructions("You are a helpful assistant that can run Python code to analyze data and solve problems.") diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/CustomCodeInterpreterSync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/CustomCodeInterpreterSync.java index b10a07f0ee1b..6beed15813c8 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/CustomCodeInterpreterSync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/CustomCodeInterpreterSync.java @@ -10,7 +10,6 @@ import com.azure.ai.agents.models.AgentVersionDetails; import com.azure.ai.agents.models.McpTool; import com.azure.ai.agents.models.PromptAgentDefinition; -import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; import com.openai.models.responses.Response; @@ -49,7 +48,7 @@ public static void main(String[] args) { McpTool customCodeInterpreter = new McpTool("custom-code-interpreter") .setServerUrl(mcpServerUrl) .setProjectConnectionId(connectionId) - .setRequireApproval(BinaryData.fromObject("never")); + .setRequireApproval("never"); PromptAgentDefinition agentDefinition = new PromptAgentDefinition(model) .setInstructions("You are a helpful assistant that can run Python code to analyze data and solve problems.") diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpAsync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpAsync.java index 26ba9880d50c..2842f193bb8b 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpAsync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpAsync.java @@ -10,7 +10,6 @@ import com.azure.ai.agents.models.AgentVersionDetails; import com.azure.ai.agents.models.McpTool; import com.azure.ai.agents.models.PromptAgentDefinition; -import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; import com.openai.models.responses.ResponseCreateParams; @@ -56,7 +55,7 @@ public static void main(String[] args) { // Uses gitmcp.io to expose a GitHub repository as an MCP-compatible server McpTool tool = new McpTool("api-specs") .setServerUrl("https://gitmcp.io/Azure/azure-rest-api-specs") - .setRequireApproval(BinaryData.fromObject("always")); + .setRequireApproval("always"); PromptAgentDefinition agentDefinition = new PromptAgentDefinition(model) .setInstructions("You are a helpful agent that can use MCP tools to assist users. " diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpSync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpSync.java index 75cf8157bf22..6c4a3e6b4f05 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpSync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpSync.java @@ -10,7 +10,6 @@ import com.azure.ai.agents.models.AgentVersionDetails; import com.azure.ai.agents.models.McpTool; import com.azure.ai.agents.models.PromptAgentDefinition; -import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; import com.openai.models.responses.Response; @@ -56,7 +55,7 @@ public static void main(String[] args) { // Uses gitmcp.io to expose a GitHub repository as an MCP-compatible server McpTool tool = new McpTool("api-specs") .setServerUrl("https://gitmcp.io/Azure/azure-rest-api-specs") - .setRequireApproval(BinaryData.fromObject("always")); + .setRequireApproval("always"); // END: com.azure.ai.agents.built_in_mcp // Create the agent definition with MCP tool enabled diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionAsync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionAsync.java index 04fd7d2bf5d0..0dc0f05ff444 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionAsync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionAsync.java @@ -10,7 +10,6 @@ import com.azure.ai.agents.models.AgentVersionDetails; import com.azure.ai.agents.models.McpTool; import com.azure.ai.agents.models.PromptAgentDefinition; -import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; import com.openai.models.responses.ResponseCreateParams; @@ -53,7 +52,7 @@ public static void main(String[] args) { McpTool mcpTool = new McpTool("api-specs") .setServerUrl("https://api.githubcopilot.com/mcp") .setProjectConnectionId(mcpConnectionId) - .setRequireApproval(BinaryData.fromObject("always")); + .setRequireApproval("always"); PromptAgentDefinition agentDefinition = new PromptAgentDefinition(model) .setInstructions("Use MCP tools as needed") diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionSync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionSync.java index c28d421a8a14..a9bf5bf04c31 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionSync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/McpWithConnectionSync.java @@ -11,7 +11,6 @@ import com.azure.ai.agents.models.AgentVersionDetails; import com.azure.ai.agents.models.McpTool; import com.azure.ai.agents.models.PromptAgentDefinition; -import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; import com.openai.models.conversations.Conversation; @@ -58,7 +57,7 @@ public static void main(String[] args) { McpTool mcpTool = new McpTool("api-specs") .setServerUrl("https://api.githubcopilot.com/mcp") .setProjectConnectionId(mcpConnectionId) - .setRequireApproval(BinaryData.fromObject("always")); + .setRequireApproval("always"); // END: com.azure.ai.agents.define_mcp_with_connection // Create agent with MCP tool diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ToolsAsyncTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ToolsAsyncTests.java index ef667ef08902..13a61b709c1f 100644 --- a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ToolsAsyncTests.java +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ToolsAsyncTests.java @@ -252,7 +252,7 @@ public void mcpToolEndToEnd(HttpClient httpClient, AgentsServiceVersion serviceV ResponsesAsyncClient responsesClient = getResponsesAsyncClient(httpClient, serviceVersion); McpTool tool = new McpTool("api-specs").setServerUrl("https://gitmcp.io/Azure/azure-rest-api-specs") - .setRequireApproval(BinaryData.fromObject("always")); + .setRequireApproval("always"); PromptAgentDefinition agentDefinition = new PromptAgentDefinition("gpt-4o") .setInstructions("You are a helpful agent that can use MCP tools to assist users.") diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ToolsTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ToolsTests.java index 0b5305fd3014..96b26e6f2353 100644 --- a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ToolsTests.java +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/ToolsTests.java @@ -251,7 +251,7 @@ public void mcpToolEndToEnd(HttpClient httpClient, AgentsServiceVersion serviceV ResponsesClient responsesClient = getResponsesSyncClient(httpClient, serviceVersion); McpTool tool = new McpTool("api-specs").setServerUrl("https://gitmcp.io/Azure/azure-rest-api-specs") - .setRequireApproval(BinaryData.fromObject("always")); + .setRequireApproval("always"); PromptAgentDefinition agentDefinition = new PromptAgentDefinition("gpt-4o") .setInstructions("You are a helpful agent that can use MCP tools to assist users.") diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/CodeInterpreterToolSerializationTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/CodeInterpreterToolSerializationTests.java new file mode 100644 index 000000000000..9374a454f5b2 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/CodeInterpreterToolSerializationTests.java @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.models; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for CodeInterpreterTool serialization, focusing on the container union type handling. + * The container property is a union type: string (container ID) | AutoCodeInterpreterToolParam. + */ +public class CodeInterpreterToolSerializationTests { + + /** + * Tests serialization when container is not set (null). + */ + @Test + public void testSerializationWithoutContainer() throws IOException { + CodeInterpreterTool tool = new CodeInterpreterTool(); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"type\":\"code_interpreter\"")); + assertFalse(json.contains("\"container\"")); + } + + /** + * Tests serialization with container set to a string container ID. + */ + @Test + public void testSerializationWithContainerIdString() throws IOException { + CodeInterpreterTool tool = new CodeInterpreterTool(); + tool.setContainer("my-container-id"); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"container\"")); + assertTrue(json.contains("my-container-id")); + } + + /** + * Tests serialization with container set to an AutoCodeInterpreterToolParam. + */ + @Test + public void testSerializationWithAutoCodeInterpreterToolParam() throws IOException { + AutoCodeInterpreterToolParam autoParam + = new AutoCodeInterpreterToolParam().setFileIds(Arrays.asList("file-1", "file-2")) + .setMemoryLimit(ContainerMemoryLimit.MEMORY_4GB); + + CodeInterpreterTool tool = new CodeInterpreterTool(); + tool.setContainer(autoParam); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"container\"")); + assertTrue(json.contains("\"type\":\"auto\"")); + assertTrue(json.contains("file-1")); + assertTrue(json.contains("file-2")); + } + + /** + * Tests serialization with a minimal AutoCodeInterpreterToolParam (no file IDs). + */ + @Test + public void testSerializationWithMinimalAutoCodeInterpreterToolParam() throws IOException { + AutoCodeInterpreterToolParam autoParam = new AutoCodeInterpreterToolParam(); + + CodeInterpreterTool tool = new CodeInterpreterTool(); + tool.setContainer(autoParam); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"container\"")); + assertTrue(json.contains("\"type\":\"auto\"")); + } + + /** + * Tests deserialization with container set to a string container ID. + */ + @Test + public void testDeserializationWithContainerIdString() throws IOException { + String json = "{\"type\":\"code_interpreter\",\"container\":\"my-container-id\"}"; + + CodeInterpreterTool tool = deserializeFromJson(json); + + assertNotNull(tool); + assertEquals(ToolType.CODE_INTERPRETER, tool.getType()); + assertNotNull(tool.getContainerAsString()); + assertTrue(tool.getContainerAsString().contains("my-container-id")); + } + + /** + * Tests deserialization with container set to an AutoCodeInterpreterToolParam object. + */ + @Test + public void testDeserializationWithAutoCodeInterpreterToolParam() throws IOException { + String json + = "{\"type\":\"code_interpreter\",\"container\":{\"type\":\"auto\",\"file_ids\":[\"file-1\",\"file-2\"]}}"; + + CodeInterpreterTool tool = deserializeFromJson(json); + + assertNotNull(tool); + assertEquals(ToolType.CODE_INTERPRETER, tool.getType()); + assertNotNull(tool.getContainerAsAutoCodeInterpreterToolParam()); + AutoCodeInterpreterToolParam autoParam = tool.getContainerAsAutoCodeInterpreterToolParam(); + assertEquals("auto", autoParam.getType()); + assertNotNull(autoParam.getFileIds()); + assertEquals(2, autoParam.getFileIds().size()); + } + + /** + * Tests deserialization with container absent. + */ + @Test + public void testDeserializationWithoutContainer() throws IOException { + String json = "{\"type\":\"code_interpreter\"}"; + + CodeInterpreterTool tool = deserializeFromJson(json); + + assertNotNull(tool); + assertNull(tool.getContainerAsString()); + assertNull(tool.getContainerAsAutoCodeInterpreterToolParam()); + } + + /** + * Tests round-trip serialization/deserialization with a container ID string. + */ + @Test + public void testRoundTripWithContainerIdString() throws IOException { + CodeInterpreterTool original = new CodeInterpreterTool(); + original.setContainer("my-container-id"); + + String json = serializeToJson(original); + CodeInterpreterTool deserialized = deserializeFromJson(json); + + assertNotNull(deserialized); + assertNotNull(deserialized.getContainerAsString()); + assertTrue(deserialized.getContainerAsString().contains("my-container-id")); + } + + /** + * Tests round-trip serialization/deserialization with an AutoCodeInterpreterToolParam. + */ + @Test + public void testRoundTripWithAutoCodeInterpreterToolParam() throws IOException { + AutoCodeInterpreterToolParam autoParam + = new AutoCodeInterpreterToolParam().setFileIds(Arrays.asList("file-a", "file-b")); + + CodeInterpreterTool original = new CodeInterpreterTool(); + original.setContainer(autoParam); + + String json = serializeToJson(original); + CodeInterpreterTool deserialized = deserializeFromJson(json); + + assertNotNull(deserialized); + AutoCodeInterpreterToolParam deserializedParam = deserialized.getContainerAsAutoCodeInterpreterToolParam(); + assertNotNull(deserializedParam); + assertEquals("auto", deserializedParam.getType()); + assertEquals(2, deserializedParam.getFileIds().size()); + } + + /** + * Regression: container string must not be double-quoted in serialized JSON. + * Setting via setContainer(String) must produce "container":"value", never "container":"\"value\"". + */ + @Test + public void testContainerStringNoDoubleQuoting() throws IOException { + CodeInterpreterTool tool = new CodeInterpreterTool(); + tool.setContainer("cntr-abc-123"); + + String json = serializeToJson(tool); + + assertTrue(json.contains("\"container\":\"cntr-abc-123\""), + "Container string must not be double-quoted, got: " + json); + assertFalse(json.contains("\\\"cntr-abc-123\\\""), + "Container string must not have escaped quotes, got: " + json); + } + + /** + * Regression: container string getter must return the plain string after round-trip, + * not a JSON-encoded value with surrounding quotes. + */ + @Test + public void testContainerStringRoundTripNoExtraQuotes() throws IOException { + CodeInterpreterTool original = new CodeInterpreterTool(); + original.setContainer("cntr-abc-123"); + + String json = serializeToJson(original); + CodeInterpreterTool deserialized = deserializeFromJson(json); + + assertEquals("cntr-abc-123", deserialized.getContainerAsString(), + "Round-tripped container string must not have extra quotes"); + } + + /** + * Regression: container string deserialized from JSON must return the plain string, + * not a JSON-encoded value with surrounding quotes. + */ + @Test + public void testContainerStringDeserializationNoExtraQuotes() throws IOException { + String json = "{\"type\":\"code_interpreter\",\"container\":\"my-id-456\"}"; + + CodeInterpreterTool tool = deserializeFromJson(json); + + assertEquals("my-id-456", tool.getContainerAsString(), + "Deserialized container string must not have extra quotes"); + } + + // Helper method to serialize to JSON string + private String serializeToJson(CodeInterpreterTool tool) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + tool.toJson(jsonWriter); + } + return outputStream.toString("UTF-8"); + } + + // Helper method to deserialize from JSON string + private CodeInterpreterTool deserializeFromJson(String json) throws IOException { + try (JsonReader jsonReader = JsonProviders.createReader(json)) { + return CodeInterpreterTool.fromJson(jsonReader); + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/McpToolSerializationTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/McpToolSerializationTests.java new file mode 100644 index 000000000000..239d293baf55 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/models/McpToolSerializationTests.java @@ -0,0 +1,422 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.models; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for McpTool serialization, focusing on the allowedTools and requireApproval union type handling. + * allowedTools is a union type: List<String> | McpToolFilter. + * requireApproval is a union type: String ("always"/"never") | McpToolRequireApproval. + */ +public class McpToolSerializationTests { + + private static final String TEST_SERVER_LABEL = "test-mcp-server"; + + // ===== allowedTools tests ===== + + /** + * Tests serialization when allowedTools is not set (null). + */ + @Test + public void testSerializationWithoutAllowedTools() throws IOException { + McpTool tool = new McpTool(TEST_SERVER_LABEL); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"server_label\":\"test-mcp-server\"")); + assertFalse(json.contains("\"allowed_tools\"")); + } + + /** + * Tests serialization with allowedTools set to a list of tool names. + */ + @Test + public void testSerializationWithAllowedToolsAsStringList() throws IOException { + McpTool tool = new McpTool(TEST_SERVER_LABEL).setAllowedTools(Arrays.asList("tool_a", "tool_b", "tool_c")); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"allowed_tools\"")); + assertTrue(json.contains("tool_a")); + assertTrue(json.contains("tool_b")); + assertTrue(json.contains("tool_c")); + } + + /** + * Tests serialization with allowedTools set to an McpToolFilter. + */ + @Test + public void testSerializationWithAllowedToolsAsMcpToolFilter() throws IOException { + McpToolFilter filter = new McpToolFilter().setToolNames(Arrays.asList("func_1", "func_2")).setReadOnly(true); + + McpTool tool = new McpTool(TEST_SERVER_LABEL).setAllowedTools(filter); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"allowed_tools\"")); + assertTrue(json.contains("func_1")); + assertTrue(json.contains("func_2")); + assertTrue(json.contains("\"read_only\":true")); + } + + /** + * Tests deserialization with allowedTools set to a list of tool names. + */ + @Test + public void testDeserializationWithAllowedToolsAsStringList() throws IOException { + String json + = "{\"server_label\":\"test-mcp-server\",\"type\":\"mcp\",\"allowed_tools\":[\"tool_a\",\"tool_b\"]}"; + + McpTool tool = deserializeFromJson(json); + + assertNotNull(tool); + assertEquals(TEST_SERVER_LABEL, tool.getServerLabel()); + List toolNames = tool.getAllowedToolsAsStringList(); + assertNotNull(toolNames); + assertEquals(2, toolNames.size()); + assertTrue(toolNames.contains("tool_a")); + assertTrue(toolNames.contains("tool_b")); + } + + /** + * Tests deserialization with allowedTools set to an McpToolFilter object. + */ + @Test + public void testDeserializationWithAllowedToolsAsMcpToolFilter() throws IOException { + String json + = "{\"server_label\":\"test-mcp-server\",\"type\":\"mcp\",\"allowed_tools\":{\"tool_names\":[\"func_1\"],\"read_only\":true}}"; + + McpTool tool = deserializeFromJson(json); + + assertNotNull(tool); + McpToolFilter filter = tool.getAllowedToolsAsMcpToolFilter(); + assertNotNull(filter); + assertNotNull(filter.getToolNames()); + assertEquals(1, filter.getToolNames().size()); + assertEquals("func_1", filter.getToolNames().get(0)); + assertEquals(true, filter.isReadOnly()); + } + + /** + * Tests deserialization with allowedTools absent. + */ + @Test + public void testDeserializationWithoutAllowedTools() throws IOException { + String json = "{\"server_label\":\"test-mcp-server\",\"type\":\"mcp\"}"; + + McpTool tool = deserializeFromJson(json); + + assertNotNull(tool); + assertNull(tool.getAllowedToolsAsStringList()); + assertNull(tool.getAllowedToolsAsMcpToolFilter()); + } + + /** + * Tests round-trip with allowedTools as a list of tool names. + */ + @Test + public void testRoundTripWithAllowedToolsAsStringList() throws IOException { + McpTool original = new McpTool(TEST_SERVER_LABEL).setAllowedTools(Arrays.asList("my_tool_1", "my_tool_2")); + + String json = serializeToJson(original); + McpTool deserialized = deserializeFromJson(json); + + assertNotNull(deserialized); + List tools = deserialized.getAllowedToolsAsStringList(); + assertNotNull(tools); + assertEquals(2, tools.size()); + } + + /** + * Tests round-trip with allowedTools as an McpToolFilter. + */ + @Test + public void testRoundTripWithAllowedToolsAsMcpToolFilter() throws IOException { + McpToolFilter filter = new McpToolFilter().setToolNames(Arrays.asList("func_x")).setReadOnly(false); + + McpTool original = new McpTool(TEST_SERVER_LABEL).setAllowedTools(filter); + + String json = serializeToJson(original); + McpTool deserialized = deserializeFromJson(json); + + assertNotNull(deserialized); + McpToolFilter deserializedFilter = deserialized.getAllowedToolsAsMcpToolFilter(); + assertNotNull(deserializedFilter); + assertEquals(1, deserializedFilter.getToolNames().size()); + } + + // ===== requireApproval tests ===== + + /** + * Tests serialization when requireApproval is not set (null). + */ + @Test + public void testSerializationWithoutRequireApproval() throws IOException { + McpTool tool = new McpTool(TEST_SERVER_LABEL); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertFalse(json.contains("\"require_approval\"")); + } + + /** + * Tests serialization with requireApproval set to "always" string. + */ + @Test + public void testSerializationWithRequireApprovalAlwaysString() throws IOException { + McpTool tool = new McpTool(TEST_SERVER_LABEL).setRequireApproval("always"); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"require_approval\"")); + assertTrue(json.contains("always")); + } + + /** + * Tests serialization with requireApproval set to "never" string. + */ + @Test + public void testSerializationWithRequireApprovalNeverString() throws IOException { + McpTool tool = new McpTool(TEST_SERVER_LABEL).setRequireApproval("never"); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"require_approval\"")); + assertTrue(json.contains("never")); + } + + /** + * Tests serialization with requireApproval set to an McpToolRequireApproval filter. + */ + @Test + public void testSerializationWithRequireApprovalAsMcpToolRequireApproval() throws IOException { + McpToolFilter alwaysFilter = new McpToolFilter().setToolNames(Arrays.asList("dangerous_tool")); + McpToolFilter neverFilter = new McpToolFilter().setToolNames(Arrays.asList("safe_tool")); + McpToolRequireApproval approval = new McpToolRequireApproval().setAlways(alwaysFilter).setNever(neverFilter); + + McpTool tool = new McpTool(TEST_SERVER_LABEL).setRequireApproval(approval); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"require_approval\"")); + assertTrue(json.contains("dangerous_tool")); + assertTrue(json.contains("safe_tool")); + assertTrue(json.contains("\"always\"")); + assertTrue(json.contains("\"never\"")); + } + + /** + * Tests deserialization with requireApproval set to "always" string. + */ + @Test + public void testDeserializationWithRequireApprovalAlwaysString() throws IOException { + String json = "{\"server_label\":\"test-mcp-server\",\"type\":\"mcp\",\"require_approval\":\"always\"}"; + + McpTool tool = deserializeFromJson(json); + + assertNotNull(tool); + assertNotNull(tool.getRequireApprovalAsString()); + assertTrue(tool.getRequireApprovalAsString().contains("always")); + } + + /** + * Tests deserialization with requireApproval set to "never" string. + */ + @Test + public void testDeserializationWithRequireApprovalNeverString() throws IOException { + String json = "{\"server_label\":\"test-mcp-server\",\"type\":\"mcp\",\"require_approval\":\"never\"}"; + + McpTool tool = deserializeFromJson(json); + + assertNotNull(tool); + assertNotNull(tool.getRequireApprovalAsString()); + assertTrue(tool.getRequireApprovalAsString().contains("never")); + } + + /** + * Tests deserialization with requireApproval set to an McpToolRequireApproval object. + */ + @Test + public void testDeserializationWithRequireApprovalAsMcpToolRequireApproval() throws IOException { + String json + = "{\"server_label\":\"test-mcp-server\",\"type\":\"mcp\",\"require_approval\":{\"always\":{\"tool_names\":[\"dangerous_tool\"]},\"never\":{\"tool_names\":[\"safe_tool\"]}}}"; + + McpTool tool = deserializeFromJson(json); + + assertNotNull(tool); + McpToolRequireApproval approval = tool.getRequireApprovalAsMcpToolRequireApproval(); + assertNotNull(approval); + assertNotNull(approval.getAlways()); + assertNotNull(approval.getNever()); + assertEquals(1, approval.getAlways().getToolNames().size()); + assertEquals("dangerous_tool", approval.getAlways().getToolNames().get(0)); + assertEquals("safe_tool", approval.getNever().getToolNames().get(0)); + } + + /** + * Tests deserialization with requireApproval absent. + */ + @Test + public void testDeserializationWithoutRequireApproval() throws IOException { + String json = "{\"server_label\":\"test-mcp-server\",\"type\":\"mcp\"}"; + + McpTool tool = deserializeFromJson(json); + + assertNotNull(tool); + assertNull(tool.getRequireApprovalAsString()); + assertNull(tool.getRequireApprovalAsMcpToolRequireApproval()); + } + + /** + * Tests round-trip with requireApproval as "always" string. + */ + @Test + public void testRoundTripWithRequireApprovalAlwaysString() throws IOException { + McpTool original = new McpTool(TEST_SERVER_LABEL).setRequireApproval("always"); + + String json = serializeToJson(original); + McpTool deserialized = deserializeFromJson(json); + + assertNotNull(deserialized); + assertNotNull(deserialized.getRequireApprovalAsString()); + assertTrue(deserialized.getRequireApprovalAsString().contains("always")); + } + + /** + * Tests round-trip with requireApproval as McpToolRequireApproval. + */ + @Test + public void testRoundTripWithRequireApprovalAsMcpToolRequireApproval() throws IOException { + McpToolFilter alwaysFilter = new McpToolFilter().setToolNames(Arrays.asList("tool_1")); + McpToolRequireApproval approval = new McpToolRequireApproval().setAlways(alwaysFilter); + + McpTool original = new McpTool(TEST_SERVER_LABEL).setRequireApproval(approval); + + String json = serializeToJson(original); + McpTool deserialized = deserializeFromJson(json); + + assertNotNull(deserialized); + McpToolRequireApproval deserializedApproval = deserialized.getRequireApprovalAsMcpToolRequireApproval(); + assertNotNull(deserializedApproval); + assertNotNull(deserializedApproval.getAlways()); + assertEquals(1, deserializedApproval.getAlways().getToolNames().size()); + } + + /** + * Tests serialization with both allowedTools and requireApproval set. + */ + @Test + public void testSerializationWithBothAllowedToolsAndRequireApproval() throws IOException { + McpTool tool = new McpTool(TEST_SERVER_LABEL).setServerUrl("https://mcp.example.com") + .setAllowedTools(Arrays.asList("tool_1", "tool_2")) + .setRequireApproval("always"); + + String json = serializeToJson(tool); + + assertNotNull(json); + assertTrue(json.contains("\"server_label\":\"test-mcp-server\"")); + assertTrue(json.contains("\"server_url\":\"https://mcp.example.com\"")); + assertTrue(json.contains("\"allowed_tools\"")); + assertTrue(json.contains("\"require_approval\"")); + } + + // ===== Regression: no double-quoting ===== + + /** + * Regression: requireApproval string must not be double-quoted in serialized JSON. + * Setting via setRequireApproval(String) must produce "require_approval":"always", + * never "require_approval":"\"always\"". + */ + @Test + public void testRequireApprovalStringNoDoubleQuoting() throws IOException { + McpTool tool = new McpTool(TEST_SERVER_LABEL).setRequireApproval("always"); + + String json = serializeToJson(tool); + + assertTrue(json.contains("\"require_approval\":\"always\""), + "requireApproval string must not be double-quoted, got: " + json); + assertFalse(json.contains("\\\"always\\\""), + "requireApproval string must not have escaped quotes, got: " + json); + } + + /** + * Regression: requireApproval "never" string must not be double-quoted in serialized JSON. + */ + @Test + public void testRequireApprovalNeverStringNoDoubleQuoting() throws IOException { + McpTool tool = new McpTool(TEST_SERVER_LABEL).setRequireApproval("never"); + + String json = serializeToJson(tool); + + assertTrue(json.contains("\"require_approval\":\"never\""), + "requireApproval string must not be double-quoted, got: " + json); + } + + /** + * Regression: requireApproval string getter must return the plain string after round-trip, + * not a JSON-encoded value with surrounding quotes. + */ + @Test + public void testRequireApprovalStringRoundTripNoExtraQuotes() throws IOException { + McpTool original = new McpTool(TEST_SERVER_LABEL).setRequireApproval("always"); + + String json = serializeToJson(original); + McpTool deserialized = deserializeFromJson(json); + + assertEquals("always", deserialized.getRequireApprovalAsString(), + "Round-tripped requireApproval string must not have extra quotes"); + } + + /** + * Regression: requireApproval string deserialized from JSON must return the plain string, + * not a JSON-encoded value with surrounding quotes. + */ + @Test + public void testRequireApprovalStringDeserializationNoExtraQuotes() throws IOException { + String json = "{\"server_label\":\"test-mcp-server\",\"type\":\"mcp\",\"require_approval\":\"never\"}"; + + McpTool tool = deserializeFromJson(json); + + assertEquals("never", tool.getRequireApprovalAsString(), + "Deserialized requireApproval string must not have extra quotes"); + } + + // Helper method to serialize to JSON string + private String serializeToJson(McpTool tool) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + tool.toJson(jsonWriter); + } + return outputStream.toString("UTF-8"); + } + + // Helper method to deserialize from JSON string + private McpTool deserializeFromJson(String json) throws IOException { + try (JsonReader jsonReader = JsonProviders.createReader(json)) { + return McpTool.fromJson(jsonReader); + } + } +}