diff --git a/sdk/ai/azure-ai-agents/assets.json b/sdk/ai/azure-ai-agents/assets.json index 08d8cb4c7c20..42ea9478529a 100644 --- a/sdk/ai/azure-ai-agents/assets.json +++ b/sdk/ai/azure-ai-agents/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/ai/azure-ai-agents", - "Tag": "java/ai/azure-ai-agents_34d0d1c5d4" + "Tag": "java/ai/azure-ai-agents_69bc682982" } \ No newline at end of file diff --git a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/OpenApiFunctionDefinition.java b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/OpenApiFunctionDefinition.java index 2eedcbc561cf..085e214d84b1 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/OpenApiFunctionDefinition.java +++ b/sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/models/OpenApiFunctionDefinition.java @@ -6,11 +6,14 @@ import com.azure.core.annotation.Fluent; import com.azure.core.annotation.Generated; import com.azure.core.util.BinaryData; +import com.azure.json.JsonProviders; import com.azure.json.JsonReader; import com.azure.json.JsonSerializable; import com.azure.json.JsonToken; import com.azure.json.JsonWriter; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Map; @@ -197,6 +200,22 @@ public OpenApiFunctionDefinition(String name, Map spec, Open this.auth = auth; } + /** + * Reads an OpenAPI specification from a JSON file and returns it as a {@code Map} + * suitable for the {@code spec} parameter of + * {@link #OpenApiFunctionDefinition(String, Map, OpenApiAuthDetails)}. + * + * @param path the path to the OpenAPI spec JSON file. + * @return the spec as a map of top-level keys to their serialized values. + * @throws IOException if the file cannot be read or parsed. + */ + public static Map readSpecFromFile(Path path) throws IOException { + try (JsonReader reader = JsonProviders.createReader(Files.readAllBytes(path))) { + return reader + .readMap(r -> r.getNullable(nonNullReader -> BinaryData.fromObject(nonNullReader.readUntyped()))); + } + } + /* * List of OpenAPI spec parameters that will use user-provided defaults */ diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/SampleUtils.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/SampleUtils.java new file mode 100644 index 000000000000..bdfa7bad1229 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/SampleUtils.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents; + +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class SampleUtils { + + /** + * Gets the path to a file in the sample resource folder. + * @param fileName the name of the file in the sample resource folder + * @return Path to the sample resource file + */ + public static Path getResourcePath(String fileName) { + try { + URL resourceUrl = SampleUtils.class.getClassLoader().getResource(fileName); + if (resourceUrl == null) { + throw new RuntimeException("Sample resource file not found: " + fileName); + } + return Paths.get(resourceUrl.toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid URI for sample resource: " + fileName, e); + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/OpenApiSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/OpenApiSample.java new file mode 100644 index 000000000000..1823d374c882 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/OpenApiSample.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.tools; + +import com.azure.ai.agents.AgentsClient; +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.ConversationsClient; +import com.azure.ai.agents.ResponsesClient; +import com.azure.ai.agents.SampleUtils; +import com.azure.ai.agents.models.AgentReference; +import com.azure.ai.agents.models.AgentVersionDetails; +import com.azure.ai.agents.models.OpenApiAnonymousAuthDetails; +import com.azure.ai.agents.models.OpenApiFunctionDefinition; +import com.azure.ai.agents.models.OpenApiTool; +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; +import com.openai.models.conversations.items.ItemCreateParams; +import com.openai.models.responses.EasyInputMessage; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; + +import java.util.Arrays; +import java.util.Map; + +/** + * This sample demonstrates how to create an agent with an OpenAPI tool that calls + * an external API defined by an OpenAPI specification file. + * + *

Before running the sample, set these environment variables:

+ *
    + *
  • FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint.
  • + *
  • FOUNDRY_MODEL_DEPLOYMENT_NAME - The model deployment name.
  • + *
+ * + *

Also place an OpenAPI spec JSON file at {@code src/samples/resources/assets/httpbin_openapi.json}.

+ */ +public class OpenApiSample { + public static void main(String[] args) throws Exception { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String model = Configuration.getGlobalConfiguration().get("FOUNDRY_MODEL_DEPLOYMENT_NAME"); + + AgentsClientBuilder builder = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint); + + AgentsClient agentsClient = builder.buildAgentsClient(); + ResponsesClient responsesClient = builder.buildResponsesClient(); + ConversationsClient conversationsClient = builder.buildConversationsClient(); + + + // Load the OpenAPI spec from a JSON file + Map spec = OpenApiFunctionDefinition.readSpecFromFile( + SampleUtils.getResourcePath("assets/httpbin_openapi.json")); + + OpenApiFunctionDefinition toolDefinition = new OpenApiFunctionDefinition( + "httpbin_get", + spec, + new OpenApiAnonymousAuthDetails()) + .setDescription("Get request metadata from an OpenAPI endpoint."); + + PromptAgentDefinition agentDefinition = new PromptAgentDefinition(model) + .setInstructions("Use the OpenAPI tool for HTTP request metadata.") + .setTools(Arrays.asList(new OpenApiTool(toolDefinition))); + + AgentVersionDetails agentVersion = agentsClient.createAgentVersion("openapi-agent", agentDefinition); + System.out.println("Agent: " + agentVersion.getName() + ", version: " + agentVersion.getVersion()); + + // Create a conversation and add a user message + Conversation conversation = conversationsClient.getConversationService().create(); + conversationsClient.getConversationService().items().create( + ItemCreateParams.builder() + .conversationId(conversation.id()) + .addItem(EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("Use the OpenAPI tool and summarize the returned URL and origin in one sentence.") + .build()) + .build()); + + try { + AgentReference agentReference = new AgentReference(agentVersion.getName()) + .setVersion(agentVersion.getVersion()); + + ResponseCreateParams.Builder options = ResponseCreateParams.builder() + .maxOutputTokens(300L); + + Response response = responsesClient.createWithAgentConversation( + agentReference, conversation.id(), options); + + String text = response.output().stream() + .filter(item -> item.isMessage()) + .map(item -> item.asMessage().content() + .get(item.asMessage().content().size() - 1) + .asOutputText() + .text()) + .reduce((first, second) -> second) + .orElse(""); + + System.out.println("Status: " + response.status().map(Object::toString).orElse("unknown")); + System.out.println("Response: " + text); + } finally { + agentsClient.deleteAgentVersion(agentVersion.getName(), agentVersion.getVersion()); + System.out.println("Agent deleted"); + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/resources/assets/httpbin_openapi.json b/sdk/ai/azure-ai-agents/src/samples/resources/assets/httpbin_openapi.json new file mode 100644 index 000000000000..92b037313537 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/resources/assets/httpbin_openapi.json @@ -0,0 +1,39 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "httpbin", + "version": "1.0.0", + "description": "A simple HTTP request/response service" + }, + "servers": [ + { + "url": "https://httpbin.org" + } + ], + "paths": { + "/get": { + "get": { + "operationId": "httpbin_get", + "summary": "Returns the GET request data including headers, origin IP, and URL", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "args": { "type": "object" }, + "headers": { "type": "object" }, + "origin": { "type": "string" }, + "url": { "type": "string" } + } + } + } + } + } + } + } + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/OpenAPIToolTests.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/OpenAPIToolTests.java new file mode 100644 index 000000000000..c1d814a798b8 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/OpenAPIToolTests.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents; + +import com.azure.ai.agents.models.AgentReference; +import com.azure.ai.agents.models.AgentVersionDetails; +import com.azure.ai.agents.models.OpenApiAnonymousAuthDetails; +import com.azure.ai.agents.models.OpenApiFunctionDefinition; +import com.azure.ai.agents.models.OpenApiTool; +import com.azure.ai.agents.models.PromptAgentDefinition; +import com.azure.core.http.HttpClient; +import com.azure.core.util.BinaryData; +import com.openai.models.conversations.Conversation; +import com.openai.models.conversations.items.ItemCreateParams; +import com.openai.models.responses.EasyInputMessage; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseStatus; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +import static com.azure.ai.agents.TestUtils.DISPLAY_NAME_WITH_ARGUMENTS; +import static org.junit.jupiter.api.Assertions.*; + +public class OpenAPIToolTests extends ClientTestBase { + + private static final String AGENT_NAME = "openapi-tool-test-agent-java"; + + @ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS) + @MethodSource("com.azure.ai.agents.TestUtils#getTestParameters") + public void openApiToolEndToEnd(HttpClient httpClient, AgentsServiceVersion serviceVersion) throws IOException { + AgentsClient agentsClient = getAgentsSyncClient(httpClient, serviceVersion); + ResponsesClient responsesClient = getResponsesSyncClient(httpClient, serviceVersion); + ConversationsClient conversationsClient = getConversationsSyncClient(httpClient, serviceVersion); + + // Load the OpenAPI spec from test resources + Map spec + = OpenApiFunctionDefinition.readSpecFromFile(TestUtils.getTestResourcePath("assets/httpbin_openapi.json")); + + OpenApiFunctionDefinition toolDefinition + = new OpenApiFunctionDefinition("httpbin_get", spec, new OpenApiAnonymousAuthDetails()) + .setDescription("Get request metadata from an OpenAPI endpoint."); + + PromptAgentDefinition agentDefinition + = new PromptAgentDefinition("gpt-4o").setInstructions("Use the OpenAPI tool for HTTP request metadata.") + .setTools(Arrays.asList(new OpenApiTool(toolDefinition))); + + AgentVersionDetails agentVersion = agentsClient.createAgentVersion(AGENT_NAME, agentDefinition); + assertNotNull(agentVersion); + assertNotNull(agentVersion.getId()); + assertEquals(AGENT_NAME, agentVersion.getName()); + + try { + // Create a conversation and add a user message + Conversation conversation = conversationsClient.getConversationService().create(); + assertNotNull(conversation); + assertNotNull(conversation.id()); + + conversationsClient.getConversationService() + .items() + .create(ItemCreateParams.builder() + .conversationId(conversation.id()) + .addItem(EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("Use the OpenAPI tool and summarize the returned URL and origin in one sentence.") + .build()) + .build()); + + // Create a response using the agent with the conversation + AgentReference agentReference + = new AgentReference(agentVersion.getName()).setVersion(agentVersion.getVersion()); + + ResponseCreateParams.Builder options = ResponseCreateParams.builder().maxOutputTokens(300L); + + Response response = responsesClient.createWithAgentConversation(agentReference, conversation.id(), options); + + assertNotNull(response); + assertTrue(response.id().startsWith("resp")); + assertTrue(response.status().isPresent()); + assertEquals(ResponseStatus.COMPLETED, response.status().get()); + assertFalse(response.output().isEmpty()); + assertTrue(response.output().stream().anyMatch(item -> item.isMessage())); + } finally { + // Clean up + agentsClient.deleteAgentVersion(agentVersion.getName(), agentVersion.getVersion()); + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/test/resources/assets/httpbin_openapi.json b/sdk/ai/azure-ai-agents/src/test/resources/assets/httpbin_openapi.json new file mode 100644 index 000000000000..92b037313537 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/test/resources/assets/httpbin_openapi.json @@ -0,0 +1,39 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "httpbin", + "version": "1.0.0", + "description": "A simple HTTP request/response service" + }, + "servers": [ + { + "url": "https://httpbin.org" + } + ], + "paths": { + "/get": { + "get": { + "operationId": "httpbin_get", + "summary": "Returns the GET request data including headers, origin IP, and URL", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "args": { "type": "object" }, + "headers": { "type": "object" }, + "origin": { "type": "string" }, + "url": { "type": "string" } + } + } + } + } + } + } + } + } + } +}