Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions sdk/ai/azure-ai-agents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
- Added new `OpenApiSync`/`OpenApiAsync` samples demonstrating end-to-end OpenAPI tool integration: loading a spec file, creating an agent with an `OpenApiTool`, and invoking an external API via conversation.
- Added new tool samples for parity with the Python SDK: `AzureFunctionSync`/`AzureFunctionAsync`, `BingCustomSearchSync`/`BingCustomSearchAsync`, `MemorySearchSync`/`MemorySearchAsync`, `McpWithConnectionSync`/`McpWithConnectionAsync`, and `OpenApiWithConnectionSync`/`OpenApiWithConnectionAsync`.
- Added `setComparisonFilter(ComparisonFilter)` and `setCompoundFilter(CompoundFilter)` convenience methods on `FileSearchTool`, accepting the openai-java filter types directly.
- Added streaming response methods to `ResponsesClient` and `ResponsesAsyncClient`:
- `createStreamingWithAgent` and `createStreamingWithAgentConversation` on `ResponsesClient` return `IterableStream<ResponseStreamEvent>` for synchronous streaming.
- `createStreamingWithAgent` and `createStreamingWithAgentConversation` on `ResponsesAsyncClient` return `Flux<ResponseStreamEvent>` for asynchronous streaming.
- Added `StreamingUtils` implementation helper that bridges OpenAI `StreamResponse` to `IterableStream` and `AsyncStreamResponse` to `Flux`.
- Added streaming samples: `SimpleStreamingSync`/`SimpleStreamingAsync`, `FunctionCallStreamingSync`/`FunctionCallStreamingAsync`, and `CodeInterpreterStreamingSync`/`CodeInterpreterStreamingAsync`.

### Breaking Changes

Expand All @@ -21,6 +26,7 @@
### Other Changes

- Added `ToolsTests` and `ToolsAsyncTests` with recorded end-to-end test coverage for OpenAPI, Code Interpreter, Function Call, Web Search, MCP, and File Search tools.
- Added `StreamingTests` and `StreamingAsyncTests` with recorded test coverage for streaming responses (simple prompt, function calling, and Code Interpreter scenarios).

## 2.0.0-beta.2 (2026-03-04)

