diff --git a/sdk/ai/azure-ai-agents/CHANGELOG.md b/sdk/ai/azure-ai-agents/CHANGELOG.md index a626f1b89fef..2611fc2732fa 100644 --- a/sdk/ai/azure-ai-agents/CHANGELOG.md +++ b/sdk/ai/azure-ai-agents/CHANGELOG.md @@ -10,6 +10,12 @@ ### Other Changes +- Added sync and async conversation samples demonstrating the `x-ms-user-identity` header with the OpenAI ConversationService. +- Added sync and async samples for draft agent versions, reminder toolbox tools, hosted-agent enable/disable, + advanced memory-store workflows, and agent optimization. +- Improved the Fabric IQ sync and async samples with configurable agent names, readable response and annotation + output, and reliable asynchronous cleanup. + ## 2.4.0 (2026-08-19) ### Features Added diff --git a/sdk/ai/azure-ai-agents/README.md b/sdk/ai/azure-ai-agents/README.md index 29791c76ad2e..46df8730ab15 100644 --- a/sdk/ai/azure-ai-agents/README.md +++ b/sdk/ai/azure-ai-agents/README.md @@ -7,7 +7,7 @@ The client library uses a single service version `v1` of the AI Foundry [data pl > [!IMPORTANT] > **Preview and beta features** > - Build `Beta*Client` and `Beta*AsyncClient` instances through `AgentsClientBuilder.beta()`. These clients automatically opt in to their preview service area; you do not need `allowPreview(true)` for them. -> - Use `AgentsClientBuilder.allowPreview(true)` only when calling preview APIs on non-Beta clients, such as preview hosted-agent sessions, session files, and code package operations on `AgentsClient` / `AgentsAsyncClient`. +> - Use `AgentsClientBuilder.allowPreview(true)` when calling preview APIs on non-Beta clients, such as draft agent versions, hosted-agent sessions, session files, and code package operations on `AgentsClient` / `AgentsAsyncClient`. > - Classes and methods annotated with `@Beta` are preview API surface and may change in future releases. See [Preview operation groups and beta clients](#preview-operation-groups-and-beta-clients) for details. ## Documentation @@ -63,7 +63,7 @@ AgentsAsyncClient agentsAsyncClient = new AgentsClientBuilder() ``` The Agents client library has the following sub-clients which group the different operations that can be performed: -- `AgentsClient` / `AgentsAsyncClient`: Perform operations related to agents, such as creating, retrieving, updating, and deleting agents. When `allowPreview(true)` is configured, these clients can also use preview hosted-agent sessions, session files, and code package operations. +- `AgentsClient` / `AgentsAsyncClient`: Perform operations related to agents, such as creating, retrieving, updating, and deleting agents. When `allowPreview(true)` is configured, these clients can also use preview draft versions, hosted-agent sessions, session files, and code package operations. - `BetaAgentsClient` / `BetaAgentsAsyncClient` **(preview)**: Perform preview agent optimization operations. - `ResponsesClient` / `ResponsesAsyncClient`: Handle responses operations. See the [OpenAI's Responses API documentation][openai_responses_api_docs] for more information. - `BetaMemoryStoresClient` / `BetaMemoryStoresAsyncClient` **(preview)**: Manage memory stores and individual memory items for agents. @@ -110,6 +110,16 @@ ResponseService responseService = responsesClient.getResponseService(); ConversationService conversationService = openAIClient.conversations(); ``` +### Agent version drafts + +Draft agent versions are preview candidates that are not promoted to the agent's latest released version. Create one with +`CreateAgentVersionInput.setDraft(true)`, and pass `true` as the `includeDrafts` argument to +`listAgentVersions` when you need to list draft versions. Build the non-Beta client with +`allowPreview(true)` to opt in to the `DraftAgents=V1Preview` service feature. + +See the full samples in [AgentDraftSample.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/agents/AgentDraftSample.java) +and [AgentDraftAsyncSample.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/agents/AgentDraftAsyncSample.java). + ### Agent tools The SDK supports a variety of tools that can be attached to agent definitions. Some tools are generally available, while others are in **preview** and may change in future releases. @@ -176,10 +186,21 @@ Build clients whose names start with `Beta` from `AgentsClientBuilder.beta()`. T The async `Beta*AsyncClient` counterparts follow the same behavior. +### Agent optimization + +The preview `BetaAgentsClient` and `BetaAgentsAsyncClient` can create and monitor agent optimization jobs. These jobs +evaluate an agent against a registered dataset and evaluator, then return scored candidates for instructions, skills, +tools, or model improvements. Agent optimization is currently in preview and requires an allow-listed Foundry project. +See [Agent optimizer in Foundry Agent Service][agent_optimizer_overview] for the service workflow and the complete +examples in [AgentOptimizationSample.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/optimization/AgentOptimizationSample.java) +and [AgentOptimizationAsyncSample.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/optimization/AgentOptimizationAsyncSample.java). + ### Memory item management `BetaMemoryStoresClient` and `BetaMemoryStoresAsyncClient` manage memory stores and individual memory items. In addition to store-level operations, use `createMemory`, `updateMemory`, `listMemories`, `getMemory`, and `deleteMemory` to manage individual memories. `ListMemoriesOptions` supports filtering by scope and `MemoryItemKind`, including `MemoryItemKind.PROCEDURAL`. See `MemoryStoreItemsSample` and `MemoryStoreItemsAsyncSample` for complete examples. +For conversational memory workflows, use `beginUpdateMemories` to extract memories from conversation items, `searchMemories` to retrieve relevant memories, and `deleteScope` to remove all memories for a scope. See `MemoryStoreAdvancedSample` and `MemoryStoreAdvancedAsyncSample` for complete synchronous and asynchronous examples. + ### Using OpenAI's official library If you prefer using the [OpenAI official Java client library][openai_java_sdk] instead, you can do so by including that dependency in your project instead and following the instructions in the linked repository. Additionally, you will have to set up your `OpenAIClient` as shown below: @@ -244,6 +265,8 @@ conversationsClient.items().create( ); ``` +To scope conversation operations to a delegated end user, set `FOUNDRY_USER_IDENTITY` to an opaque application-generated value and apply it as the `x-ms-user-identity` header. The caller must have the `agents/endpoints/UserIdentityImpersonation/action` RBAC permission. See the sync [UserIdentityConversation.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/conversations/UserIdentityConversation.java) and async [UserIdentityConversationAsync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/conversations/UserIdentityConversationAsync.java) samples. + #### Text generation with Responses And the final step that ties everything together, we pass the `AgentReference` and the `conversation.id()` as parameters for the `Response` creation: @@ -525,19 +548,42 @@ See the full sample in [FabricSync.java](https://github.com/Azure/azure-sdk-for- --- -##### **Fabric IQ (Preview)** +##### **Fabric IQ (Preview)** ([documentation](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/fabric-iq)) Connect agents to Fabric IQ project connections for enterprise data grounding: ```java com.azure.ai.agents.define_fabric_iq FabricIqPreviewTool fabricIqTool = new FabricIqPreviewTool(fabricIqConnectionId) - .setServerLabel("fabric_iq") + .setServerLabel("fabric-iq-tool") .setRequireApproval("never"); ``` -See the full sample in [FabricIQSync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQSync.java). +The samples use `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_NAME`, and the fully qualified +`FABRIC_IQ_PROJECT_CONNECTION_ID`. `FOUNDRY_AGENT_NAME` and `FABRIC_IQ_USER_INPUT` are optional. +The response text and any returned annotations are printed before the temporary agent version is deleted. + +See the full samples in [FabricIQSync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQSync.java) +and [FabricIQAsync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQAsync.java). + +--- + +##### **Work IQ (Preview)** ([documentation](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/work-iq)) + +Ground agent responses in the signed-in user's Microsoft 365 work context through a Work IQ project connection: + +```java com.azure.ai.agents.define_work_iq +// Create a Work IQ tool with a fully qualified project connection resource ID +WorkIqPreviewTool workIqTool = new WorkIqPreviewTool(workIqConnectionId); +``` + +Set `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_NAME`, and `WORK_IQ_PROJECT_CONNECTION_ID` before running the +sample. `FOUNDRY_AGENT_NAME` and `WORK_IQ_USER_INPUT` are optional. Work IQ uses delegated authentication and +honors the signed-in user's Microsoft 365 permissions. + +See the full samples in [WorkIQSync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQSync.java) +and [WorkIQAsync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQAsync.java). --- @@ -657,6 +703,32 @@ for (ToolboxTool tool : version.getTools()) { See the full sample in [ToolboxSearchToolboxSample.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ToolboxSearchToolboxSample.java). +##### **Reminder (preview)** + +The Reminder tool lets a hosted agent schedule itself to run again at a future time. It is connectionless and is available only to hosted agents, not prompt agents. + +```java com.azure.ai.agents.toolboxes.ReminderPreviewToolboxSample.createReminderToolbox + +ReminderPreviewToolboxTool reminderTool = new ReminderPreviewToolboxTool() + .setName("schedule_reminder") + .setDescription("Schedule a reminder that re-invokes this agent at a future time."); + +ToolboxVersionDetails version = toolboxesClient.createToolboxVersion( + toolboxName, + Collections.singletonList(reminderTool), + "Built-in reminder tool for a self-scheduling agent.", + null, + null, + null); + +System.out.printf("Created toolbox: %s%n", version.getName()); +System.out.printf("Toolbox version: %s%n", version.getVersion()); +System.out.printf("Tool type: %s%n", version.getTools().get(0).getType()); + +``` + +See the full samples in [ReminderPreviewToolboxSample.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ReminderPreviewToolboxSample.java) and [ReminderPreviewToolboxAsyncSample.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ReminderPreviewToolboxAsyncSample.java). + --- ### Streaming responses @@ -873,3 +945,4 @@ For details on contributing to this repository, see the [contributing guide](htt [openai_conversations_api_docs]: https://platform.openai.com/docs/api-reference/conversations [logLevels]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/core/azure-core/src/main/java/com/azure/core/util/logging/LogLevel.java [performance_tuning]: https://github.com/Azure/azure-sdk-for-java/blob/main/docs/performance-tuning.md +[agent_optimizer_overview]: https://learn.microsoft.com/azure/foundry/agents/concepts/agent-optimizer-overview diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/agents/AgentDraftAsyncSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/agents/AgentDraftAsyncSample.java new file mode 100644 index 000000000000..6f6472949489 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/agents/AgentDraftAsyncSample.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.agents; + +import com.azure.ai.agents.AgentsAsyncClient; +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.models.AgentDetails; +import com.azure.ai.agents.models.AgentVersionDetails; +import com.azure.ai.agents.models.CreateAgentVersionInput; +import com.azure.ai.agents.models.PromptAgentDefinition; +import com.azure.core.util.Configuration; +import com.azure.identity.DefaultAzureCredentialBuilder; +import reactor.core.publisher.Mono; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Sample demonstrating how to create and inspect draft agent versions using the asynchronous + * {@link AgentsAsyncClient}. + * + *

Draft agent versions are a preview feature. They are not promoted to the agent's latest released version and + * are excluded from version listings unless drafts are explicitly included.

+ * + *

Before running the sample, set the {@code FOUNDRY_PROJECT_ENDPOINT} and {@code FOUNDRY_MODEL_NAME} environment + * variables.

