Skip to content

Commit c8cb24c

Browse files
committed
response-accumulator: added accumulator and documented streaming support.
1 parent 4dc27fc commit c8cb24c

File tree

4 files changed

+296
-61
lines changed

4 files changed

+296
-61
lines changed

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,11 @@ client.chat()
349349

350350
ChatCompletion chatCompletion = chatCompletionAccumulator.chatCompletion();
351351
```
352+
352353
The SDK provides conveniences for streamed responses. A
353354
[`ResponseAccumulator`](openai-java-core/src/main/kotlin/com/openai/helpers/ResponseAccumulator.kt)
354355
can record the stream of response events as they are processed and accumulate a
355-
[`Response`](openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletion.kt)
356+
[`Response`](openai-java-core/src/main/kotlin/com/openai/models/responses/Response.kt)
356357
object similar to that which would have been returned by the non-streaming API.
357358

358359
For a synchronous response add a
@@ -573,11 +574,16 @@ For a full example of the usage of _Structured Outputs_ with Streaming and the C
573574
see
574575
[`StructuredOutputsStreamingExample`](openai-java-example/src/main/java/com/openai/example/StructuredOutputsStreamingExample.java).
575576

576-
At present, there is no accumulator for streaming responses using the Responses API. It is still
577-
possible to derive a JSON schema from a Java class and create a streaming response for a
578-
[`StructuredResponseCreateParams`](openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseCreateParams.kt)
579-
object, but there is no helper for deserialization of the response to an instance of that Java
580-
class.
577+
With the Responses API, accumulate events while streaming using the
578+
[`ResponseAccumulator`](openai-java-core/src/main/kotlin/com/openai/helpers/ResponseAccumulator.kt).
579+
Once accumulated, use `ResponseAccumulator.response(Class<T>)` to convert the accumulated `Response`
580+
into a
581+
[`StructuredResponse`](openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponse.kt).
582+
The [`StructuredResponse`] can then automatically deserialize the JSON strings into instances of
583+
your Java class.
584+
585+
For a full example of the usage of _Structured Outputs_ with Streaming and the Responses API, see
586+
[`ResponsesStructuredOutputsStreamingExample`](openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsStreamingExample.java).
581587

582588
### Defining JSON schema properties
583589

openai-java-core/src/main/kotlin/com/openai/helpers/ResponseAccumulator.kt

Lines changed: 166 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.openai.helpers
22

3+
import com.openai.errors.OpenAIInvalidDataException
34
import com.openai.models.responses.Response
45
import com.openai.models.responses.ResponseAudioDeltaEvent
56
import com.openai.models.responses.ResponseAudioDoneEvent
@@ -21,19 +22,41 @@ import com.openai.models.responses.ResponseFileSearchCallInProgressEvent
2122
import com.openai.models.responses.ResponseFileSearchCallSearchingEvent
2223
import com.openai.models.responses.ResponseFunctionCallArgumentsDeltaEvent
2324
import com.openai.models.responses.ResponseFunctionCallArgumentsDoneEvent
25+
import com.openai.models.responses.ResponseImageGenCallCompletedEvent
26+
import com.openai.models.responses.ResponseImageGenCallGeneratingEvent
27+
import com.openai.models.responses.ResponseImageGenCallInProgressEvent
28+
import com.openai.models.responses.ResponseImageGenCallPartialImageEvent
2429
import com.openai.models.responses.ResponseInProgressEvent
2530
import com.openai.models.responses.ResponseIncompleteEvent
31+
import com.openai.models.responses.ResponseMcpCallArgumentsDeltaEvent
32+
import com.openai.models.responses.ResponseMcpCallArgumentsDoneEvent
33+
import com.openai.models.responses.ResponseMcpCallCompletedEvent
34+
import com.openai.models.responses.ResponseMcpCallFailedEvent
35+
import com.openai.models.responses.ResponseMcpCallInProgressEvent
36+
import com.openai.models.responses.ResponseMcpListToolsCompletedEvent
37+
import com.openai.models.responses.ResponseMcpListToolsFailedEvent
38+
import com.openai.models.responses.ResponseMcpListToolsInProgressEvent
2639
import com.openai.models.responses.ResponseOutputItemAddedEvent
2740
import com.openai.models.responses.ResponseOutputItemDoneEvent
41+
import com.openai.models.responses.ResponseOutputTextAnnotationAddedEvent
42+
import com.openai.models.responses.ResponseQueuedEvent
43+
import com.openai.models.responses.ResponseReasoningDeltaEvent
44+
import com.openai.models.responses.ResponseReasoningDoneEvent
45+
import com.openai.models.responses.ResponseReasoningSummaryDeltaEvent
46+
import com.openai.models.responses.ResponseReasoningSummaryDoneEvent
47+
import com.openai.models.responses.ResponseReasoningSummaryPartAddedEvent
48+
import com.openai.models.responses.ResponseReasoningSummaryPartDoneEvent
49+
import com.openai.models.responses.ResponseReasoningSummaryTextDeltaEvent
50+
import com.openai.models.responses.ResponseReasoningSummaryTextDoneEvent
2851
import com.openai.models.responses.ResponseRefusalDeltaEvent
2952
import com.openai.models.responses.ResponseRefusalDoneEvent
3053
import com.openai.models.responses.ResponseStreamEvent
31-
import com.openai.models.responses.ResponseTextAnnotationDeltaEvent
3254
import com.openai.models.responses.ResponseTextDeltaEvent
3355
import com.openai.models.responses.ResponseTextDoneEvent
3456
import com.openai.models.responses.ResponseWebSearchCallCompletedEvent
3557
import com.openai.models.responses.ResponseWebSearchCallInProgressEvent
3658
import com.openai.models.responses.ResponseWebSearchCallSearchingEvent
59+
import com.openai.models.responses.StructuredResponse
3760

3861
/**
3962
* An accumulator that constructs a [Response] from a sequence of streamed events. Pass all events
@@ -64,9 +87,28 @@ class ResponseAccumulator private constructor() {
6487
*/
6588
fun response() = checkNotNull(response) { "Completed response is not yet received." }
6689

90+
/**
91+
* Gets the final accumulated response with support for structured outputs. Until the last event
92+
* has been accumulated, a [StructuredResponse] will not be available. Wait until all events
93+
* have been handled by [accumulate] before calling this method. See that method for more
94+
* details on how the last event is detected. See the
95+
* [SDK documentation](https://github.com/openai/openai-java/#usage-with-streaming) for more
96+
* details and example code.
97+
*
98+
* @param responseType The Java class from which the JSON schema in the request was derived. The
99+
* output JSON conforming to that schema can be converted automatically back to an instance of
100+
* that Java class by the [StructuredResponse].
101+
* @throws IllegalStateException If called before the last event has been accumulated.
102+
* @throws OpenAIInvalidDataException If the JSON data cannot be parsed to an instance of the
103+
* [responseType] class.
104+
*/
105+
fun <T : Any> response(responseType: Class<T>) = StructuredResponse(responseType, response())
106+
67107
/**
68108
* Accumulates a streamed event and uses it to construct a [Response]. When all events have been
69-
* accumulated, the response can be retrieved by calling [response].
109+
* accumulated, the response can be retrieved by calling [response]. The last event is detected
110+
* if one of `ResponseCompletedEvent`, `ResponseIncompleteEvent`, or `ResponseFailedEvent` is
111+
* accumulated. After that event, no more events are expected.
70112
*
71113
* @return The given [event] for convenience, such as when chaining method calls.
72114
* @throws IllegalStateException If [accumulate] is called again after the last event has been
@@ -78,64 +120,93 @@ class ResponseAccumulator private constructor() {
78120
event.accept(
79121
object : ResponseStreamEvent.Visitor<Unit> {
80122
// --------------------------------------------------------------------------------
81-
// The following events _all_ have a `Response` property.
123+
// The following events _all_ have a `response` property.
82124

83125
override fun visitCreated(created: ResponseCreatedEvent) {
84-
// TODO: Taking not action here on the assumption that there is no need to store
85-
// the initial `Response` (devoid of any content), as it will be replaced
86-
// later by one of the "terminal" events. OTOH, this could be useful if the
87-
// events stop suddenly before any further response details can be recorded.
126+
// The initial response (on creation) has no content, so it is not stored.
127+
}
128+
129+
override fun visitCompleted(completed: ResponseCompletedEvent) {
130+
response = completed.response()
88131
}
89132

90133
override fun visitInProgress(inProgress: ResponseInProgressEvent) {
91-
// TODO: Taking no action here on the assumption that this is just some sort of
92-
// "keep-alive" event that carries no new data that needs to be accumulated.
93-
// OTOH, if the events stop suddenly, this could be used as a "partial"
94-
// response, or an ongoing "story so far".
134+
// An in-progress response is not complete, so it is not stored.
95135
}
96136

97-
override fun visitCompleted(completed: ResponseCompletedEvent) {
98-
response = completed.response()
137+
override fun visitQueued(queued: ResponseQueuedEvent) {
138+
// A queued response that is awaiting processing is not complete, so it is not
139+
// stored.
99140
}
100141

101142
override fun visitFailed(failed: ResponseFailedEvent) {
102143
// TODO: Confirm that this is a "terminal" event and will occur _instead of_
103-
// `ResponseCompletedEvent`.
144+
// `ResponseCompletedEvent` or `ResponseIncompleteEvent`.
145+
// Store the response so the reason for the failure can be interrogated.
104146
response = failed.response()
105147
}
106148

107149
override fun visitIncomplete(incomplete: ResponseIncompleteEvent) {
108150
// TODO: Confirm that this is a "terminal" event and will occur _instead of_
109-
// `ResponseCompletedEvent`.
151+
// `ResponseCompletedEvent` or `ResponseFailedEvent`.
152+
// Store the response so the reason for the incompleteness can be interrogated.
110153
response = incomplete.response()
111154
}
112155

113156
// --------------------------------------------------------------------------------
114157
// The following events do _not_ have a `Response` property.
115158

116-
override fun visitError(error: ResponseErrorEvent) {}
159+
override fun visitAudioDelta(audioDelta: ResponseAudioDeltaEvent) {}
117160

118-
override fun visitOutputItemAdded(outputItemAdded: ResponseOutputItemAddedEvent) {}
161+
override fun visitAudioDone(audioDone: ResponseAudioDoneEvent) {}
119162

120-
override fun visitOutputItemDone(outputItemDone: ResponseOutputItemDoneEvent) {}
163+
override fun visitAudioTranscriptDelta(
164+
audioTranscriptDelta: ResponseAudioTranscriptDeltaEvent
165+
) {}
166+
167+
override fun visitAudioTranscriptDone(
168+
audioTranscriptDone: ResponseAudioTranscriptDoneEvent
169+
) {}
170+
171+
override fun visitCodeInterpreterCallCodeDelta(
172+
codeInterpreterCallCodeDelta: ResponseCodeInterpreterCallCodeDeltaEvent
173+
) {}
174+
175+
override fun visitCodeInterpreterCallCodeDone(
176+
codeInterpreterCallCodeDone: ResponseCodeInterpreterCallCodeDoneEvent
177+
) {}
178+
179+
override fun visitCodeInterpreterCallCompleted(
180+
codeInterpreterCallCompleted: ResponseCodeInterpreterCallCompletedEvent
181+
) {}
182+
183+
override fun visitCodeInterpreterCallInProgress(
184+
codeInterpreterCallInProgress: ResponseCodeInterpreterCallInProgressEvent
185+
) {}
186+
187+
override fun visitCodeInterpreterCallInterpreting(
188+
codeInterpreterCallInterpreting: ResponseCodeInterpreterCallInterpretingEvent
189+
) {}
121190

122191
override fun visitContentPartAdded(
123192
contentPartAdded: ResponseContentPartAddedEvent
124193
) {}
125194

126195
override fun visitContentPartDone(contentPartDone: ResponseContentPartDoneEvent) {}
127196

128-
override fun visitOutputTextDelta(outputTextDelta: ResponseTextDeltaEvent) {}
197+
override fun visitError(error: ResponseErrorEvent) {}
129198

130-
override fun visitOutputTextAnnotationAdded(
131-
outputTextAnnotationAdded: ResponseTextAnnotationDeltaEvent
199+
override fun visitFileSearchCallCompleted(
200+
fileSearchCallCompleted: ResponseFileSearchCallCompletedEvent
132201
) {}
133202

134-
override fun visitOutputTextDone(outputTextDone: ResponseTextDoneEvent) {}
135-
136-
override fun visitRefusalDelta(refusalDelta: ResponseRefusalDeltaEvent) {}
203+
override fun visitFileSearchCallInProgress(
204+
fileSearchCallInProgress: ResponseFileSearchCallInProgressEvent
205+
) {}
137206

138-
override fun visitRefusalDone(refusalDone: ResponseRefusalDoneEvent) {}
207+
override fun visitFileSearchCallSearching(
208+
fileSearchCallSearching: ResponseFileSearchCallSearchingEvent
209+
) {}
139210

140211
override fun visitFunctionCallArgumentsDelta(
141212
functionCallArgumentsDelta: ResponseFunctionCallArgumentsDeltaEvent
@@ -145,16 +216,36 @@ class ResponseAccumulator private constructor() {
145216
functionCallArgumentsDone: ResponseFunctionCallArgumentsDoneEvent
146217
) {}
147218

148-
override fun visitFileSearchCallInProgress(
149-
fileSearchCallInProgress: ResponseFileSearchCallInProgressEvent
219+
override fun visitOutputItemAdded(outputItemAdded: ResponseOutputItemAddedEvent) {}
220+
221+
override fun visitOutputItemDone(outputItemDone: ResponseOutputItemDoneEvent) {}
222+
223+
override fun visitReasoningSummaryPartAdded(
224+
reasoningSummaryPartAdded: ResponseReasoningSummaryPartAddedEvent
150225
) {}
151226

152-
override fun visitFileSearchCallSearching(
153-
fileSearchCallSearching: ResponseFileSearchCallSearchingEvent
227+
override fun visitReasoningSummaryPartDone(
228+
reasoningSummaryPartDone: ResponseReasoningSummaryPartDoneEvent
154229
) {}
155230

156-
override fun visitFileSearchCallCompleted(
157-
fileSearchCallCompleted: ResponseFileSearchCallCompletedEvent
231+
override fun visitReasoningSummaryTextDelta(
232+
reasoningSummaryTextDelta: ResponseReasoningSummaryTextDeltaEvent
233+
) {}
234+
235+
override fun visitReasoningSummaryTextDone(
236+
reasoningSummaryTextDone: ResponseReasoningSummaryTextDoneEvent
237+
) {}
238+
239+
override fun visitRefusalDelta(refusalDelta: ResponseRefusalDeltaEvent) {}
240+
241+
override fun visitRefusalDone(refusalDone: ResponseRefusalDoneEvent) {}
242+
243+
override fun visitOutputTextDelta(outputTextDelta: ResponseTextDeltaEvent) {}
244+
245+
override fun visitOutputTextDone(outputTextDone: ResponseTextDoneEvent) {}
246+
247+
override fun visitWebSearchCallCompleted(
248+
webSearchCallCompleted: ResponseWebSearchCallCompletedEvent
158249
) {}
159250

160251
override fun visitWebSearchCallInProgress(
@@ -165,40 +256,66 @@ class ResponseAccumulator private constructor() {
165256
webSearchCallSearching: ResponseWebSearchCallSearchingEvent
166257
) {}
167258

168-
override fun visitWebSearchCallCompleted(
169-
webSearchCallCompleted: ResponseWebSearchCallCompletedEvent
259+
override fun visitImageGenerationCallCompleted(
260+
imageGenerationCallCompleted: ResponseImageGenCallCompletedEvent
170261
) {}
171262

172-
override fun visitAudioDelta(audioDelta: ResponseAudioDeltaEvent) {}
263+
override fun visitImageGenerationCallGenerating(
264+
imageGenerationCallGenerating: ResponseImageGenCallGeneratingEvent
265+
) {}
173266

174-
override fun visitAudioDone(audioDone: ResponseAudioDoneEvent) {}
267+
override fun visitImageGenerationCallInProgress(
268+
imageGenerationCallInProgress: ResponseImageGenCallInProgressEvent
269+
) {}
175270

176-
override fun visitAudioTranscriptDelta(
177-
audioTranscriptDelta: ResponseAudioTranscriptDeltaEvent
271+
override fun visitImageGenerationCallPartialImage(
272+
imageGenerationCallPartialImage: ResponseImageGenCallPartialImageEvent
178273
) {}
179274

180-
override fun visitAudioTranscriptDone(
181-
audioTranscriptDone: ResponseAudioTranscriptDoneEvent
275+
override fun visitMcpCallArgumentsDelta(
276+
mcpCallArgumentsDelta: ResponseMcpCallArgumentsDeltaEvent
182277
) {}
183278

184-
override fun visitCodeInterpreterCallCodeDelta(
185-
codeInterpreterCallCodeDelta: ResponseCodeInterpreterCallCodeDeltaEvent
279+
override fun visitMcpCallArgumentsDone(
280+
mcpCallArgumentsDone: ResponseMcpCallArgumentsDoneEvent
186281
) {}
187282

188-
override fun visitCodeInterpreterCallCodeDone(
189-
codeInterpreterCallCodeDone: ResponseCodeInterpreterCallCodeDoneEvent
283+
override fun visitMcpCallCompleted(
284+
mcpCallCompleted: ResponseMcpCallCompletedEvent
190285
) {}
191286

192-
override fun visitCodeInterpreterCallInProgress(
193-
codeInterpreterCallInProgress: ResponseCodeInterpreterCallInProgressEvent
287+
override fun visitMcpCallFailed(mcpCallFailed: ResponseMcpCallFailedEvent) {}
288+
289+
override fun visitMcpCallInProgress(
290+
mcpCallInProgress: ResponseMcpCallInProgressEvent
194291
) {}
195292

196-
override fun visitCodeInterpreterCallInterpreting(
197-
codeInterpreterCallInterpreting: ResponseCodeInterpreterCallInterpretingEvent
293+
override fun visitMcpListToolsCompleted(
294+
mcpListToolsCompleted: ResponseMcpListToolsCompletedEvent
198295
) {}
199296

200-
override fun visitCodeInterpreterCallCompleted(
201-
codeInterpreterCallCompleted: ResponseCodeInterpreterCallCompletedEvent
297+
override fun visitMcpListToolsFailed(
298+
mcpListToolsFailed: ResponseMcpListToolsFailedEvent
299+
) {}
300+
301+
override fun visitMcpListToolsInProgress(
302+
mcpListToolsInProgress: ResponseMcpListToolsInProgressEvent
303+
) {}
304+
305+
override fun visitOutputTextAnnotationAdded(
306+
outputTextAnnotationAdded: ResponseOutputTextAnnotationAddedEvent
307+
) {}
308+
309+
override fun visitReasoningDelta(reasoningDelta: ResponseReasoningDeltaEvent) {}
310+
311+
override fun visitReasoningDone(reasoningDone: ResponseReasoningDoneEvent) {}
312+
313+
override fun visitReasoningSummaryDelta(
314+
reasoningSummaryDelta: ResponseReasoningSummaryDeltaEvent
315+
) {}
316+
317+
override fun visitReasoningSummaryDone(
318+
reasoningSummaryDone: ResponseReasoningSummaryDoneEvent
202319
) {}
203320
}
204321
)

0 commit comments

Comments
 (0)