Expand Down
61 changes: 61 additions & 0 deletions sdk/ai/azure-ai-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,67 @@ See the full sample in [OpenApiWithConnectionSync.java](https://github.com/Azure

---

### Streaming responses

The `ResponsesClient` and `ResponsesAsyncClient` support streaming, which allows you to process response events as they arrive rather than waiting for the full response. This is useful for displaying text to users in real time and observing tool execution progress.

#### Synchronous streaming

The synchronous streaming methods return `IterableStream<ResponseStreamEvent>`, which can be consumed with a standard for-each loop. Use the `ResponseAccumulator` from the OpenAI SDK to collect events into a final `Response`:

```java com.azure.ai.agents.streaming.simple_sync
// Use ResponseAccumulator to collect streamed events into a final Response
ResponseAccumulator responseAccumulator = ResponseAccumulator.create();

// Stream response - text is printed as it arrives
IterableStream<ResponseStreamEvent> events =
responsesClient.createStreamingWithAgent(agentReference,
ResponseCreateParams.builder()
.input("Tell me a short story about a brave explorer."));

for (ResponseStreamEvent event : events) {
responseAccumulator.accumulate(event);
event.outputTextDelta()
.ifPresent(textEvent -> System.out.print(textEvent.delta()));
}
System.out.println(); // newline after streamed text

// Access the complete accumulated response
Response response = responseAccumulator.response();
System.out.println("\nResponse ID: " + response.id());
```

See the full samples in [SimpleStreamingSync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/streaming/SimpleStreamingSync.java), [FunctionCallStreamingSync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/streaming/FunctionCallStreamingSync.java), and [CodeInterpreterStreamingSync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/streaming/CodeInterpreterStreamingSync.java).

#### Asynchronous streaming

The asynchronous streaming methods return `Flux<ResponseStreamEvent>`, integrating naturally with Reactor pipelines:

```java com.azure.ai.agents.streaming.simple_async
// Use ResponseAccumulator to collect streamed events into a final Response
ResponseAccumulator responseAccumulator = ResponseAccumulator.create();

// Stream response asynchronously - text is printed as each chunk arrives
return responsesAsyncClient.createStreamingWithAgent(agentReference,
ResponseCreateParams.builder()
.input("Tell me a short story about a brave explorer."))
.doOnNext(event -> {
responseAccumulator.accumulate(event);
event.outputTextDelta()
.ifPresent(textEvent -> System.out.print(textEvent.delta()));
})
.then(Mono.fromCallable(() -> {
System.out.println(); // newline after streamed text

// Access the complete accumulated response
Response response = responseAccumulator.response();
System.out.println("\nResponse ID: " + response.id());
```

See the full samples in [SimpleStreamingAsync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/streaming/SimpleStreamingAsync.java), [FunctionCallStreamingAsync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/streaming/FunctionCallStreamingAsync.java), and [CodeInterpreterStreamingAsync.java](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/ai/azure-ai-agents/src/samples/java/com/azure/ai/agents/streaming/CodeInterpreterStreamingAsync.java).

---

### Service API versions

The client library targets the latest service API version by default.
Expand Down
2 changes: 1 addition & 1 deletion sdk/ai/azure-ai-agents/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "java",
"TagPrefix": "java/ai/azure-ai-agents",
"Tag": "java/ai/azure-ai-agents_25e4920008"
"Tag": "java/ai/azure-ai-agents_4b3a190a4f"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
package com.azure.ai.agents;

import com.azure.ai.agents.implementation.OpenAIJsonHelper;
import com.azure.ai.agents.implementation.StreamingUtils;
import com.azure.ai.agents.models.AgentReference;
import com.azure.core.annotation.ServiceClient;
import com.openai.client.OpenAIClientAsync;
import com.openai.core.JsonValue;
import com.openai.models.responses.Response;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseStreamEvent;
import com.openai.services.async.ResponseServiceAsync;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.HashMap;
Expand Down Expand Up @@ -107,4 +110,49 @@ public Mono<Response> createWithAgent(AgentReference agentReference, ResponseCre
public Mono<Response> createWithAgent(AgentReference agentReference) {
return createWithAgent(agentReference, new ResponseCreateParams.Builder());
}

/**
* Creates a streaming response with an agent.
*
* @param agentReference The agent reference.
* @param params The parameters to create the response.
* @return A Flux of ResponseStreamEvent.
*/
public Flux<ResponseStreamEvent> createStreamingWithAgent(AgentReference agentReference,
ResponseCreateParams.Builder params) {
Objects.requireNonNull(agentReference, "agentReference cannot be null");
Objects.requireNonNull(params, "params cannot be null");

JsonValue agentRefJsonValue = OpenAIJsonHelper.toJsonValue(agentReference);

Map<String, JsonValue> additionalBodyProperties = new HashMap<>();
additionalBodyProperties.put("agent_reference", agentRefJsonValue);

params.additionalBodyProperties(additionalBodyProperties);
return StreamingUtils.toFlux(this.responseServiceAsync.createStreaming(params.build()));
}

/**
* Creates a streaming response with an agent conversation.
*
* @param agentReference The agent reference.
* @param conversationId The conversation ID.
* @param params The parameters to create the response.
* @return A Flux of ResponseStreamEvent.
*/
public Flux<ResponseStreamEvent> createStreamingWithAgentConversation(AgentReference agentReference,
String conversationId, ResponseCreateParams.Builder params) {
Objects.requireNonNull(agentReference, "agentReference cannot be null");
Objects.requireNonNull(conversationId, "conversationId cannot be null");
Objects.requireNonNull(params, "params cannot be null");

JsonValue agentRefJsonValue = OpenAIJsonHelper.toJsonValue(agentReference);

Map<String, JsonValue> additionalBodyProperties = new HashMap<>();
params.conversation(conversationId);
additionalBodyProperties.put("agent_reference", agentRefJsonValue);

params.additionalBodyProperties(additionalBodyProperties);
return StreamingUtils.toFlux(this.responseServiceAsync.createStreaming(params.build()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
package com.azure.ai.agents;

import com.azure.ai.agents.implementation.OpenAIJsonHelper;
import com.azure.ai.agents.implementation.StreamingUtils;
import com.azure.ai.agents.models.AgentReference;
import com.azure.core.annotation.ServiceClient;
import com.azure.core.util.IterableStream;
import com.openai.client.OpenAIClient;
import com.openai.core.JsonValue;
import com.openai.models.responses.Response;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseStreamEvent;
import com.openai.services.blocking.ResponseService;

import java.util.HashMap;
Expand Down Expand Up @@ -106,4 +109,49 @@ public Response createWithAgent(AgentReference agentReference, ResponseCreatePar
public Response createWithAgent(AgentReference agentReference) {
return createWithAgent(agentReference, new ResponseCreateParams.Builder());
}

/**
* Creates a streaming response with an agent.
*
* @param agentReference The agent reference.
* @param params The parameters to create the response.
* @return An IterableStream of ResponseStreamEvent.
*/
public IterableStream<ResponseStreamEvent> createStreamingWithAgent(AgentReference agentReference,
ResponseCreateParams.Builder params) {
Objects.requireNonNull(agentReference, "agentReference cannot be null");
Objects.requireNonNull(params, "params cannot be null");

JsonValue agentRefJsonValue = OpenAIJsonHelper.toJsonValue(agentReference);

Map<String, JsonValue> additionalBodyProperties = new HashMap<>();
additionalBodyProperties.put("agent_reference", agentRefJsonValue);

params.additionalBodyProperties(additionalBodyProperties);
return StreamingUtils.toIterableStream(this.responseService.createStreaming(params.build()));
}

/**
* Creates a streaming response with an agent conversation.
*
* @param agentReference The agent reference.
* @param conversationId The conversation ID.
* @param params The parameters to create the response.
* @return An IterableStream of ResponseStreamEvent.
*/
public IterableStream<ResponseStreamEvent> createStreamingWithAgentConversation(AgentReference agentReference,
String conversationId, ResponseCreateParams.Builder params) {
Objects.requireNonNull(agentReference, "agentReference cannot be null");
Objects.requireNonNull(conversationId, "conversationId cannot be null");
Objects.requireNonNull(params, "params cannot be null");

JsonValue agentRefJsonValue = OpenAIJsonHelper.toJsonValue(agentReference);

Map<String, JsonValue> additionalBodyProperties = new HashMap<>();
params.conversation(conversationId);
additionalBodyProperties.put("agent_reference", agentRefJsonValue);

params.additionalBodyProperties(additionalBodyProperties);
return StreamingUtils.toIterableStream(this.responseService.createStreaming(params.build()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.ai.agents.implementation;

import com.azure.core.util.IterableStream;
import com.azure.core.util.logging.ClientLogger;
import com.openai.core.http.AsyncStreamResponse;
import com.openai.core.http.StreamResponse;
import reactor.core.publisher.Flux;

import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* Utility methods for bridging OpenAI streaming types to Azure SDK / Reactor types.
*/
public final class StreamingUtils {

private static final ClientLogger LOGGER = new ClientLogger(StreamingUtils.class);

private StreamingUtils() {
}

/**
* Converts an OpenAI {@link StreamResponse} into an {@link IterableStream} that automatically
* closes the underlying stream resource when all events have been consumed.
*
* <p>The returned {@link IterableStream} is single-use: calling {@code iterator()} or
* {@code stream()} more than once will throw {@link IllegalStateException}, because the
* underlying {@link StreamResponse} can only be consumed once.</p>
*
* @param streamResponse The OpenAI stream response.
* @param <T> The type of the streamed items.
* @return An IterableStream wrapping the given StreamResponse.
*/
public static <T> IterableStream<T> toIterableStream(StreamResponse<T> streamResponse) {
Iterator<T> inner = streamResponse.stream().iterator();
AtomicBoolean iteratorCreated = new AtomicBoolean(false);

Iterable<T> singleUseIterable = () -> {
if (!iteratorCreated.compareAndSet(false, true)) {
throw LOGGER
.logExceptionAsError(new IllegalStateException("This streaming response has already been consumed. "
+ "A streaming IterableStream can only be iterated once."));
}
return new Iterator<T>() {
@Override
public boolean hasNext() {
boolean hasNext = inner.hasNext();
if (!hasNext) {
streamResponse.close();
}
return hasNext;
}

@Override
public T next() {
if (!inner.hasNext()) {
throw LOGGER.logExceptionAsError(new NoSuchElementException());
}
return inner.next();
}
};
};
return new IterableStream<>(singleUseIterable);
}

/**
* Bridges an OpenAI {@link AsyncStreamResponse} into a Reactor {@link Flux}, forwarding
* events, errors, and completion signals while closing the underlying stream on disposal.
*
* <p>Thread safety: {@code Flux.create} defaults to {@code PUSH_PULL} mode, which wraps the
* sink in a {@code SerializedFluxSink} that serializes {@code next/error/complete} calls.
* Additionally, the OpenAI {@code AsyncStreamResponse} implementation invokes the handler
* sequentially on a single executor thread (via {@code Stream.forEach}), so concurrent
* emission cannot occur in practice.</p>
*
* @param asyncStream The OpenAI async stream response.
* @param <T> The type of the streamed items.
* @return A Flux wrapping the given AsyncStreamResponse.
*/
public static <T> Flux<T> toFlux(AsyncStreamResponse<T> asyncStream) {
return Flux.create(sink -> {
asyncStream.subscribe(new AsyncStreamResponse.Handler<T>() {
@Override
public void onNext(T event) {
sink.next(event);
}

@Override
public void onComplete(Optional<Throwable> error) {
if (error.isPresent()) {
sink.error(error.get());
} else {
sink.complete();
}
}
});
Comment thread
jpalvarezl marked this conversation as resolved.
sink.onDispose(asyncStream::close);
});
}
}
Loading