+ */ +public class AgentDraftAsyncSample { + public static void main(String[] args) { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String model = Configuration.getGlobalConfiguration().get("FOUNDRY_MODEL_NAME"); + + AgentsAsyncClient agentsAsyncClient = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .allowPreview(true) + .buildAgentsAsyncClient(); + + String agentName = "java-draft-agent-" + UUID.randomUUID(); + AtomicBoolean agentCreated = new AtomicBoolean(); + + Mono workflow = agentsAsyncClient.createAgentVersion(agentName, + new CreateAgentVersionInput(new PromptAgentDefinition(model) + .setInstructions("You are a prompt agent that gives helpful answers.")) + .setDescription("Released agent version created by the draft sample.")) + .doOnNext(releaseVersion -> { + agentCreated.set(true); + System.out.printf("Agent created: name: %s, version: %s%n", + releaseVersion.getName(), releaseVersion.getVersion()); + }) + .then(agentsAsyncClient.getAgent(agentName)) + .doOnNext(AgentDraftAsyncSample::printLatestVersion) + .then(agentsAsyncClient.createAgentVersion(agentName, + new CreateAgentVersionInput(new PromptAgentDefinition(model) + .setInstructions("You are a prompt agent that is still being tested.")) + .setDescription("Draft agent version created by the draft sample.") + .setDraft(true))) + .doOnNext(draftVersion -> { + System.out.printf("Agent draft created: name: %s, version: %s, is draft: %s%n", + draftVersion.getName(), draftVersion.getVersion(), isDraft(draftVersion)); + }) + .then(agentsAsyncClient.getAgent(agentName)) + .doOnNext(agent -> System.out.printf( + "The latest released version of agent \"%s\" is still %s.%n", + agent.getName(), agent.getVersions().getLatest().getVersion())) + .then(agentsAsyncClient.listAgentVersions(agentName) + .doOnNext(version -> printVersion("Released", version)) + .then()) + .then(agentsAsyncClient.listAgentVersions(agentName, null, null, null, null, true) + .doOnNext(version -> printVersion("All", version)) + .then()); + + workflow + .onErrorResume(error -> cleanup(agentsAsyncClient, agentName, agentCreated).then(Mono.error(error))) + .then(Mono.defer(() -> cleanup(agentsAsyncClient, agentName, agentCreated))) + .block(); + } + + private static Mono cleanup(AgentsAsyncClient agentsAsyncClient, String agentName, + AtomicBoolean agentCreated) { + if (!agentCreated.get()) { + return Mono.empty(); + } + return agentsAsyncClient.deleteAgent(agentName) + .doOnSuccess(unused -> System.out.printf("Agent deleted (name: %s)%n", agentName)); + } + + private static void printLatestVersion(AgentDetails agent) { + System.out.printf("The latest released version of agent \"%s\" is %s.%n", + agent.getName(), agent.getVersions().getLatest().getVersion()); + } + + private static void printVersion(String collection, AgentVersionDetails version) { + System.out.printf("%s version: %s (is draft: %s)%n", collection, version.getVersion(), isDraft(version)); + } + + private static boolean isDraft(AgentVersionDetails version) { + return Boolean.TRUE.equals(version.isDraft()); + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/agents/AgentDraftSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/agents/AgentDraftSample.java new file mode 100644 index 000000000000..a0814327b8f8 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/agents/AgentDraftSample.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.agents; + +import com.azure.ai.agents.AgentsClient; +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.models.AgentDetails; +import com.azure.ai.agents.models.AgentVersionDetails; +import com.azure.ai.agents.models.CreateAgentVersionInput; +import com.azure.ai.agents.models.PromptAgentDefinition; +import com.azure.core.util.Configuration; +import com.azure.identity.DefaultAzureCredentialBuilder; + +import java.util.UUID; + +/** + * Sample demonstrating how to create and inspect draft agent versions using the synchronous {@link AgentsClient}. + * + *

Draft agent versions are a preview feature. They are not promoted to the agent's latest released version and + * are excluded from version listings unless drafts are explicitly included.

+ * + *

Before running the sample, set the {@code FOUNDRY_PROJECT_ENDPOINT} and {@code FOUNDRY_MODEL_NAME} environment + * variables.

+ */ +public class AgentDraftSample { + public static void main(String[] args) { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String model = Configuration.getGlobalConfiguration().get("FOUNDRY_MODEL_NAME"); + + AgentsClient agentsClient = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .allowPreview(true) + .buildAgentsClient(); + + String agentName = "java-draft-agent-" + UUID.randomUUID(); + boolean agentCreated = false; + + try { + // BEGIN:com.azure.ai.agents.agents.AgentDraftSample.createReleaseVersion + AgentVersionDetails releaseVersion = agentsClient.createAgentVersion(agentName, + new CreateAgentVersionInput(new PromptAgentDefinition(model) + .setInstructions("You are a prompt agent that gives helpful answers.")) + .setDescription("Released agent version created by the draft sample.")); + agentCreated = true; + System.out.printf("Agent created: name: %s, version: %s%n", + releaseVersion.getName(), releaseVersion.getVersion()); + + printLatestVersion(agentsClient.getAgent(agentName)); + // END:com.azure.ai.agents.agents.AgentDraftSample.createReleaseVersion + + // BEGIN:com.azure.ai.agents.agents.AgentDraftSample.createDraftVersion + AgentVersionDetails draftVersion = agentsClient.createAgentVersion(agentName, + new CreateAgentVersionInput(new PromptAgentDefinition(model) + .setInstructions("You are a prompt agent that is still being tested.")) + .setDescription("Draft agent version created by the draft sample.") + .setDraft(true)); + System.out.printf("Agent draft created: name: %s, version: %s, is draft: %s%n", + draftVersion.getName(), draftVersion.getVersion(), isDraft(draftVersion)); + + AgentDetails agent = agentsClient.getAgent(agentName); + System.out.printf("The latest released version of agent \"%s\" is still %s.%n", + agent.getName(), agent.getVersions().getLatest().getVersion()); + // END:com.azure.ai.agents.agents.AgentDraftSample.createDraftVersion + + System.out.printf("Released versions for agent %s:%n", agentName); + for (AgentVersionDetails version : agentsClient.listAgentVersions(agentName)) { + System.out.printf(" %s (is draft: %s)%n", version.getVersion(), isDraft(version)); + } + + System.out.printf("All versions for agent %s:%n", agentName); + for (AgentVersionDetails version + : agentsClient.listAgentVersions(agentName, null, null, null, null, true)) { + System.out.printf(" %s (is draft: %s)%n", version.getVersion(), isDraft(version)); + } + } finally { + if (agentCreated) { + agentsClient.deleteAgent(agentName); + System.out.printf("Agent deleted (name: %s)%n", agentName); + } + } + } + + private static void printLatestVersion(AgentDetails agent) { + System.out.printf("The latest released version of agent \"%s\" is %s.%n", + agent.getName(), agent.getVersions().getLatest().getVersion()); + } + + private static boolean isDraft(AgentVersionDetails version) { + return Boolean.TRUE.equals(version.isDraft()); + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/conversations/UserIdentityConversation.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/conversations/UserIdentityConversation.java new file mode 100644 index 000000000000..27bfae02bf94 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/conversations/UserIdentityConversation.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.conversations; + +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.core.util.Configuration; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.openai.core.JsonValue; +import com.openai.models.conversations.Conversation; +import com.openai.models.conversations.ConversationUpdateParams; +import com.openai.models.conversations.ConversationDeletedResource; +import com.openai.services.blocking.ConversationService; + +/** + * Demonstrates applying the {@code x-ms-user-identity} header to OpenAI conversation calls. + * + *

Set {@code FOUNDRY_PROJECT_ENDPOINT} and {@code FOUNDRY_USER_IDENTITY} before running this sample. + * The user identity should be an opaque, application-generated value and must not contain secrets. + */ +public class UserIdentityConversation { + public static void main(String[] args) { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String userIdentity = Configuration.getGlobalConfiguration().get("FOUNDRY_USER_IDENTITY"); + if (userIdentity == null || userIdentity.trim().isEmpty()) { + throw new IllegalStateException("Set FOUNDRY_USER_IDENTITY to an opaque end-user identity value."); + } + + ConversationService conversationService = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .buildOpenAIClient() + .conversations() + .withOptions(options -> options.putHeader("x-ms-user-identity", userIdentity)); + + String conversationId = null; + try { + Conversation conversation = conversationService.create(); + conversationId = conversation.id(); + System.out.println("Created conversation: " + conversationId); + + ConversationUpdateParams.Metadata metadata = ConversationUpdateParams.Metadata.builder() + .putAdditionalProperty("sample", JsonValue.from("java-user-identity")) + .build(); + conversation = conversationService.update(conversationId, + ConversationUpdateParams.builder().metadata(metadata).build()); + System.out.println("Updated conversation: " + conversation.id()); + + conversation = conversationService.retrieve(conversationId); + System.out.println("Retrieved conversation: " + conversation.id()); + } finally { + if (conversationId != null) { + ConversationDeletedResource deletedConversation = conversationService.delete(conversationId); + System.out.println("Deleted conversation: " + deletedConversation.id()); + } + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/conversations/UserIdentityConversationAsync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/conversations/UserIdentityConversationAsync.java new file mode 100644 index 000000000000..96eafdedbba5 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/conversations/UserIdentityConversationAsync.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.conversations; + +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.core.util.Configuration; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.openai.core.JsonValue; +import com.openai.models.conversations.Conversation; +import com.openai.models.conversations.ConversationDeletedResource; +import com.openai.models.conversations.ConversationUpdateParams; +import com.openai.services.async.ConversationServiceAsync; + +/** + * Demonstrates applying the {@code x-ms-user-identity} header to asynchronous OpenAI conversation calls. + * + *

Set {@code FOUNDRY_PROJECT_ENDPOINT} and {@code FOUNDRY_USER_IDENTITY} before running this sample. + * The user identity should be an opaque, application-generated value and must not contain secrets. + */ +public class UserIdentityConversationAsync { + public static void main(String[] args) { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String userIdentity = Configuration.getGlobalConfiguration().get("FOUNDRY_USER_IDENTITY"); + if (userIdentity == null || userIdentity.trim().isEmpty()) { + throw new IllegalStateException("Set FOUNDRY_USER_IDENTITY to an opaque end-user identity value."); + } + + ConversationServiceAsync conversationService = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .buildOpenAIAsyncClient() + .conversations() + .withOptions(options -> options.putHeader("x-ms-user-identity", userIdentity)); + + String conversationId = null; + try { + Conversation conversation = conversationService.create().join(); + conversationId = conversation.id(); + System.out.println("Created conversation: " + conversationId); + + ConversationUpdateParams.Metadata metadata = ConversationUpdateParams.Metadata.builder() + .putAdditionalProperty("sample", JsonValue.from("java-user-identity-async")) + .build(); + conversation = conversationService.update(conversationId, + ConversationUpdateParams.builder().metadata(metadata).build()).join(); + System.out.println("Updated conversation: " + conversation.id()); + + conversation = conversationService.retrieve(conversationId).join(); + System.out.println("Retrieved conversation: " + conversation.id()); + } finally { + if (conversationId != null) { + ConversationDeletedResource deletedConversation = conversationService.delete(conversationId).join(); + System.out.println("Deleted conversation: " + deletedConversation.id()); + } + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/HostedAgentDisableAsyncSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/HostedAgentDisableAsyncSample.java new file mode 100644 index 000000000000..99c90488da96 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/HostedAgentDisableAsyncSample.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.hostedagents; + +import com.azure.ai.agents.AgentsAsyncClient; +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.hostedagents.utils.HostedAgentsSampleUtils; +import com.azure.ai.agents.models.AgentSessionResource; +import com.azure.ai.agents.models.AgentVersionDetails; +import com.azure.core.exception.HttpResponseException; +import com.azure.core.exception.ResourceNotFoundException; +import com.azure.core.util.Configuration; +import com.azure.identity.DefaultAzureCredentialBuilder; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * This sample demonstrates disabling and enabling a hosted agent using the async client. + * + *

When disabled, a hosted agent cannot accept new sessions. Before running, set these environment variables:

+ *
    + *
  • FOUNDRY_PROJECT_ENDPOINT - The Azure AI Foundry project endpoint.
  • + *
  • FOUNDRY_AGENT_CONTAINER_IMAGE - The hosted-agent container image.
  • + *
+ */ +public class HostedAgentDisableAsyncSample { + private static final String AGENT_NAME = "java-disable-async-" + UUID.randomUUID().toString().substring(0, 8); + private static final Duration WORKFLOW_TIMEOUT = Duration.ofMinutes(5); + private static final Duration CLEANUP_TIMEOUT = Duration.ofMinutes(1); + + public static void main(String[] args) { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String image = Configuration.getGlobalConfiguration().get("FOUNDRY_AGENT_CONTAINER_IMAGE"); + String agentName = AGENT_NAME; + + AgentsClientBuilder builder = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .allowPreview(true); + AgentsAsyncClient agentsAsyncClient = builder.buildAgentsAsyncClient(); + + AtomicReference agentRef = new AtomicReference<>(); + AtomicReference unexpectedSessionRef = new AtomicReference<>(); + AtomicBoolean agentDisabled = new AtomicBoolean(); + + Mono workflow = agentsAsyncClient.enableAgent(agentName) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .then(HostedAgentsSampleUtils.createActiveHostedAgentVersionAsync(agentsAsyncClient, agentName, image)) + .flatMap(agent -> { + agentRef.set(agent); + String agentVersion = agent.getVersion(); + + // BEGIN: com.azure.ai.agents.hostedagents.HostedAgentDisableAsyncSample.disableAgent + + return agentsAsyncClient.disableAgent(agentName) + .doOnSuccess(unused -> agentDisabled.set(true)) + .then(HostedAgentsSampleUtils.createSessionAsync(agentsAsyncClient, agentName, agentVersion) + .doOnNext(unexpectedSessionRef::set) + .flatMap(unused -> Mono.error(new IllegalStateException( + "A disabled agent unexpectedly created a session."))) + .onErrorResume(HttpResponseException.class, ex -> { + if (ex.getResponse().getStatusCode() != 403) { + return Mono.error(ex); + } + System.out.println( + "Creating a session for the disabled agent failed with HTTP 403 as expected."); + return Mono.empty(); + })) + + // END: com.azure.ai.agents.hostedagents.HostedAgentDisableAsyncSample.disableAgent + // BEGIN: com.azure.ai.agents.hostedagents.HostedAgentDisableAsyncSample.enableAgent + + .then(agentsAsyncClient.enableAgent(agentName)) + .doOnSuccess(unused -> agentDisabled.set(false)) + .then(); + + // END: com.azure.ai.agents.hostedagents.HostedAgentDisableAsyncSample.enableAgent + }); + + workflow.timeout(WORKFLOW_TIMEOUT) + .onErrorResume(error -> Mono.defer(() -> cleanupAsync(agentsAsyncClient, agentName, agentDisabled, + unexpectedSessionRef, agentRef)).then(Mono.error(error))) + .then(Mono.defer(() -> cleanupAsync(agentsAsyncClient, agentName, agentDisabled, unexpectedSessionRef, + agentRef))) + .block(); + } + + private static Mono cleanupAsync(AgentsAsyncClient agentsAsyncClient, String agentName, + AtomicBoolean agentDisabled, AtomicReference unexpectedSessionRef, + AtomicReference agentRef) { + Mono restoreAgent = Mono.defer(() -> { + if (!agentDisabled.get()) { + return Mono.empty(); + } + return agentsAsyncClient.enableAgent(agentName) + .doOnSuccess(unused -> agentDisabled.set(false)) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .timeout(CLEANUP_TIMEOUT) + .onErrorResume(error -> { + System.err.println("Unable to restore the agent to the enabled state: " + error.getMessage()); + return Mono.empty(); + }); + }); + + Mono deleteSession = Mono.defer(() -> { + AgentSessionResource session = unexpectedSessionRef.get(); + if (session == null) { + return Mono.empty(); + } + return agentsAsyncClient.deleteSession(agentName, session.getAgentSessionId()) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .timeout(CLEANUP_TIMEOUT) + .onErrorResume(error -> { + System.err.println("Unable to delete the unexpected session: " + error.getMessage()); + return Mono.empty(); + }); + }); + + return restoreAgent + .then(deleteSession) + .then(Mono.defer(() -> { + AgentVersionDetails agent = agentRef.get(); + if (agent == null) { + return Mono.empty(); + } + return agentsAsyncClient.deleteAgentVersion(agentName, agent.getVersion()) + .doOnSuccess(unused -> System.out.printf("Agent version %s deleted.%n", agent.getVersion())); + }) + .timeout(CLEANUP_TIMEOUT) + .onErrorResume(error -> { + System.err.println("Unable to finish hosted-agent cleanup: " + error.getMessage()); + return Mono.empty(); + })); + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/HostedAgentDisableSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/HostedAgentDisableSample.java new file mode 100644 index 000000000000..94c97fd0372a --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/HostedAgentDisableSample.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.hostedagents; + +import com.azure.ai.agents.AgentsClient; +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.hostedagents.utils.HostedAgentsSampleUtils; +import com.azure.ai.agents.models.AgentSessionResource; +import com.azure.ai.agents.models.AgentVersionDetails; +import com.azure.core.exception.HttpResponseException; +import com.azure.core.exception.ResourceNotFoundException; +import com.azure.core.util.Configuration; +import com.azure.identity.DefaultAzureCredentialBuilder; + +import java.util.UUID; + +/** + * This sample demonstrates disabling and enabling a hosted agent. + * + *

When disabled, a hosted agent cannot accept new sessions. Before running, set these environment variables:

+ *
    + *
  • FOUNDRY_PROJECT_ENDPOINT - The Azure AI Foundry project endpoint.
  • + *
  • FOUNDRY_AGENT_CONTAINER_IMAGE - The hosted-agent container image.
  • + *
+ */ +public class HostedAgentDisableSample { + private static final String AGENT_NAME = "java-disable-sync-" + UUID.randomUUID().toString().substring(0, 8); + + public static void main(String[] args) { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String image = Configuration.getGlobalConfiguration().get("FOUNDRY_AGENT_CONTAINER_IMAGE"); + String agentName = AGENT_NAME; + + AgentsClientBuilder builder = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .allowPreview(true); + AgentsClient agentsClient = builder.buildAgentsClient(); + + AgentVersionDetails agent = null; + AgentSessionResource unexpectedSession = null; + boolean agentDisabled = false; + RuntimeException cleanupError = null; + try { + try { + agentsClient.enableAgent(agentName); + } catch (ResourceNotFoundException ignored) { + // The sample agent does not already exist. + } + + agent = HostedAgentsSampleUtils.createActiveHostedAgentVersion(agentsClient, agentName, image); + String agentVersion = agent.getVersion(); + + // BEGIN: com.azure.ai.agents.hostedagents.HostedAgentDisableSample.disableAgent + + agentsClient.disableAgent(agentName); + agentDisabled = true; + try { + unexpectedSession = HostedAgentsSampleUtils.createSession(agentsClient, agentName, agentVersion); + throw new IllegalStateException("A disabled agent unexpectedly created a session."); + } catch (HttpResponseException ex) { + if (ex.getResponse().getStatusCode() != 403) { + throw ex; + } + System.out.println("Creating a session for the disabled agent failed with HTTP 403 as expected."); + } + + // END: com.azure.ai.agents.hostedagents.HostedAgentDisableSample.disableAgent + // BEGIN: com.azure.ai.agents.hostedagents.HostedAgentDisableSample.enableAgent + + agentsClient.enableAgent(agentName); + agentDisabled = false; + + // END: com.azure.ai.agents.hostedagents.HostedAgentDisableSample.enableAgent + } finally { + if (agentDisabled) { + try { + agentsClient.enableAgent(agentName); + } catch (ResourceNotFoundException ignored) { + // The sample agent may not have been created. + } catch (RuntimeException ex) { + System.err.println("Unable to restore the agent to the enabled state: " + ex.getMessage()); + } + } + + if (unexpectedSession != null) { + try { + agentsClient.deleteSession(agentName, unexpectedSession.getAgentSessionId()); + } catch (ResourceNotFoundException ignored) { + // The sample may have already deleted the session. + } catch (RuntimeException error) { + cleanupError = error; + } + } + if (agent != null) { + try { + agentsClient.deleteAgentVersion(agentName, agent.getVersion()); + System.out.printf("Agent version %s deleted.%n", agent.getVersion()); + } catch (ResourceNotFoundException ignored) { + // The sample may have already deleted the agent version. + } catch (RuntimeException error) { + if (cleanupError == null) { + cleanupError = error; + } else { + cleanupError.addSuppressed(error); + } + } + } + if (cleanupError != null) { + throw cleanupError; + } + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/utils/HostedAgentsSampleUtils.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/utils/HostedAgentsSampleUtils.java index 6ba9ed58795b..ed1aa4db6793 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/utils/HostedAgentsSampleUtils.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/hostedagents/utils/HostedAgentsSampleUtils.java @@ -50,30 +50,88 @@ private HostedAgentsSampleUtils() { } public static HostedAgentSessionResources createAgentAndSession(AgentsClient agentsClient, + String agentName, String image) { + AgentVersionDetails agent = createActiveHostedAgentVersion(agentsClient, agentName, image); + try { + AgentSessionResource session = createSession(agentsClient, agentName, agent.getVersion()); + return new HostedAgentSessionResources(agent, session); + } catch (RuntimeException error) { + try { + agentsClient.deleteAgentVersion(agentName, agent.getVersion()); + } catch (ResourceNotFoundException ignored) { + // The agent version was already deleted. + } catch (RuntimeException cleanupError) { + error.addSuppressed(cleanupError); + } + throw error; + } + } + + public static AgentVersionDetails createActiveHostedAgentVersion(AgentsClient agentsClient, String agentName, String image) { AgentVersionDetails agent = createHostedAgentVersion(agentsClient, agentName, image); - waitForAgentVersionActive(agentsClient, agentName, agent.getVersion()); + try { + waitForAgentVersionActive(agentsClient, agentName, agent.getVersion()); + return agent; + } catch (RuntimeException error) { + try { + agentsClient.deleteAgentVersion(agentName, agent.getVersion()); + } catch (ResourceNotFoundException ignored) { + // The agent version was already deleted. + } catch (RuntimeException cleanupError) { + error.addSuppressed(cleanupError); + } + throw error; + } + } + public static AgentSessionResource createSession(AgentsClient agentsClient, String agentName, String agentVersion) { AgentSessionResource session = agentsClient.createSessionWithResponse(agentName, - BinaryData.fromObject(createSessionRequest(agent.getVersion())), new RequestOptions()).getValue() + BinaryData.fromObject(createSessionRequest(agentVersion)), new RequestOptions()).getValue() .toObject(AgentSessionResource.class); System.out.printf("Session created (id: %s, status: %s)%n", session.getAgentSessionId(), session.getStatus()); - - return new HostedAgentSessionResources(agent, session); + return session; } public static Mono createAgentAndSessionAsync(AgentsAsyncClient agentsAsyncClient, String agentName, String image) { - return createHostedAgentVersionAsync(agentsAsyncClient, agentName, image) - .flatMap(agent -> waitForAgentVersionActiveAsync(agentsAsyncClient, agentName, agent.getVersion()) - .then(agentsAsyncClient.createSessionWithResponse(agentName, - BinaryData.fromObject(createSessionRequest(agent.getVersion())), new RequestOptions()) - .map(response -> response.getValue().toObject(AgentSessionResource.class))) - .map(session -> { - System.out.printf("Session created (id: %s, status: %s)%n", session.getAgentSessionId(), - session.getStatus()); - return new HostedAgentSessionResources(agent, session); - })); + return Mono.usingWhen( + createActiveHostedAgentVersionAsync(agentsAsyncClient, agentName, image), + agent -> createSessionAsync(agentsAsyncClient, agentName, agent.getVersion()) + .map(session -> new HostedAgentSessionResources(agent, session)), + agent -> Mono.empty(), + (agent, error) -> deleteAgentVersionAfterSetupFailure(agentsAsyncClient, agentName, agent), + agent -> deleteAgentVersionAfterSetupFailure(agentsAsyncClient, agentName, agent)); + } + + public static Mono createActiveHostedAgentVersionAsync(AgentsAsyncClient agentsAsyncClient, + String agentName, String image) { + return Mono.usingWhen( + createHostedAgentVersionAsync(agentsAsyncClient, agentName, image), + agent -> waitForAgentVersionActiveAsync(agentsAsyncClient, agentName, agent.getVersion()), + agent -> Mono.empty(), + (agent, error) -> deleteAgentVersionAfterSetupFailure(agentsAsyncClient, agentName, agent), + agent -> deleteAgentVersionAfterSetupFailure(agentsAsyncClient, agentName, agent)); + } + + private static Mono deleteAgentVersionAfterSetupFailure(AgentsAsyncClient agentsAsyncClient, + String agentName, AgentVersionDetails agent) { + return agentsAsyncClient.deleteAgentVersion(agentName, agent.getVersion()) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .onErrorResume(error -> { + System.err.printf("Unable to delete agent version %s after setup failed: %s%n", + agent.getVersion(), error.getMessage()); + return Mono.empty(); + }); + } + + public static Mono createSessionAsync(AgentsAsyncClient agentsAsyncClient, + String agentName, String agentVersion) { + return agentsAsyncClient.createSessionWithResponse(agentName, + BinaryData.fromObject(createSessionRequest(agentVersion)), new RequestOptions()) + .map(response -> response.getValue().toObject(AgentSessionResource.class)) + .doOnNext(session -> System.out.printf("Session created (id: %s, status: %s)%n", + session.getAgentSessionId(), session.getStatus())); } public static void cleanup(AgentsClient agentsClient, String agentName, @@ -82,12 +140,15 @@ public static void cleanup(AgentsClient agentsClient, String agentName, return; } + RuntimeException cleanupError = null; if (resources.getSession() != null) { try { agentsClient.deleteSession(agentName, resources.getSession().getAgentSessionId()); System.out.printf("Session with id: %s deleted.%n", resources.getSession().getAgentSessionId()); } catch (ResourceNotFoundException ignored) { // The sample may have already deleted the session. + } catch (RuntimeException error) { + cleanupError = error; } } @@ -97,8 +158,17 @@ public static void cleanup(AgentsClient agentsClient, String agentName, System.out.printf("Agent version %s deleted.%n", resources.getAgent().getVersion()); } catch (ResourceNotFoundException ignored) { // The sample may have already deleted the agent version. + } catch (RuntimeException error) { + if (cleanupError == null) { + cleanupError = error; + } else { + cleanupError.addSuppressed(error); + } } } + if (cleanupError != null) { + throw cleanupError; + } } public static Mono cleanupAsync(AgentsAsyncClient agentsAsyncClient, @@ -112,7 +182,11 @@ public static Mono cleanupAsync(AgentsAsyncClient agentsAsyncClient, String sessionId = resources.getSession().getAgentSessionId(); deleteSession = agentsAsyncClient.deleteSession(agentName, sessionId) .doOnSuccess(unused -> System.out.printf("Session with id: %s deleted.%n", sessionId)) - .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()); + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .onErrorResume(error -> { + System.err.println("Unable to delete session " + sessionId + ": " + error.getMessage()); + return Mono.empty(); + }); } Mono deleteAgentVersion = Mono.empty(); @@ -120,7 +194,11 @@ public static Mono cleanupAsync(AgentsAsyncClient agentsAsyncClient, String version = resources.getAgent().getVersion(); deleteAgentVersion = agentsAsyncClient.deleteAgentVersion(agentName, version) .doOnSuccess(unused -> System.out.printf("Agent version %s deleted.%n", version)) - .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()); + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .onErrorResume(error -> { + System.err.println("Unable to delete agent version " + version + ": " + error.getMessage()); + return Mono.empty(); + }); } return deleteSession.then(deleteAgentVersion); diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/memory/MemoryStoreAdvancedAsyncSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/memory/MemoryStoreAdvancedAsyncSample.java new file mode 100644 index 000000000000..a24db3dc1489 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/memory/MemoryStoreAdvancedAsyncSample.java @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.memory; + +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.BetaMemoryStoresAsyncClient; +import com.azure.ai.agents.models.MemoryOperation; +import com.azure.ai.agents.models.MemorySearchItem; +import com.azure.ai.agents.models.MemorySearchOptions; +import com.azure.ai.agents.models.MemoryStoreDefaultDefinition; +import com.azure.ai.agents.models.MemoryStoreDefaultOptions; +import com.azure.ai.agents.models.MemoryStoreDetails; +import com.azure.ai.agents.models.MemoryStoreSearchResponse; +import com.azure.ai.agents.models.MemoryStoreUpdateCompletedResult; +import com.azure.ai.agents.models.MemoryStoreUpdateResponse; +import com.azure.core.exception.ResourceNotFoundException; +import com.azure.core.util.Configuration; +import com.azure.core.util.polling.AsyncPollResponse; +import com.azure.core.util.polling.PollerFlux; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.openai.models.responses.EasyInputMessage; +import com.openai.models.responses.ResponseInputItem; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Arrays; + +/** + * Sample demonstrating conversational memory store operations using the asynchronous + * {@link BetaMemoryStoresAsyncClient}. + * + *

Memory stores are a preview feature. Before running, set the following environment variables:

+ *
    + *
  • {@code FOUNDRY_PROJECT_ENDPOINT} - the Azure AI Foundry project endpoint.
  • + *
  • {@code AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME} - a chat completion model deployment name.
  • + *
  • {@code AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME} - an embedding model deployment name.
  • + *
+ */ +public class MemoryStoreAdvancedAsyncSample { + private static final String MEMORY_STORE_NAME = "memory_advanced_store_java_async"; + private static final Duration POLL_TIMEOUT = Duration.ofMinutes(3); + private static final Duration CLEANUP_TIMEOUT = Duration.ofMinutes(1); + + public static void main(String[] args) { + Configuration configuration = Configuration.getGlobalConfiguration(); + String endpoint = configuration.get("FOUNDRY_PROJECT_ENDPOINT"); + String chatModel = configuration.get("AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME"); + String embeddingModel = configuration.get("AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME"); + + BetaMemoryStoresAsyncClient memoryStoresAsyncClient = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .beta() + .buildBetaMemoryStoresAsyncClient(); + + MemoryStoreDefaultOptions options = new MemoryStoreDefaultOptions(true, true) + .setUserProfileDetails("Preferences and interests relevant to a coffee expert agent"); + MemoryStoreDefaultDefinition definition = new MemoryStoreDefaultDefinition(chatModel, embeddingModel) + .setOptions(options); + + memoryStoresAsyncClient.deleteMemoryStore(MEMORY_STORE_NAME) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .then(memoryStoresAsyncClient.createMemoryStore( + MEMORY_STORE_NAME, definition, "Example memory store for conversations", null)) + .flatMap(memoryStore -> runAdvancedMemoryOperations(memoryStoresAsyncClient, memoryStore) + .onErrorResume(error -> cleanupAsync(memoryStoresAsyncClient, memoryStore.getName(), "user_123") + .then(Mono.error(error)))) + .block(); + } + + private static Mono runAdvancedMemoryOperations(BetaMemoryStoresAsyncClient memoryStoresClient, + MemoryStoreDetails memoryStore) { + String scope = "user_123"; + ResponseInputItem initialMessage = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("I prefer dark roast coffee and usually drink it in the morning") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + + PollerFlux initialPoller + = memoryStoresClient.beginUpdateMemories( + memoryStore.getName(), scope, Arrays.asList(initialMessage), null, 300); + + return initialPoller.next() + .map(AsyncPollResponse::getValue) + .flatMap(initialResponse -> { + System.out.printf("Scheduled memory update (id: %s, status: %s)%n", + initialResponse.getUpdateId(), initialResponse.getStatus()); + + ResponseInputItem chainedMessage = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("I also like cappuccinos in the afternoon") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + PollerFlux chainedPoller + = memoryStoresClient.beginUpdateMemories( + memoryStore.getName(), scope, Arrays.asList(chainedMessage), + initialResponse.getUpdateId(), 0); + + return waitForUpdateCompletion(chainedPoller) + .doOnNext(updateResult -> { + System.out.printf("Memory update completed with %d operations%n", + updateResult.getMemoryOperations().size()); + for (MemoryOperation operation : updateResult.getMemoryOperations()) { + System.out.printf(" - Operation: %s, memory ID: %s, content: %s%n", + operation.getKind(), operation.getMemoryItem().getMemoryId(), + operation.getMemoryItem().getContent()); + } + }) + .flatMap(updateResult -> searchMemories(memoryStoresClient, memoryStore.getName(), scope)); + }) + .then(memoryStoresClient.deleteScope(memoryStore.getName(), scope)) + .doOnSuccess(unused -> System.out.printf("Deleted memories for scope '%s'%n", scope)) + .then(memoryStoresClient.deleteMemoryStore(memoryStore.getName())) + .doOnSuccess(unused -> System.out.printf("Memory store deleted (name: %s)%n", memoryStore.getName())); + } + + private static Mono searchMemories(BetaMemoryStoresAsyncClient memoryStoresClient, String memoryStoreName, + String scope) { + MemorySearchOptions searchOptions = new MemorySearchOptions().setMaxMemories(5); + ResponseInputItem searchQuery = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("What are my morning coffee preferences?") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + + return memoryStoresClient.searchMemories( + memoryStoreName, scope, Arrays.asList(searchQuery), null, searchOptions) + .doOnNext(MemoryStoreAdvancedAsyncSample::printSearchResults) + .flatMap(searchResponse -> { + ResponseInputItem agentMessage = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.ASSISTANT) + .content("You previously indicated a preference for dark roast coffee in the morning.") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + ResponseInputItem followupQuery = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("What about afternoon?") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + + return memoryStoresClient.searchMemories( + memoryStoreName, scope, Arrays.asList(agentMessage, followupQuery), + searchResponse.getSearchId(), searchOptions) + .doOnNext(MemoryStoreAdvancedAsyncSample::printSearchResults) + .then(); + }); + } + + private static Mono waitForUpdateCompletion( + PollerFlux poller) { + return poller.takeUntil(response -> response.getStatus().isComplete()) + .last() + .flatMap(AsyncPollResponse::getFinalResult) + .timeout(POLL_TIMEOUT); + } + + private static Mono cleanupAsync(BetaMemoryStoresAsyncClient memoryStoresClient, String memoryStoreName, + String scope) { + return memoryStoresClient.deleteScope(memoryStoreName, scope) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .timeout(CLEANUP_TIMEOUT) + .onErrorResume(error -> { + System.err.println("Unable to delete the memory scope: " + error.getMessage()); + return Mono.empty(); + }) + .then(memoryStoresClient.deleteMemoryStore(memoryStoreName) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .timeout(CLEANUP_TIMEOUT) + .onErrorResume(error -> { + System.err.println("Unable to delete the memory store: " + error.getMessage()); + return Mono.empty(); + })); + } + + private static void printSearchResults(MemoryStoreSearchResponse searchResponse) { + System.out.printf("Found %d memories (search ID: %s)%n", + searchResponse.getMemories().size(), searchResponse.getSearchId()); + for (MemorySearchItem memory : searchResponse.getMemories()) { + System.out.printf(" - Memory ID: %s, content: %s%n", + memory.getMemoryItem().getMemoryId(), memory.getMemoryItem().getContent()); + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/memory/MemoryStoreAdvancedSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/memory/MemoryStoreAdvancedSample.java new file mode 100644 index 000000000000..2405a8cd8be4 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/memory/MemoryStoreAdvancedSample.java @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.memory; + +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.BetaMemoryStoresClient; +import com.azure.ai.agents.models.MemoryOperation; +import com.azure.ai.agents.models.MemorySearchItem; +import com.azure.ai.agents.models.MemorySearchOptions; +import com.azure.ai.agents.models.MemoryStoreDefaultDefinition; +import com.azure.ai.agents.models.MemoryStoreDefaultOptions; +import com.azure.ai.agents.models.MemoryStoreDetails; +import com.azure.ai.agents.models.MemoryStoreSearchResponse; +import com.azure.ai.agents.models.MemoryStoreUpdateCompletedResult; +import com.azure.ai.agents.models.MemoryStoreUpdateResponse; +import com.azure.core.util.Configuration; +import com.azure.core.util.polling.SyncPoller; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.openai.models.responses.EasyInputMessage; +import com.openai.models.responses.ResponseInputItem; + +import java.time.Duration; +import java.util.Arrays; + +/** + * Sample demonstrating conversational memory store operations using the synchronous + * {@link BetaMemoryStoresClient}. + * + *

Memory stores are a preview feature. Before running, set the following environment variables:

+ *
    + *
  • {@code FOUNDRY_PROJECT_ENDPOINT} - the Azure AI Foundry project endpoint.
  • + *
  • {@code AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME} - a chat completion model deployment name.
  • + *
  • {@code AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME} - an embedding model deployment name.
  • + *
+ */ +public class MemoryStoreAdvancedSample { + private static final String MEMORY_STORE_NAME = "memory_advanced_store_java_sync"; + private static final Duration POLL_TIMEOUT = Duration.ofMinutes(3); + + public static void main(String[] args) { + Configuration configuration = Configuration.getGlobalConfiguration(); + String endpoint = configuration.get("FOUNDRY_PROJECT_ENDPOINT"); + String chatModel = configuration.get("AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME"); + String embeddingModel = configuration.get("AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME"); + + BetaMemoryStoresClient memoryStoresClient = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .beta() + .buildBetaMemoryStoresClient(); + + try { + memoryStoresClient.deleteMemoryStore(MEMORY_STORE_NAME); + } catch (RuntimeException ignored) { + // The sample memory store does not already exist. + } + + MemoryStoreDefaultOptions options = new MemoryStoreDefaultOptions(true, true) + .setUserProfileDetails("Preferences and interests relevant to a coffee expert agent"); + MemoryStoreDefaultDefinition definition = new MemoryStoreDefaultDefinition(chatModel, embeddingModel) + .setOptions(options); + MemoryStoreDetails memoryStore + = memoryStoresClient.createMemoryStore(MEMORY_STORE_NAME, definition, + "Example memory store for conversations", null); + + String scope = "user_123"; + try { + ResponseInputItem initialMessage = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("I prefer dark roast coffee and usually drink it in the morning") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + + SyncPoller initialPoller + = memoryStoresClient.beginUpdateMemories( + memoryStore.getName(), scope, Arrays.asList(initialMessage), null, 300); + MemoryStoreUpdateResponse initialResponse = initialPoller.poll().getValue(); + System.out.printf("Scheduled memory update (id: %s, status: %s)%n", + initialResponse.getUpdateId(), initialResponse.getStatus()); + + ResponseInputItem chainedMessage = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("I also like cappuccinos in the afternoon") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + SyncPoller chainedPoller + = memoryStoresClient.beginUpdateMemories( + memoryStore.getName(), scope, Arrays.asList(chainedMessage), + initialResponse.getUpdateId(), 0); + MemoryStoreUpdateResponse chainedResponse = chainedPoller.poll().getValue(); + System.out.printf("Scheduled chained memory update (id: %s, status: %s)%n", + chainedResponse.getUpdateId(), chainedResponse.getStatus()); + + chainedPoller.waitForCompletion(POLL_TIMEOUT); + MemoryStoreUpdateCompletedResult updateResult = chainedPoller.getFinalResult(); + System.out.printf("Memory update completed with %d operations%n", + updateResult.getMemoryOperations().size()); + for (MemoryOperation operation : updateResult.getMemoryOperations()) { + System.out.printf(" - Operation: %s, memory ID: %s, content: %s%n", + operation.getKind(), operation.getMemoryItem().getMemoryId(), + operation.getMemoryItem().getContent()); + } + + MemorySearchOptions searchOptions = new MemorySearchOptions().setMaxMemories(5); + ResponseInputItem searchQuery = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("What are my morning coffee preferences?") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + MemoryStoreSearchResponse searchResponse = memoryStoresClient.searchMemories( + memoryStore.getName(), scope, Arrays.asList(searchQuery), null, searchOptions); + printSearchResults(searchResponse); + + ResponseInputItem agentMessage = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.ASSISTANT) + .content("You previously indicated a preference for dark roast coffee in the morning.") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + ResponseInputItem followupQuery = ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .role(EasyInputMessage.Role.USER) + .content("What about afternoon?") + .type(EasyInputMessage.Type.MESSAGE) + .build()); + MemoryStoreSearchResponse followupSearchResponse = memoryStoresClient.searchMemories( + memoryStore.getName(), scope, Arrays.asList(agentMessage, followupQuery), + searchResponse.getSearchId(), searchOptions); + printSearchResults(followupSearchResponse); + + memoryStoresClient.deleteScope(memoryStore.getName(), scope); + System.out.printf("Deleted memories for scope '%s'%n", scope); + } finally { + memoryStoresClient.deleteMemoryStore(memoryStore.getName()); + System.out.printf("Memory store deleted (name: %s)%n", memoryStore.getName()); + } + } + + private static void printSearchResults(MemoryStoreSearchResponse searchResponse) { + System.out.printf("Found %d memories (search ID: %s)%n", + searchResponse.getMemories().size(), searchResponse.getSearchId()); + for (MemorySearchItem memory : searchResponse.getMemories()) { + System.out.printf(" - Memory ID: %s, content: %s%n", + memory.getMemoryItem().getMemoryId(), memory.getMemoryItem().getContent()); + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/optimization/AgentOptimizationAsyncSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/optimization/AgentOptimizationAsyncSample.java new file mode 100644 index 000000000000..651facd4c700 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/optimization/AgentOptimizationAsyncSample.java @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.optimization; + +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.BetaAgentsAsyncClient; +import com.azure.ai.agents.models.AgentOptimizationEvaluatorRef; +import com.azure.ai.agents.models.AgentOptimizationJob; +import com.azure.ai.agents.models.AgentOptimizationJobInputs; +import com.azure.ai.agents.models.AgentOptimizationJobResult; +import com.azure.ai.agents.models.AgentOptimizationOptions; +import com.azure.ai.agents.models.AgentOptimizationReferenceDatasetInput; +import com.azure.ai.agents.models.OptimizedAgentIdentifier; +import com.azure.core.util.BinaryData; +import com.azure.core.util.Configuration; +import com.azure.core.util.polling.AsyncPollResponse; +import com.azure.core.util.polling.LongRunningOperationStatus; +import com.azure.core.util.polling.PollerFlux; +import com.azure.identity.DefaultAzureCredentialBuilder; + +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * This sample demonstrates how to create and monitor an agent optimization job with the asynchronous beta client. + * + *

Agent optimization is currently a preview feature. Before running the sample, set these environment variables:

+ *
    + *
  • {@code FOUNDRY_PROJECT_ENDPOINT} - the Azure AI Foundry project endpoint.
  • + *
  • {@code FOUNDRY_AGENT_NAME} - the registered agent to optimize.
  • + *
  • {@code DATASET_NAME} - the registered training dataset.
  • + *
  • {@code DATASET_VERSION} - the training dataset version (defaults to {@code 1}).
  • + *
  • {@code EVALUATOR_NAME} - the registered evaluator (defaults to {@code task_adherence}).
  • + *
  • {@code EVAL_MODEL} - the model deployment used to score responses.
  • + *
  • {@code OPTIMIZATION_MODEL} - the model deployment used to generate candidates.
  • + *
+ * + *

For a hosted agent, also set {@code FOUNDRY_AGENT_SYSTEM_PROMPT} to include the baseline system prompt in the + * optimization request. The prompt is optional for agents whose baseline configuration is resolved by the service.

+ */ +public class AgentOptimizationAsyncSample { + private static final Duration POLL_TIMEOUT = Duration.ofMinutes(30); + + public static void main(String[] args) { + Configuration configuration = Configuration.getGlobalConfiguration(); + String endpoint = configuration.get("FOUNDRY_PROJECT_ENDPOINT"); + + BetaAgentsAsyncClient betaAgentsAsyncClient = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .beta() + .buildBetaAgentsAsyncClient(); + + AtomicReference jobId = new AtomicReference<>(); + PollerFlux poller + = betaAgentsAsyncClient.beginCreateOptimizationJob(createOptimizationJob(configuration)) + .setPollInterval(Duration.ofSeconds( + Integer.parseInt(configuration.get("POLL_INTERVAL_SECONDS", "10")))); + + poller + .take(POLL_TIMEOUT) + .doOnNext(response -> recordProgress(response, jobId)) + .last() + .flatMap(AgentOptimizationAsyncSample::getResult) + .doOnNext(AgentOptimizationSample::printResult) + .then(Mono.defer(() -> cleanupAsync(betaAgentsAsyncClient, jobId))) + .onErrorResume(error -> Mono.defer(() -> cleanupAsync(betaAgentsAsyncClient, jobId)) + .then(Mono.error(error))) + .block(); + } + + private static AgentOptimizationJob createOptimizationJob(Configuration configuration) { + String evaluatorVersion = configuration.get("EVALUATOR_VERSION"); + AgentOptimizationEvaluatorRef evaluator = new AgentOptimizationEvaluatorRef( + configuration.get("EVALUATOR_NAME", "task_adherence")); + if (evaluatorVersion != null) { + evaluator.setVersion(evaluatorVersion); + } + + AgentOptimizationReferenceDatasetInput trainDataset = new AgentOptimizationReferenceDatasetInput( + configuration.get("DATASET_NAME")); + trainDataset.setVersion(configuration.get("DATASET_VERSION", "1")); + + AgentOptimizationOptions options = new AgentOptimizationOptions() + .setMaxCandidates(Integer.parseInt(configuration.get("MAX_CANDIDATES", "2"))) + .setEvalModel(configuration.get("EVAL_MODEL", "gpt-4.1-mini")) + .setOptimizationModel(configuration.get("OPTIMIZATION_MODEL", "gpt-5.1")); + + String systemPrompt = configuration.get("FOUNDRY_AGENT_SYSTEM_PROMPT"); + if (systemPrompt != null && !systemPrompt.isEmpty()) { + Map optimizationConfig = new HashMap<>(); + optimizationConfig.put("system_prompt", BinaryData.fromObject(systemPrompt)); + options.setOptimizationConfig(optimizationConfig); + } + + AgentOptimizationJobInputs inputs = new AgentOptimizationJobInputs( + new OptimizedAgentIdentifier(configuration.get("FOUNDRY_AGENT_NAME")), + trainDataset, + Collections.singletonList(evaluator)); + inputs.setOptions(options); + return new AgentOptimizationJob().setInputs(inputs); + } + + private static void recordProgress(AsyncPollResponse response, + AtomicReference jobId) { + AgentOptimizationJob job = response.getValue(); + if (job != null && job.getId() != null) { + jobId.set(job.getId()); + if (job.getProgress() != null) { + System.out.printf("Job %s: %d candidates completed, best score %.4f%n", + job.getId(), job.getProgress().getCandidatesCompleted(), job.getProgress().getBestScore()); + } + } + } + + private static Mono getResult( + AsyncPollResponse response) { + if (response.getStatus() != LongRunningOperationStatus.SUCCESSFULLY_COMPLETED) { + return Mono.error(new IllegalStateException( + "Optimization job completed with status: " + response.getStatus())); + } + return response.getFinalResult() + .switchIfEmpty(Mono.error(new IllegalStateException("The optimization job did not return a result."))); + } + + private static Mono cleanupAsync(BetaAgentsAsyncClient betaAgentsAsyncClient, + AtomicReference jobId) { + String id = jobId.get(); + if (id == null) { + return Mono.empty(); + } + + return betaAgentsAsyncClient.deleteOptimizationJob(id) + .doOnSuccess(unused -> System.out.printf("Optimization job deleted (id: %s)%n", id)) + .onErrorResume(cleanupError -> { + System.err.printf("Failed to delete optimization job %s: %s%n", id, cleanupError.getMessage()); + return Mono.empty(); + }); + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/optimization/AgentOptimizationSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/optimization/AgentOptimizationSample.java new file mode 100644 index 000000000000..45c5141b4c57 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/optimization/AgentOptimizationSample.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.optimization; + +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.BetaAgentsClient; +import com.azure.ai.agents.models.AgentOptimizationCandidate; +import com.azure.ai.agents.models.AgentOptimizationEvaluatorRef; +import com.azure.ai.agents.models.AgentOptimizationJob; +import com.azure.ai.agents.models.AgentOptimizationJobInputs; +import com.azure.ai.agents.models.AgentOptimizationJobResult; +import com.azure.ai.agents.models.AgentOptimizationOptions; +import com.azure.ai.agents.models.AgentOptimizationReferenceDatasetInput; +import com.azure.ai.agents.models.OptimizedAgentIdentifier; +import com.azure.core.util.Configuration; +import com.azure.core.util.BinaryData; +import com.azure.core.util.polling.PollResponse; +import com.azure.core.util.polling.SyncPoller; +import com.azure.identity.DefaultAzureCredentialBuilder; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This sample demonstrates how to create and monitor an agent optimization job with the synchronous beta client. + * + *

Agent optimization is currently a preview feature. Before running the sample, set these environment variables:

+ *
    + *
  • {@code FOUNDRY_PROJECT_ENDPOINT} - the Azure AI Foundry project endpoint.
  • + *
  • {@code FOUNDRY_AGENT_NAME} - the registered agent to optimize.
  • + *
  • {@code DATASET_NAME} - the registered training dataset.
  • + *
  • {@code DATASET_VERSION} - the training dataset version (defaults to {@code 1}).
  • + *
  • {@code EVALUATOR_NAME} - the registered evaluator (defaults to {@code task_adherence}).
  • + *
  • {@code EVAL_MODEL} - the model deployment used to score responses.
  • + *
  • {@code OPTIMIZATION_MODEL} - the model deployment used to generate candidates.
  • + *
+ * + *

For a hosted agent, also set {@code FOUNDRY_AGENT_SYSTEM_PROMPT} to include the baseline system prompt in the + * optimization request. The prompt is optional for agents whose baseline configuration is resolved by the service.

+ */ +public class AgentOptimizationSample { + private static final Duration POLL_TIMEOUT = Duration.ofMinutes(30); + + public static void main(String[] args) { + Configuration configuration = Configuration.getGlobalConfiguration(); + String endpoint = configuration.get("FOUNDRY_PROJECT_ENDPOINT"); + + BetaAgentsClient betaAgentsClient = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .beta() + .buildBetaAgentsClient(); + + AgentOptimizationJob job = createOptimizationJob(configuration); + SyncPoller poller + = betaAgentsClient.beginCreateOptimizationJob(job); + poller.setPollInterval(Duration.ofSeconds( + Integer.parseInt(configuration.get("POLL_INTERVAL_SECONDS", "10")))); + + String jobId = null; + try { + PollResponse initialResponse = poller.poll(); + AgentOptimizationJob createdJob = initialResponse.getValue(); + if (createdJob == null || createdJob.getId() == null) { + throw new IllegalStateException("The optimization service did not return a job ID."); + } + + jobId = createdJob.getId(); + System.out.printf("Optimization job started (id: %s, status: %s)%n", + jobId, initialResponse.getStatus()); + + poller.waitForCompletion(POLL_TIMEOUT); + printResult(poller.getFinalResult()); + } finally { + deleteJob(betaAgentsClient, jobId); + } + } + + private static AgentOptimizationJob createOptimizationJob(Configuration configuration) { + String evaluatorVersion = configuration.get("EVALUATOR_VERSION"); + AgentOptimizationEvaluatorRef evaluator = new AgentOptimizationEvaluatorRef( + configuration.get("EVALUATOR_NAME", "task_adherence")); + if (evaluatorVersion != null) { + evaluator.setVersion(evaluatorVersion); + } + + AgentOptimizationReferenceDatasetInput trainDataset = new AgentOptimizationReferenceDatasetInput( + configuration.get("DATASET_NAME")); + trainDataset.setVersion(configuration.get("DATASET_VERSION", "1")); + + AgentOptimizationOptions options = new AgentOptimizationOptions() + .setMaxCandidates(Integer.parseInt(configuration.get("MAX_CANDIDATES", "2"))) + .setEvalModel(configuration.get("EVAL_MODEL", "gpt-4.1-mini")) + .setOptimizationModel(configuration.get("OPTIMIZATION_MODEL", "gpt-5.1")); + + String systemPrompt = configuration.get("FOUNDRY_AGENT_SYSTEM_PROMPT"); + if (systemPrompt != null && !systemPrompt.isEmpty()) { + Map optimizationConfig = new HashMap<>(); + optimizationConfig.put("system_prompt", BinaryData.fromObject(systemPrompt)); + options.setOptimizationConfig(optimizationConfig); + } + + AgentOptimizationJobInputs inputs = new AgentOptimizationJobInputs( + new OptimizedAgentIdentifier(configuration.get("FOUNDRY_AGENT_NAME")), + trainDataset, + Collections.singletonList(evaluator)); + inputs.setOptions(options); + return new AgentOptimizationJob().setInputs(inputs); + } + + static void printResult(AgentOptimizationJobResult result) { + if (result == null) { + System.out.println("The optimization job did not return a result."); + return; + } + + System.out.printf("Baseline candidate: %s%n", result.getBaseline()); + System.out.printf("Best candidate: %s%n", result.getBest()); + List candidates = result.getCandidates(); + if (candidates != null) { + for (AgentOptimizationCandidate candidate : candidates) { + System.out.printf(" %s (id: %s, score: %.4f, tokens: %.0f)%n", + candidate.getName(), candidate.getCandidateId(), candidate.getAverageScore(), + candidate.getAverageTokens()); + } + } + } + + private static void deleteJob(BetaAgentsClient betaAgentsClient, String jobId) { + if (jobId == null) { + return; + } + + try { + betaAgentsClient.deleteOptimizationJob(jobId); + System.out.printf("Optimization job deleted (id: %s)%n", jobId); + } catch (RuntimeException cleanupError) { + System.err.printf("Failed to delete optimization job %s: %s%n", jobId, cleanupError.getMessage()); + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ReminderPreviewToolboxAsyncSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ReminderPreviewToolboxAsyncSample.java new file mode 100644 index 000000000000..65de9380761f --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ReminderPreviewToolboxAsyncSample.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.toolboxes; + +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.ToolboxesAsyncClient; +import com.azure.ai.agents.models.ReminderPreviewToolboxTool; +import com.azure.ai.agents.models.ToolboxTool; +import com.azure.core.exception.ResourceNotFoundException; +import com.azure.core.util.Configuration; +import com.azure.identity.DefaultAzureCredentialBuilder; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Collections; + +/** + * This sample demonstrates asynchronously creating a toolbox version with the Reminder (preview) tool. + * + *

The reminder tool is available only to hosted agents. Before running, set + * {@code FOUNDRY_PROJECT_ENDPOINT} to your Azure AI Foundry project endpoint.

+ */ +public class ReminderPreviewToolboxAsyncSample { + public static void main(String[] args) { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String toolboxName = "reminder-toolbox-java-async"; + + ToolboxesAsyncClient toolboxesAsyncClient = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .buildToolboxesAsyncClient(); + + ReminderPreviewToolboxTool reminderTool = new ReminderPreviewToolboxTool() + .setName("schedule_reminder") + .setDescription("Schedule a reminder that re-invokes this agent at a future time."); + + Mono workflow = toolboxesAsyncClient.deleteToolbox(toolboxName) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .then(toolboxesAsyncClient.createToolboxVersion( + toolboxName, + Collections.singletonList(reminderTool), + "Built-in reminder tool for a self-scheduling agent.", + null, + null, + null)) + .doOnNext(version -> { + System.out.printf("Created toolbox: %s%n", version.getName()); + System.out.printf("Toolbox version: %s%n", version.getVersion()); + System.out.printf("Tool type: %s%n", version.getTools().get(0).getType()); + }) + .then(toolboxesAsyncClient.deleteToolbox(toolboxName)) + .onErrorResume(error -> toolboxesAsyncClient.deleteToolbox(toolboxName) + .onErrorResume(ResourceNotFoundException.class, ignored -> Mono.empty()) + .then(Mono.error(error))) + .timeout(Duration.ofMinutes(5)); + + workflow.block(); + System.out.printf("Deleted toolbox: %s%n", toolboxName); + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ReminderPreviewToolboxSample.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ReminderPreviewToolboxSample.java new file mode 100644 index 000000000000..664319bad583 --- /dev/null +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/toolboxes/ReminderPreviewToolboxSample.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.agents.toolboxes; + +import com.azure.ai.agents.AgentsClientBuilder; +import com.azure.ai.agents.ToolboxesClient; +import com.azure.ai.agents.models.ReminderPreviewToolboxTool; +import com.azure.ai.agents.models.ToolboxTool; +import com.azure.ai.agents.models.ToolboxVersionDetails; +import com.azure.core.exception.ResourceNotFoundException; +import com.azure.core.util.Configuration; +import com.azure.identity.DefaultAzureCredentialBuilder; + +import java.util.Collections; + +/** + * This sample demonstrates creating a toolbox version with the Reminder (preview) tool. + * + *

The reminder tool is available only to hosted agents. Before running, set + * {@code FOUNDRY_PROJECT_ENDPOINT} to your Azure AI Foundry project endpoint.

+ */ +public class ReminderPreviewToolboxSample { + public static void main(String[] args) { + String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); + String toolboxName = "reminder-toolbox-java"; + + ToolboxesClient toolboxesClient = new AgentsClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .buildToolboxesClient(); + + try { + toolboxesClient.deleteToolbox(toolboxName); + } catch (ResourceNotFoundException ignored) { + // The sample toolbox does not already exist. + } + + try { + // BEGIN: com.azure.ai.agents.toolboxes.ReminderPreviewToolboxSample.createReminderToolbox + + ReminderPreviewToolboxTool reminderTool = new ReminderPreviewToolboxTool() + .setName("schedule_reminder") + .setDescription("Schedule a reminder that re-invokes this agent at a future time."); + + ToolboxVersionDetails version = toolboxesClient.createToolboxVersion( + toolboxName, + Collections.singletonList(reminderTool), + "Built-in reminder tool for a self-scheduling agent.", + null, + null, + null); + + System.out.printf("Created toolbox: %s%n", version.getName()); + System.out.printf("Toolbox version: %s%n", version.getVersion()); + System.out.printf("Tool type: %s%n", version.getTools().get(0).getType()); + + // END: com.azure.ai.agents.toolboxes.ReminderPreviewToolboxSample.createReminderToolbox + } finally { + try { + toolboxesClient.deleteToolbox(toolboxName); + System.out.printf("Deleted toolbox: %s%n", toolboxName); + } catch (ResourceNotFoundException ignored) { + // The sample toolbox may not have been created. + } + } + } +} diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQAsync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQAsync.java index c345bc353a21..0bcb498f6600 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQAsync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQAsync.java @@ -13,12 +13,14 @@ import com.azure.ai.agents.models.PromptAgentDefinition; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; +import com.openai.models.responses.Response; import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseOutputItem; +import com.openai.models.responses.ResponseOutputMessage; import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Collections; -import java.util.concurrent.atomic.AtomicReference; /** * This sample demonstrates how to create an agent with the FabricIQ preview tool using async clients. @@ -27,7 +29,8 @@ *
    *
  • FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint.
  • *
  • FOUNDRY_MODEL_NAME - The model deployment name.
  • - *
  • FABRIC_IQ_PROJECT_CONNECTION_ID - The FabricIQ connection ID.
  • + *
  • FOUNDRY_AGENT_NAME - Optional. The agent name; defaults to fabric-iq-agent.
  • + *
  • FABRIC_IQ_PROJECT_CONNECTION_ID - The fully qualified Fabric IQ project connection resource ID.
  • *
  • FABRIC_IQ_USER_INPUT - Optional. The natural-language question to send to the agent.
  • *
*/ @@ -35,6 +38,7 @@ public class FabricIQAsync { public static void main(String[] args) { String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); String model = Configuration.getGlobalConfiguration().get("FOUNDRY_MODEL_NAME"); + String agentName = Configuration.getGlobalConfiguration().get("FOUNDRY_AGENT_NAME", "fabric-iq-agent"); String fabricIqConnectionId = Configuration.getGlobalConfiguration().get("FABRIC_IQ_PROJECT_CONNECTION_ID"); String userInput = Configuration.getGlobalConfiguration().get("FABRIC_IQ_USER_INPUT", "Use FabricIQ to summarize the available enterprise context."); @@ -45,19 +49,18 @@ public static void main(String[] args) { AgentsAsyncClient agentsAsyncClient = builder.buildAgentsAsyncClient(); ResponsesAsyncClient responsesAsyncClient = builder.buildResponsesAsyncClient(); - AtomicReference agentRef = new AtomicReference<>(); FabricIqPreviewTool fabricIqTool = new FabricIqPreviewTool(fabricIqConnectionId) - .setServerLabel("fabric_iq") + .setServerLabel("fabric-iq-tool") .setRequireApproval("never"); PromptAgentDefinition agentDefinition = new PromptAgentDefinition(model) .setInstructions("Use the available Fabric IQ tools to answer questions and perform tasks.") .setTools(Collections.singletonList(fabricIqTool)); - agentsAsyncClient.createAgentVersion("fabric-iq-agent", agentDefinition) - .flatMap(agent -> { - agentRef.set(agent); + Mono workflow = Mono.usingWhen( + agentsAsyncClient.createAgentVersion(agentName, agentDefinition), + agent -> { System.out.printf("Agent created: %s (version %s)%n", agent.getName(), agent.getVersion()); AgentReference agentReference = new AgentReference(agent.getName()) @@ -66,18 +69,33 @@ public static void main(String[] args) { return responsesAsyncClient.createAzureResponse( new AzureCreateResponseOptions().setAgentReference(agentReference), ResponseCreateParams.builder() - .input(userInput)); - }) - .doOnNext(response -> System.out.println("Response: " + response.output())) - .then(Mono.defer(() -> { - AgentVersionDetails agent = agentRef.get(); - if (agent != null) { - return agentsAsyncClient.deleteAgentVersion(agent.getName(), agent.getVersion()) - .doOnSuccess(v -> System.out.println("Agent deleted")); - } - return Mono.empty(); - })) - .timeout(Duration.ofSeconds(300)) - .block(); + .input(userInput)) + .doOnNext(FabricIQAsync::printResponse) + .then(); + }, + agent -> cleanup(agentsAsyncClient, agent), + (agent, error) -> cleanup(agentsAsyncClient, agent), + agent -> cleanup(agentsAsyncClient, agent)) + .timeout(Duration.ofSeconds(300)); + + workflow.block(); + } + + private static Mono cleanup(AgentsAsyncClient agentsAsyncClient, AgentVersionDetails agent) { + return agentsAsyncClient.deleteAgentVersion(agent.getName(), agent.getVersion()) + .doOnSuccess(v -> System.out.println("Agent deleted")); + } + + private static void printResponse(Response response) { + for (ResponseOutputItem outputItem : response.output()) { + if (outputItem.message().isPresent()) { + ResponseOutputMessage message = outputItem.message().get(); + message.content().forEach(content -> content.outputText().ifPresent(outputText -> { + System.out.println("Agent response: " + outputText.text()); + outputText.annotations() + .forEach(annotation -> System.out.println("Annotation: " + annotation)); + })); + } + } } } diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQSync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQSync.java index 8f0914ecbd58..8e107e6935db 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQSync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/FabricIQSync.java @@ -15,6 +15,8 @@ import com.azure.identity.DefaultAzureCredentialBuilder; import com.openai.models.responses.Response; import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseOutputItem; +import com.openai.models.responses.ResponseOutputMessage; import java.util.Collections; @@ -25,7 +27,8 @@ *
    *
  • FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint.
  • *
  • FOUNDRY_MODEL_NAME - The model deployment name.
  • - *
  • FABRIC_IQ_PROJECT_CONNECTION_ID - The FabricIQ connection ID.
  • + *
  • FOUNDRY_AGENT_NAME - Optional. The agent name; defaults to fabric-iq-agent.
  • + *
  • FABRIC_IQ_PROJECT_CONNECTION_ID - The fully qualified Fabric IQ project connection resource ID.
  • *
  • FABRIC_IQ_USER_INPUT - Optional. The natural-language question to send to the agent.
  • *
*/ @@ -33,6 +36,7 @@ public class FabricIQSync { public static void main(String[] args) { String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); String model = Configuration.getGlobalConfiguration().get("FOUNDRY_MODEL_NAME"); + String agentName = Configuration.getGlobalConfiguration().get("FOUNDRY_AGENT_NAME", "fabric-iq-agent"); String fabricIqConnectionId = Configuration.getGlobalConfiguration().get("FABRIC_IQ_PROJECT_CONNECTION_ID"); String userInput = Configuration.getGlobalConfiguration().get("FABRIC_IQ_USER_INPUT", "Use FabricIQ to summarize the available enterprise context."); @@ -47,7 +51,7 @@ public static void main(String[] args) { // BEGIN: com.azure.ai.agents.define_fabric_iq FabricIqPreviewTool fabricIqTool = new FabricIqPreviewTool(fabricIqConnectionId) - .setServerLabel("fabric_iq") + .setServerLabel("fabric-iq-tool") .setRequireApproval("never"); // END: com.azure.ai.agents.define_fabric_iq @@ -56,7 +60,7 @@ public static void main(String[] args) { .setInstructions("Use the available Fabric IQ tools to answer questions and perform tasks.") .setTools(Collections.singletonList(fabricIqTool)); - AgentVersionDetails agent = agentsClient.createAgentVersion("fabric-iq-agent", agentDefinition); + AgentVersionDetails agent = agentsClient.createAgentVersion(agentName, agentDefinition); System.out.printf("Agent created: %s (version %s)%n", agent.getName(), agent.getVersion()); try { @@ -68,10 +72,23 @@ public static void main(String[] args) { ResponseCreateParams.builder() .input(userInput)); - System.out.println("Response: " + response.output()); + printResponse(response); } finally { agentsClient.deleteAgentVersion(agent.getName(), agent.getVersion()); System.out.println("Agent deleted"); } } + + private static void printResponse(Response response) { + for (ResponseOutputItem outputItem : response.output()) { + if (outputItem.message().isPresent()) { + ResponseOutputMessage message = outputItem.message().get(); + message.content().forEach(content -> content.outputText().ifPresent(outputText -> { + System.out.println("Agent response: " + outputText.text()); + outputText.annotations() + .forEach(annotation -> System.out.println("Annotation: " + annotation)); + })); + } + } + } } diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQAsync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQAsync.java index a9ff201a1083..6dbcc59ce2e2 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQAsync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQAsync.java @@ -13,21 +13,29 @@ import com.azure.ai.agents.models.WorkIqPreviewTool; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; +import com.openai.models.responses.Response; import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseOutputMessage; +import com.openai.models.responses.ToolChoiceOptions; import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Collections; -import java.util.concurrent.atomic.AtomicReference; /** * This sample demonstrates how to create an agent with the Work IQ preview tool using async clients. * + *

Work IQ uses the signed-in user's Microsoft 365 permissions. Configure a Work IQ project + * connection in Microsoft Foundry before running this sample. See the + * Work IQ + * documentation for setup and permission requirements.

+ * *

Before running the sample, set these environment variables:

*
    *
  • FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint.
  • *
  • FOUNDRY_MODEL_NAME - The model deployment name.
  • - *
  • WORK_IQ_PROJECT_CONNECTION_ID - The Work IQ connection ID.
  • + *
  • FOUNDRY_AGENT_NAME - Optional. The name of the agent. Defaults to {@code work-iq-agent}.
  • + *
  • WORK_IQ_PROJECT_CONNECTION_ID - The fully qualified Work IQ project connection resource ID.
  • *
  • WORK_IQ_USER_INPUT - Optional. The natural-language question to send to the agent.
  • *
*/ @@ -35,6 +43,7 @@ public class WorkIQAsync { public static void main(String[] args) { String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); String model = Configuration.getGlobalConfiguration().get("FOUNDRY_MODEL_NAME"); + String agentName = Configuration.getGlobalConfiguration().get("FOUNDRY_AGENT_NAME", "work-iq-agent"); String workIqConnectionId = Configuration.getGlobalConfiguration().get("WORK_IQ_PROJECT_CONNECTION_ID"); String userInput = Configuration.getGlobalConfiguration().get("WORK_IQ_USER_INPUT", "Use Work IQ to summarize the available enterprise context."); @@ -45,37 +54,70 @@ public static void main(String[] args) { AgentsAsyncClient agentsAsyncClient = builder.buildAgentsAsyncClient(); ResponsesAsyncClient responsesAsyncClient = builder.buildResponsesAsyncClient(); - AtomicReference agentRef = new AtomicReference<>(); WorkIqPreviewTool workIqTool = new WorkIqPreviewTool(workIqConnectionId); PromptAgentDefinition agentDefinition = new PromptAgentDefinition(model) - .setInstructions("Use the available Work IQ tools to answer questions and perform tasks.") + .setInstructions("You are a helpful assistant that can access Microsoft 365 data through Work IQ. " + + "Use the Work IQ tool to search and retrieve information from emails, calendar events, " + + "Teams messages, and other Microsoft 365 content.") .setTools(Collections.singletonList(workIqTool)); - agentsAsyncClient.createAgentVersion("work-iq-agent", agentDefinition) - .flatMap(agent -> { - agentRef.set(agent); - System.out.printf("Agent created: %s (version %s)%n", agent.getName(), agent.getVersion()); + agentsAsyncClient.createAgentVersion(agentName, agentDefinition) + .flatMap(agent -> Mono.usingWhen( + Mono.just(agent), + createdAgent -> { + System.out.printf("Agent created: %s (version %s)%n", + createdAgent.getName(), createdAgent.getVersion()); - AgentReference agentReference = new AgentReference(agent.getName()) - .setVersion(agent.getVersion()); + AgentReference agentReference = new AgentReference(createdAgent.getName()) + .setVersion(createdAgent.getVersion()); - return responsesAsyncClient.createAzureResponse( - new AzureCreateResponseOptions().setAgentReference(agentReference), - ResponseCreateParams.builder() - .input(userInput)); - }) - .doOnNext(response -> System.out.println("Response: " + response.output())) - .then(Mono.defer(() -> { - AgentVersionDetails agent = agentRef.get(); - if (agent != null) { - return agentsAsyncClient.deleteAgentVersion(agent.getName(), agent.getVersion()) - .doOnSuccess(v -> System.out.println("Agent deleted")); - } - return Mono.empty(); - })) + return responsesAsyncClient.createAzureResponse( + new AzureCreateResponseOptions().setAgentReference(agentReference), + ResponseCreateParams.builder() + .toolChoice(ToolChoiceOptions.REQUIRED) + .input(userInput)) + .doOnNext(response -> { + System.out.println("Response status: " + + response.status().map(Object::toString).orElse("unknown")); + System.out.println("Agent response: " + getResponseText(response)); + }); + }, + createdAgent -> deleteAgentVersion(agentsAsyncClient, createdAgent), + (createdAgent, error) -> deleteAgentVersion(agentsAsyncClient, createdAgent), + createdAgent -> deleteAgentVersion(agentsAsyncClient, createdAgent))) + .doOnError(error -> System.err.println("Error: " + error.getMessage())) .timeout(Duration.ofSeconds(300)) .block(); } + + private static Mono deleteAgentVersion(AgentsAsyncClient agentsAsyncClient, AgentVersionDetails agent) { + return agentsAsyncClient.deleteAgentVersion(agent.getName(), agent.getVersion()) + .doOnSuccess(unused -> System.out.println("Agent deleted")); + } + + private static String getResponseText(Response response) { + if (response == null || response.output().isEmpty()) { + return ""; + } + + return response.output().stream() + .filter(item -> item.isMessage()) + .map(item -> item.asMessage().content()) + .filter(content -> !content.isEmpty()) + .map(content -> getContentText(content.get(content.size() - 1))) + .reduce((first, second) -> second) + .orElse(""); + } + + private static String getContentText(ResponseOutputMessage.Content content) { + if (content.outputText().isPresent()) { + return content.outputText().get().text(); + } + if (content.refusal().isPresent()) { + return "Refusal: " + content.refusal().get().refusal(); + } + return ""; + } } diff --git a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQSync.java b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQSync.java index 7f2b732057c7..da82b461f4d9 100644 --- a/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQSync.java +++ b/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/tools/WorkIQSync.java @@ -15,17 +15,25 @@ import com.azure.identity.DefaultAzureCredentialBuilder; import com.openai.models.responses.Response; import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseOutputMessage; +import com.openai.models.responses.ToolChoiceOptions; import java.util.Collections; /** * This sample demonstrates how to create an agent with the Work IQ preview tool. * + *

Work IQ uses the signed-in user's Microsoft 365 permissions. Configure a Work IQ project + * connection in Microsoft Foundry before running this sample. See the + * Work IQ + * documentation for setup and permission requirements.

+ * *

Before running the sample, set these environment variables:

*
    *
  • FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint.
  • *
  • FOUNDRY_MODEL_NAME - The model deployment name.
  • - *
  • WORK_IQ_PROJECT_CONNECTION_ID - The Work IQ connection ID.
  • + *
  • FOUNDRY_AGENT_NAME - Optional. The name of the agent. Defaults to {@code work-iq-agent}.
  • + *
  • WORK_IQ_PROJECT_CONNECTION_ID - The fully qualified Work IQ project connection resource ID.
  • *
  • WORK_IQ_USER_INPUT - Optional. The natural-language question to send to the agent.
  • *
*/ @@ -33,6 +41,7 @@ public class WorkIQSync { public static void main(String[] args) { String endpoint = Configuration.getGlobalConfiguration().get("FOUNDRY_PROJECT_ENDPOINT"); String model = Configuration.getGlobalConfiguration().get("FOUNDRY_MODEL_NAME"); + String agentName = Configuration.getGlobalConfiguration().get("FOUNDRY_AGENT_NAME", "work-iq-agent"); String workIqConnectionId = Configuration.getGlobalConfiguration().get("WORK_IQ_PROJECT_CONNECTION_ID"); String userInput = Configuration.getGlobalConfiguration().get("WORK_IQ_USER_INPUT", "Use Work IQ to summarize the available enterprise context."); @@ -44,13 +53,18 @@ public static void main(String[] args) { AgentsClient agentsClient = builder.buildAgentsClient(); ResponsesClient responsesClient = builder.buildResponsesClient(); + // BEGIN: com.azure.ai.agents.define_work_iq + // Create a Work IQ tool with a fully qualified project connection resource ID WorkIqPreviewTool workIqTool = new WorkIqPreviewTool(workIqConnectionId); + // END: com.azure.ai.agents.define_work_iq PromptAgentDefinition agentDefinition = new PromptAgentDefinition(model) - .setInstructions("Use the available Work IQ tools to answer questions and perform tasks.") + .setInstructions("You are a helpful assistant that can access Microsoft 365 data through Work IQ. " + + "Use the Work IQ tool to search and retrieve information from emails, calendar events, " + + "Teams messages, and other Microsoft 365 content.") .setTools(Collections.singletonList(workIqTool)); - AgentVersionDetails agent = agentsClient.createAgentVersion("work-iq-agent", agentDefinition); + AgentVersionDetails agent = agentsClient.createAgentVersion(agentName, agentDefinition); System.out.printf("Agent created: %s (version %s)%n", agent.getName(), agent.getVersion()); try { @@ -60,12 +74,38 @@ public static void main(String[] args) { Response response = responsesClient.createAzureResponse( new AzureCreateResponseOptions().setAgentReference(agentReference), ResponseCreateParams.builder() + .toolChoice(ToolChoiceOptions.REQUIRED) .input(userInput)); - System.out.println("Response: " + response.output()); + System.out.println("Response status: " + response.status().map(Object::toString).orElse("unknown")); + System.out.println("Agent response: " + getResponseText(response)); } finally { agentsClient.deleteAgentVersion(agent.getName(), agent.getVersion()); System.out.println("Agent deleted"); } } + + private static String getResponseText(Response response) { + if (response == null || response.output().isEmpty()) { + return ""; + } + + return response.output().stream() + .filter(item -> item.isMessage()) + .map(item -> item.asMessage().content()) + .filter(content -> !content.isEmpty()) + .map(content -> getContentText(content.get(content.size() - 1))) + .reduce((first, second) -> second) + .orElse(""); + } + + private static String getContentText(ResponseOutputMessage.Content content) { + if (content.outputText().isPresent()) { + return content.outputText().get().text(); + } + if (content.refusal().isPresent()) { + return "Refusal: " + content.refusal().get().refusal(); + } + return ""; + } } diff --git a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/tools/WorkIQSamplesTestBase.java b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/tools/WorkIQSamplesTestBase.java index 209a36c3f70e..a11aec619677 100644 --- a/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/tools/WorkIQSamplesTestBase.java +++ b/sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/tools/WorkIQSamplesTestBase.java @@ -12,6 +12,7 @@ import com.openai.models.responses.Response; import com.openai.models.responses.ResponseCreateParams; import com.openai.models.responses.ResponseStatus; +import com.openai.models.responses.ToolChoiceOptions; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.provider.Arguments; @@ -38,7 +39,9 @@ PromptAgentDefinition createAgentDefinition() { WorkIqPreviewTool workIqTool = new WorkIqPreviewTool(getRecordedConfig("WORK_IQ_PROJECT_CONNECTION_ID")); return new PromptAgentDefinition(getRecordedConfig("FOUNDRY_MODEL_NAME")) - .setInstructions("Use the available Work IQ tools to answer questions and perform tasks.") + .setInstructions("You are a helpful assistant that can access Microsoft 365 data through Work IQ. " + + "Use the Work IQ tool to search and retrieve information from emails, calendar events, " + + "Teams messages, and other Microsoft 365 content.") .setTools(Collections.singletonList(workIqTool)); } @@ -47,7 +50,7 @@ String getUserInput() { } ResponseCreateParams.Builder createResponseParams() { - return ResponseCreateParams.builder().input(getUserInput()); + return ResponseCreateParams.builder().toolChoice(ToolChoiceOptions.REQUIRED).input(getUserInput()); } void assertCompletedResponse(Response response) { diff --git a/sdk/ai/azure-ai-projects/README.md b/sdk/ai/azure-ai-projects/README.md index 27ed0925ad17..8644e93a64df 100644 --- a/sdk/ai/azure-ai-projects/README.md +++ b/sdk/ai/azure-ai-projects/README.md @@ -151,7 +151,7 @@ The async `Beta*AsyncClient` counterparts follow the same behavior. ## Examples -The examples below show common operations for core AI Projects sub-clients. For complete runnable samples, see the [package samples][package_samples]. Additional preview samples are available for data generation jobs (`DataGenerationJobsSample`, `DataGenerationJobsAsyncSample`, and `DataGenerationJobWithEvaluationSample`), model management (`ModelsSample` and `ModelsAsyncSample`), routines (`RoutinesSample`, `RoutinesAsyncSample`, and related trigger/dispatch samples), and packaged skills (`SkillsPackageSample` and `SkillsPackageAsyncSample`). +The examples below show common operations for core AI Projects sub-clients. For complete runnable samples, see the [package samples][package_samples]. Additional preview samples are available for data generation jobs (`DataGenerationJobsSample`, `DataGenerationJobsAsyncSample`, and `DataGenerationJobWithEvaluationSample`), model management (`ModelsSample` and `ModelsAsyncSample`), routines (`RoutinesSample`, `RoutinesAsyncSample`, `RoutinesManualDispatchSample`, `RoutinesManualDispatchAsyncSample`, and related trigger samples), and packaged skills (`SkillsPackageSample` and `SkillsPackageAsyncSample`). ### Connections operations diff --git a/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesManualDispatchAsyncSample.java b/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesManualDispatchAsyncSample.java index 797fcff55b1e..09580489d691 100644 --- a/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesManualDispatchAsyncSample.java +++ b/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesManualDispatchAsyncSample.java @@ -3,25 +3,26 @@ package com.azure.ai.projects; -import com.azure.ai.projects.models.CustomRoutineTrigger; import com.azure.ai.projects.models.InvokeAgentResponsesApiDispatchPayload; import com.azure.ai.projects.models.RoutineAction; import com.azure.ai.projects.models.RoutineTrigger; +import com.azure.ai.projects.models.TimerRoutineTrigger; import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; import reactor.core.publisher.Mono; import java.time.Duration; -import java.util.Collections; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.HashMap; import java.util.Map; /** * Sample demonstrating manual dispatch of a routine using the asynchronous {@link BetaRoutinesAsyncClient}. * - *

The routine is created with a manual {@link CustomRoutineTrigger}, dispatched on demand with an input - * payload, and the resulting run is polled until completion. Routines are a preview feature. Before running, set:

+ *

The routine is created with a timer trigger scheduled in the future, dispatched early with an input payload, + * and the resulting run is polled until completion. Routines are a preview feature. Before running, set:

*
    *
  • {@code FOUNDRY_PROJECT_ENDPOINT} - the Azure AI Foundry project endpoint.
  • *
  • {@code HOSTED_AGENT_NAME} - the name of a deployed hosted agent.
  • @@ -43,14 +44,15 @@ public static void main(String[] args) { .buildBetaRoutinesAsyncClient(); RoutineAction action = RoutinesSampleUtils.agentAction(agentName); - CustomRoutineTrigger trigger = new CustomRoutineTrigger("manual", Collections.emptyMap()); + TimerRoutineTrigger trigger = new TimerRoutineTrigger() + .setAt(OffsetDateTime.now(ZoneOffset.UTC).plusHours(1)); Map triggers = new HashMap<>(); - triggers.put("manual", trigger); + triggers.put("once", trigger); - routinesAsyncClient.deleteRoutine(ROUTINE_NAME) + Mono workflow = routinesAsyncClient.deleteRoutine(ROUTINE_NAME) .onErrorResume(ignored -> Mono.empty()) .then(routinesAsyncClient.createOrUpdateRoutine(ROUTINE_NAME, - "Routine used by manual dispatch sample.", true, triggers, action)) + "Timer routine dispatched before its scheduled fire time.", true, triggers, action)) .flatMap(created -> { System.out.printf("Created routine: %s enabled=%s%n", created.getName(), created.isEnabled()); return routinesAsyncClient.dispatchRoutine(created.getName(), @@ -61,11 +63,18 @@ public static void main(String[] args) { System.out.printf("Waiting up to %d minutes for the dispatched run...%n", RUN_TIMEOUT.toMinutes()); return RoutinesSampleUtils.waitForCompletedRunAsync(routinesAsyncClient, created.getName(), - RUN_TIMEOUT) + dispatch.getDispatchId(), RUN_TIMEOUT) + .switchIfEmpty(Mono.error(new IllegalStateException( + "The dispatched routine did not complete within the timeout."))) .doOnNext(completedRun -> RoutinesSampleUtils.reportRun(completedRun, RUN_TIMEOUT)) .then(); }); - }) + }); + + workflow + .onErrorResume(error -> routinesAsyncClient.deleteRoutine(ROUTINE_NAME) + .onErrorResume(ignored -> Mono.empty()) + .then(Mono.error(error))) .then(routinesAsyncClient.deleteRoutine(ROUTINE_NAME)) .doOnSuccess(unused -> System.out.println("Routine deleted")) .block(); diff --git a/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesManualDispatchSample.java b/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesManualDispatchSample.java index d9831bcdbb62..c8bf9a3b1784 100644 --- a/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesManualDispatchSample.java +++ b/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesManualDispatchSample.java @@ -3,27 +3,28 @@ package com.azure.ai.projects; -import com.azure.ai.projects.models.CustomRoutineTrigger; import com.azure.ai.projects.models.DispatchRoutineResult; import com.azure.ai.projects.models.InvokeAgentResponsesApiDispatchPayload; import com.azure.ai.projects.models.Routine; import com.azure.ai.projects.models.RoutineAction; import com.azure.ai.projects.models.RoutineRun; import com.azure.ai.projects.models.RoutineTrigger; +import com.azure.ai.projects.models.TimerRoutineTrigger; import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.identity.DefaultAzureCredentialBuilder; import java.time.Duration; -import java.util.Collections; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.HashMap; import java.util.Map; /** * Sample demonstrating manual dispatch of a routine using the synchronous {@link BetaRoutinesClient}. * - *

    The routine is created with a manual {@link CustomRoutineTrigger}, dispatched on demand with an input - * payload, and the resulting run is polled until completion. Routines are a preview feature. Before running, set:

    + *

    The routine is created with a timer trigger scheduled in the future, dispatched early with an input payload, + * and the resulting run is polled until completion. Routines are a preview feature. Before running, set:

    *
      *
    • {@code FOUNDRY_PROJECT_ENDPOINT} - the Azure AI Foundry project endpoint.
    • *
    • {@code HOSTED_AGENT_NAME} - the name of a deployed hosted agent.
    • @@ -51,15 +52,16 @@ public static void main(String[] args) { // The sample routine does not already exist. } + Routine created = null; try { RoutineAction action = RoutinesSampleUtils.agentAction(agentName); - CustomRoutineTrigger trigger = new CustomRoutineTrigger("manual", - Collections.emptyMap()); + TimerRoutineTrigger trigger = new TimerRoutineTrigger() + .setAt(OffsetDateTime.now(ZoneOffset.UTC).plusHours(1)); Map triggers = new HashMap<>(); - triggers.put("manual", trigger); + triggers.put("once", trigger); - Routine created = routinesClient.createOrUpdateRoutine(ROUTINE_NAME, - "Routine used by manual dispatch sample.", true, triggers, action); + created = routinesClient.createOrUpdateRoutine(ROUTINE_NAME, + "Timer routine dispatched before its scheduled fire time.", true, triggers, action); System.out.printf("Created routine: %s enabled=%s%n", created.getName(), created.isEnabled()); // BEGIN:com.azure.ai.projects.RoutinesManualDispatchSample.dispatch @@ -71,11 +73,13 @@ public static void main(String[] args) { System.out.printf("Waiting up to %d minutes for the dispatched run...%n", RUN_TIMEOUT.toMinutes()); RoutineRun completedRun = RoutinesSampleUtils.waitForCompletedRun(routinesClient, created.getName(), - RUN_TIMEOUT); + dispatch.getDispatchId(), RUN_TIMEOUT); RoutinesSampleUtils.reportRun(completedRun, RUN_TIMEOUT); } finally { - routinesClient.deleteRoutine(ROUTINE_NAME); - System.out.println("Routine deleted"); + if (created != null) { + routinesClient.deleteRoutine(ROUTINE_NAME); + System.out.println("Routine deleted"); + } } } } diff --git a/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesSampleUtils.java b/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesSampleUtils.java index e45501e04a27..f7223651c2b1 100644 --- a/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesSampleUtils.java +++ b/sdk/ai/azure-ai-projects/src/samples/java/com/azure/ai/projects/RoutinesSampleUtils.java @@ -6,6 +6,7 @@ import com.azure.ai.projects.models.InvokeAgentResponsesApiRoutineAction; import com.azure.ai.projects.models.RoutineAction; import com.azure.ai.projects.models.RoutineRun; +import com.azure.ai.projects.models.RoutineRunPhase; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,26 +33,42 @@ static RoutineAction agentAction(String agentName) { } /** - * Returns {@code true} when the run has reached a terminal status. + * Returns {@code true} when the run has reached a terminal phase or status. * * @param status the run status. * @return whether the status is terminal. */ static boolean isTerminalStatus(String status) { return "finished".equalsIgnoreCase(status) + || "completed".equalsIgnoreCase(status) || "failed".equalsIgnoreCase(status) || "killed".equalsIgnoreCase(status); } + private static boolean isTerminalPhase(RoutineRunPhase phase) { + return RoutineRunPhase.COMPLETED.equals(phase) || RoutineRunPhase.FAILED.equals(phase); + } + + private static boolean isTerminalRun(RoutineRun run) { + return isTerminalPhase(run.getPhase()) || isTerminalStatus(run.getStatus()); + } + + private static boolean isFailedRun(RoutineRun run) { + return RoutineRunPhase.FAILED.equals(run.getPhase()) + || "failed".equalsIgnoreCase(run.getStatus()) + || "killed".equalsIgnoreCase(run.getStatus()); + } + private static void printRun(RoutineRun run) { - System.out.printf(" - run ID %s, status: %s, trigger type: %s, triggered at: %s, ended at: %s%n", - run.getId(), run.getStatus(), run.getTriggerType(), + System.out.printf(" - run ID %s, status: %s, phase: %s, trigger type: %s, dispatch ID: %s, " + + "triggered at: %s, ended at: %s%n", + run.getId(), run.getStatus(), run.getPhase(), run.getTriggerType(), run.getDispatchId(), run.getTriggeredAt() == null ? "" : run.getTriggeredAt(), run.getEndedAt() == null ? "" : run.getEndedAt()); } /** - * Synchronously polls the routine's runs until one reaches a terminal status or the timeout elapses. + * Synchronously polls the routine's runs until one reaches a terminal phase or the timeout elapses. * * @param routinesClient the routines client. * @param routineName the routine name. @@ -59,12 +76,26 @@ private static void printRun(RoutineRun run) { * @return the completed run, or {@code null} if none completed before the timeout. */ static RoutineRun waitForCompletedRun(BetaRoutinesClient routinesClient, String routineName, Duration timeout) { + return waitForCompletedRun(routinesClient, routineName, null, timeout); + } + + /** + * Synchronously polls a specific dispatched run until it reaches a terminal phase or the timeout elapses. + * + * @param routinesClient the routines client. + * @param routineName the routine name. + * @param dispatchId the dispatch identifier to follow. + * @param timeout the maximum time to wait. + * @return the completed run, or {@code null} if it did not complete before the timeout. + */ + static RoutineRun waitForCompletedRun(BetaRoutinesClient routinesClient, String routineName, String dispatchId, + Duration timeout) { Instant deadline = Instant.now().plus(timeout); while (Instant.now().isBefore(deadline)) { RoutineRun completed = null; for (RoutineRun run : routinesClient.listRoutineRuns(routineName)) { printRun(run); - if (isTerminalStatus(run.getStatus())) { + if ((dispatchId == null || dispatchId.equals(run.getDispatchId())) && isTerminalRun(run)) { completed = run; } } @@ -82,7 +113,7 @@ static RoutineRun waitForCompletedRun(BetaRoutinesClient routinesClient, String } /** - * Asynchronously polls the routine's runs until one reaches a terminal status or the timeout elapses. + * Asynchronously polls the routine's runs until one reaches a terminal phase or the timeout elapses. * * @param routinesAsyncClient the asynchronous routines client. * @param routineName the routine name. @@ -91,12 +122,27 @@ static RoutineRun waitForCompletedRun(BetaRoutinesClient routinesClient, String */ static Mono waitForCompletedRunAsync(BetaRoutinesAsyncClient routinesAsyncClient, String routineName, Duration timeout) { + return waitForCompletedRunAsync(routinesAsyncClient, routineName, null, timeout); + } + + /** + * Asynchronously polls a specific dispatched run until it reaches a terminal phase or the timeout elapses. + * + * @param routinesAsyncClient the asynchronous routines client. + * @param routineName the routine name. + * @param dispatchId the dispatch identifier to follow. + * @param timeout the maximum time to wait. + * @return a {@link Mono} that emits the completed run, or completes empty if it did not complete before the + * timeout. + */ + static Mono waitForCompletedRunAsync(BetaRoutinesAsyncClient routinesAsyncClient, String routineName, + String dispatchId, Duration timeout) { Instant deadline = Instant.now().plus(timeout); return Flux.interval(Duration.ZERO, Duration.ofSeconds(10)) .takeWhile(tick -> Instant.now().isBefore(deadline)) .concatMap(tick -> routinesAsyncClient.listRoutineRuns(routineName) .doOnNext(RoutinesSampleUtils::printRun) - .filter(run -> isTerminalStatus(run.getStatus())) + .filter(run -> (dispatchId == null || dispatchId.equals(run.getDispatchId())) && isTerminalRun(run)) .next()) .next(); } @@ -106,7 +152,7 @@ static void reportRun(RoutineRun completedRun, Duration timeout) { System.out.printf("The run did not complete within %d seconds.%n", timeout.getSeconds()); return; } - if ("failed".equalsIgnoreCase(completedRun.getStatus())) { + if (isFailedRun(completedRun)) { System.out.printf("The run failed. Type: %s Message: %s%n", completedRun.getErrorType(), completedRun.getErrorMessage()); return;