meta-sam-parser 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,618 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import math
7
+ from collections import deque
8
+ from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Mapping
9
+ from contextlib import suppress
10
+ from inspect import isawaitable
11
+ from types import TracebackType
12
+ from typing import Generic, Literal, TypeVar, cast
13
+
14
+ from ._errors import (
15
+ ResponsesStreamAbortedError,
16
+ ResponsesStreamConsumedError,
17
+ ResponsesStreamError,
18
+ ResponsesStreamEventError,
19
+ ResponsesStreamFailedError,
20
+ ResponsesStreamLaneError,
21
+ ResponsesStreamParserError,
22
+ ResponsesStreamRefusalError,
23
+ ResponsesStreamSourceError,
24
+ )
25
+ from ._types import (
26
+ CompletedOutcome,
27
+ IncompleteOutcome,
28
+ OutputTextLane,
29
+ ParserFinish,
30
+ ResponseFormat,
31
+ ResponseFormatParser,
32
+ ResponsesEventLike,
33
+ ResponseStreamOutcome,
34
+ )
35
+
36
+ EventT = TypeVar("EventT")
37
+ ResultT = TypeVar("ResultT")
38
+ ObservedT = TypeVar("ObservedT")
39
+ _Consumption = Literal["idle", "iteration", "final"]
40
+ _MAXIMUM_SAFE_INTEGER = (1 << 53) - 1
41
+ _MISSING = object()
42
+
43
+
44
+ def _consume_future_exception(future: asyncio.Future[ObservedT]) -> None:
45
+ if future.cancelled():
46
+ return
47
+ with suppress(BaseException):
48
+ future.exception()
49
+
50
+
51
+ def _field(value: object, key: str) -> object:
52
+ if isinstance(value, Mapping):
53
+ return value.get(key, _MISSING)
54
+ try:
55
+ return getattr(value, key)
56
+ except AttributeError:
57
+ return _MISSING
58
+
59
+
60
+ def _nested_string(value: object, *path: str) -> str | None:
61
+ current = value
62
+ for key in path:
63
+ current = _field(current, key)
64
+ if current is _MISSING:
65
+ return None
66
+ return current if isinstance(current, str) else None
67
+
68
+
69
+ def _safe_nonnegative_integer(value: object) -> int | None:
70
+ if isinstance(value, bool):
71
+ return None
72
+ if isinstance(value, int):
73
+ integer = value
74
+ elif isinstance(value, float) and math.isfinite(value) and value.is_integer():
75
+ integer = int(value)
76
+ else:
77
+ return None
78
+ return integer if 0 <= integer <= _MAXIMUM_SAFE_INTEGER else None
79
+
80
+
81
+ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
82
+ """One-owner async adapter from Responses API events to parsed snapshots."""
83
+
84
+ def __init__(
85
+ self,
86
+ source: AsyncIterable[ResponsesEventLike],
87
+ format_: ResponseFormat[EventT, ResultT],
88
+ ) -> None:
89
+ self._source = source
90
+ self._format = format_
91
+ self._loop: asyncio.AbstractEventLoop | None = None
92
+ self._parser: ResponseFormatParser[EventT, ResultT] | None = None
93
+ self._source_iterator: AsyncIterator[ResponsesEventLike] | None = None
94
+ self._consumption: _Consumption = "idle"
95
+ self._pull_lock = asyncio.Lock()
96
+ self._pending: deque[EventT] = deque()
97
+ self._drain_task: asyncio.Task[None] | None = None
98
+ self._close_task: asyncio.Task[None] | None = None
99
+ self._failure_task: asyncio.Task[None] | None = None
100
+ self._result_future: asyncio.Future[ResultT] | None = None
101
+ self._read_interruption: asyncio.Future[BaseException] | None = None
102
+ self._active_read: asyncio.Future[ResponsesEventLike] | None = None
103
+ self._terminal = False
104
+ self._consumer_done = False
105
+ self._result_value: ResultT | None = None
106
+ self._has_result = False
107
+ self._error: BaseException | None = None
108
+ self._lane: OutputTextLane | None = None
109
+ self._lane_done = False
110
+ self._text = ""
111
+ self._saw_delta = False
112
+
113
+ def __aiter__(self) -> ParsedResponsesStream[EventT, ResultT]:
114
+ if self._consumption != "idle":
115
+ raise ResponsesStreamConsumedError()
116
+ self._consumption = "iteration"
117
+ return self
118
+
119
+ async def __anext__(self) -> EventT:
120
+ if self._consumption != "iteration":
121
+ raise ResponsesStreamConsumedError()
122
+ try:
123
+ async with self._pull_lock:
124
+ return await self._next_one()
125
+ except asyncio.CancelledError:
126
+ await self._abort_after_cancellation()
127
+ raise
128
+
129
+ async def __aenter__(self) -> ParsedResponsesStream[EventT, ResultT]:
130
+ self._ensure_loop()
131
+ return self
132
+
133
+ async def __aexit__(
134
+ self,
135
+ exc_type: type[BaseException] | None,
136
+ exc_value: BaseException | None,
137
+ traceback: TracebackType | None,
138
+ ) -> Literal[False]:
139
+ await self.aclose()
140
+ return False
141
+
142
+ def final_result(self) -> Awaitable[ResultT]:
143
+ """Claim final-only mode immediately and await the shared terminal result."""
144
+
145
+ self._ensure_loop()
146
+ result = self._ensure_result_future()
147
+ if self._consumption == "idle":
148
+ self._consumption = "final"
149
+ self._drain_task = self._ensure_loop().create_task(self._drain_final())
150
+ self._observe(self._drain_task)
151
+ return self._await_result(result)
152
+
153
+ async def _await_result(self, result: asyncio.Future[ResultT]) -> ResultT:
154
+ try:
155
+ return await asyncio.shield(result)
156
+ except asyncio.CancelledError:
157
+ await self._abort_after_cancellation()
158
+ raise
159
+
160
+ async def aclose(self) -> None:
161
+ """Best-effort close; abandoning a live stream rejects its final result."""
162
+
163
+ self._ensure_loop()
164
+ current = asyncio.current_task()
165
+ if current is self._close_task or current is self._failure_task:
166
+ return
167
+ if self._consumer_done:
168
+ return
169
+ if self._consumption == "idle":
170
+ self._consumption = "final"
171
+ failure = self._start_failure(ResponsesStreamAbortedError())
172
+ try:
173
+ await asyncio.shield(failure)
174
+ except asyncio.CancelledError:
175
+ raise
176
+ self._consumer_done = True
177
+ if (
178
+ isinstance(self._error, ResponsesStreamSourceError)
179
+ and self._error.operation == "return"
180
+ ):
181
+ raise self._error
182
+
183
+ async def _next_one(self) -> EventT:
184
+ if self._pending:
185
+ return self._pending.popleft()
186
+ if self._error is not None:
187
+ raise self._error
188
+ if self._terminal:
189
+ self._complete_iteration()
190
+
191
+ try:
192
+ self._ensure_source_iterator()
193
+ self._ensure_parser()
194
+ while True:
195
+ done, event = await self._read_source()
196
+ if self._terminal:
197
+ if self._error is not None:
198
+ raise self._error
199
+ self._complete_iteration()
200
+ if done:
201
+ self._finish(IncompleteOutcome(reason="eof"), emit=True)
202
+ await self._close_source()
203
+ if self._pending:
204
+ return self._pending.popleft()
205
+ self._complete_iteration()
206
+ assert event is not None
207
+ outcome = self._accept(event, emit=True)
208
+ if outcome is not None:
209
+ self._finish(outcome, emit=True)
210
+ await self._close_source()
211
+ if self._pending:
212
+ return self._pending.popleft()
213
+ self._complete_iteration()
214
+ if self._pending:
215
+ return self._pending.popleft()
216
+ except StopAsyncIteration:
217
+ raise
218
+ except asyncio.CancelledError:
219
+ raise
220
+ except Exception as error:
221
+ await asyncio.shield(self._start_failure(error))
222
+ assert self._error is not None
223
+ raise self._error from self._error.__cause__
224
+ raise AssertionError("unreachable")
225
+
226
+ async def _drain_final(self) -> None:
227
+ try:
228
+ self._ensure_source_iterator()
229
+ self._ensure_parser()
230
+ while True:
231
+ done, event = await self._read_source()
232
+ if done:
233
+ self._finish(IncompleteOutcome(reason="eof"), emit=False)
234
+ await self._close_source()
235
+ self._consumer_done = True
236
+ self._resolve_result()
237
+ return
238
+ assert event is not None
239
+ outcome = self._accept(event, emit=False)
240
+ if outcome is not None:
241
+ self._finish(outcome, emit=False)
242
+ await self._close_source()
243
+ self._consumer_done = True
244
+ self._resolve_result()
245
+ return
246
+ except asyncio.CancelledError:
247
+ await self._abort_after_cancellation()
248
+ raise
249
+ except Exception as error:
250
+ await asyncio.shield(self._start_failure(error))
251
+
252
+ def _accept(
253
+ self, event: ResponsesEventLike, *, emit: bool
254
+ ) -> ResponseStreamOutcome | None:
255
+ event_type = _field(event, "type")
256
+ assert isinstance(event_type, str)
257
+ if event_type == "response.output_text.delta":
258
+ lane = self._accept_lane(event)
259
+ if self._lane_done:
260
+ raise ResponsesStreamLaneError(
261
+ "The response emitted output text after finalizing its lane.",
262
+ self._lane,
263
+ lane,
264
+ )
265
+ delta = _field(event, "delta")
266
+ if not isinstance(delta, str):
267
+ raise ResponsesStreamLaneError(
268
+ "The output text delta is missing its text.",
269
+ self._lane,
270
+ lane,
271
+ )
272
+ self._saw_delta = True
273
+ self._text += delta
274
+ self._push_parser(delta, emit=emit)
275
+ return None
276
+ if event_type == "response.output_text.done":
277
+ lane = self._accept_lane(event)
278
+ if self._lane_done:
279
+ raise ResponsesStreamLaneError(
280
+ "The response finalized its output text lane more than once.",
281
+ self._lane,
282
+ lane,
283
+ )
284
+ text = _field(event, "text")
285
+ if not isinstance(text, str):
286
+ raise ResponsesStreamLaneError(
287
+ "The completed output text is missing its text.",
288
+ self._lane,
289
+ lane,
290
+ )
291
+ if self._saw_delta and text != self._text:
292
+ raise ResponsesStreamLaneError(
293
+ "The finalized output text conflicts with its accumulated deltas.",
294
+ self._lane,
295
+ lane,
296
+ )
297
+ if not self._saw_delta:
298
+ self._text = text
299
+ self._push_parser(text, emit=emit)
300
+ self._lane_done = True
301
+ return None
302
+ if event_type in ("response.refusal.delta", "response.refusal.done"):
303
+ refusal = _field(event, "refusal")
304
+ delta = _field(event, "delta")
305
+ message = (
306
+ refusal
307
+ if isinstance(refusal, str)
308
+ else delta
309
+ if isinstance(delta, str)
310
+ else "The response was refused."
311
+ )
312
+ raise ResponsesStreamRefusalError(message, event)
313
+ if event_type == "response.completed":
314
+ if self._lane is None or not self._lane_done:
315
+ raise ResponsesStreamLaneError(
316
+ "The response completed before finalizing one output text lane.",
317
+ self._lane,
318
+ None,
319
+ )
320
+ return CompletedOutcome()
321
+ if event_type == "response.incomplete":
322
+ if self._lane is None or not self._lane_done:
323
+ raise ResponsesStreamLaneError(
324
+ "The response became incomplete before finalizing one "
325
+ "output text lane.",
326
+ self._lane,
327
+ None,
328
+ )
329
+ detail = _nested_string(event, "response", "incomplete_details", "reason")
330
+ return IncompleteOutcome(reason="response", detail=detail)
331
+ if event_type == "response.failed":
332
+ failed_message = _nested_string(event, "response", "error", "message")
333
+ if failed_message is None:
334
+ failed_message = "The response failed."
335
+ raise ResponsesStreamFailedError(failed_message, event)
336
+ if event_type == "error":
337
+ event_message = _nested_string(event, "error", "message")
338
+ if event_message is None:
339
+ event_message = _nested_string(event, "message")
340
+ if event_message is None:
341
+ event_message = "The Responses stream reported an error."
342
+ raise ResponsesStreamEventError(event_message, event)
343
+ return None
344
+
345
+ def _accept_lane(self, event: ResponsesEventLike) -> OutputTextLane:
346
+ item_id = _field(event, "item_id")
347
+ output_index = _safe_nonnegative_integer(_field(event, "output_index"))
348
+ content_index = _safe_nonnegative_integer(_field(event, "content_index"))
349
+ lane = (
350
+ OutputTextLane(item_id, output_index, content_index)
351
+ if isinstance(item_id, str)
352
+ and item_id
353
+ and output_index is not None
354
+ and content_index is not None
355
+ else None
356
+ )
357
+ if lane is None:
358
+ raise ResponsesStreamLaneError(
359
+ "The output text event has an invalid lane identity.",
360
+ self._lane,
361
+ None,
362
+ )
363
+ if self._lane is None:
364
+ self._lane = lane
365
+ return lane
366
+ if self._lane != lane:
367
+ raise ResponsesStreamLaneError(
368
+ "The response interleaved multiple output text lanes.",
369
+ self._lane,
370
+ lane,
371
+ )
372
+ return lane
373
+
374
+ def _push_parser(self, chunk: str, *, emit: bool) -> None:
375
+ try:
376
+ events = self._ensure_parser().push(chunk, emit=emit)
377
+ if emit:
378
+ self._pending.extend(events)
379
+ except asyncio.CancelledError:
380
+ raise
381
+ except ResponsesStreamError:
382
+ raise
383
+ except Exception as error:
384
+ raise ResponsesStreamParserError(error) from error
385
+
386
+ def _finish(self, outcome: ResponseStreamOutcome, *, emit: bool) -> None:
387
+ if self._terminal:
388
+ return
389
+ try:
390
+ finished = self._ensure_parser().finish(outcome)
391
+ if not isinstance(finished, ParserFinish):
392
+ raise TypeError("Parser finish() must return ParserFinish.")
393
+ if emit:
394
+ self._pending.extend(finished.events)
395
+ self._result_value = finished.result
396
+ except asyncio.CancelledError:
397
+ raise
398
+ except ResponsesStreamError:
399
+ raise
400
+ except Exception as error:
401
+ raise ResponsesStreamParserError(error) from error
402
+ self._has_result = True
403
+ self._terminal = True
404
+
405
+ def _ensure_parser(self) -> ResponseFormatParser[EventT, ResultT]:
406
+ if self._parser is not None:
407
+ return self._parser
408
+ try:
409
+ self._parser = self._format.create_parser()
410
+ except asyncio.CancelledError:
411
+ raise
412
+ except ResponsesStreamError:
413
+ raise
414
+ except Exception as error:
415
+ raise ResponsesStreamParserError(error) from error
416
+ return self._parser
417
+
418
+ def _complete_iteration(self) -> None:
419
+ if self._error is not None:
420
+ raise self._error
421
+ self._consumer_done = True
422
+ self._resolve_result()
423
+ raise StopAsyncIteration
424
+
425
+ def _resolve_result(self) -> None:
426
+ if not self._has_result or self._error is not None:
427
+ return
428
+ future = self._result_future
429
+ if future is not None and not future.done():
430
+ future.set_result(cast(ResultT, self._result_value))
431
+
432
+ def _start_failure(self, error: BaseException) -> asyncio.Task[None]:
433
+ if self._failure_task is not None:
434
+ return self._failure_task
435
+ failure = (
436
+ error
437
+ if isinstance(error, ResponsesStreamError)
438
+ else ResponsesStreamParserError(error)
439
+ )
440
+ self._failure_task = self._ensure_loop().create_task(
441
+ self._fail_and_close(failure)
442
+ )
443
+ self._observe(self._failure_task)
444
+ return self._failure_task
445
+
446
+ async def _fail_and_close(self, failure: ResponsesStreamError) -> None:
447
+ self._interrupt_reads(failure)
448
+ active = self._active_read
449
+ if active is not None and not active.done():
450
+ active.cancel()
451
+ with suppress(BaseException):
452
+ await active
453
+ if (
454
+ isinstance(failure, ResponsesStreamSourceError)
455
+ and failure.operation == "return"
456
+ and self._close_task is not None
457
+ ):
458
+ self._fail(failure)
459
+ return
460
+ try:
461
+ await self._close_source()
462
+ except ResponsesStreamSourceError as close_error:
463
+ cause = close_error.cause
464
+ assert cause is not None
465
+ failure = ResponsesStreamSourceError("return", cause, prior_error=failure)
466
+ self._fail(failure)
467
+
468
+ def _interrupt_reads(self, error: BaseException) -> None:
469
+ future = self._ensure_read_interruption()
470
+ if not future.done():
471
+ future.set_result(error)
472
+
473
+ def _fail(self, error: BaseException) -> None:
474
+ if self._error is not None:
475
+ return
476
+ self._error = error
477
+ self._terminal = True
478
+ self._pending.clear()
479
+ future = self._result_future
480
+ if future is not None and not future.done():
481
+ future.set_exception(error)
482
+
483
+ async def _abort_after_cancellation(self) -> None:
484
+ if self._consumer_done:
485
+ return
486
+ failure = self._start_failure(ResponsesStreamAbortedError())
487
+ with suppress(asyncio.CancelledError):
488
+ await asyncio.shield(failure)
489
+ self._consumer_done = True
490
+
491
+ def _ensure_source_iterator(self) -> AsyncIterator[ResponsesEventLike]:
492
+ if self._source_iterator is not None:
493
+ return self._source_iterator
494
+ try:
495
+ self._source_iterator = aiter(self._source)
496
+ except asyncio.CancelledError:
497
+ raise
498
+ except Exception as error:
499
+ raise ResponsesStreamSourceError("iterator", error) from error
500
+ return self._source_iterator
501
+
502
+ async def _read_source(self) -> tuple[bool, ResponsesEventLike | None]:
503
+ iterator = self._ensure_source_iterator()
504
+ try:
505
+ awaitable = iterator.__anext__()
506
+ reading = asyncio.ensure_future(awaitable)
507
+ except StopAsyncIteration:
508
+ return True, None
509
+ except asyncio.CancelledError:
510
+ raise
511
+ except Exception as error:
512
+ raise ResponsesStreamSourceError("next", error) from error
513
+ self._active_read = reading
514
+ self._observe(reading)
515
+ interrupted = self._ensure_read_interruption()
516
+ try:
517
+ completed, _pending = await asyncio.wait(
518
+ (reading, interrupted), return_when=asyncio.FIRST_COMPLETED
519
+ )
520
+ if interrupted in completed:
521
+ interrupted_error = interrupted.result()
522
+ reading.cancel()
523
+ raise interrupted_error
524
+ try:
525
+ event = reading.result()
526
+ except StopAsyncIteration:
527
+ return True, None
528
+ except asyncio.CancelledError:
529
+ raise
530
+ except Exception as error:
531
+ raise ResponsesStreamSourceError("next", error) from error
532
+ try:
533
+ event_type = _field(event, "type")
534
+ except Exception as error:
535
+ raise ResponsesStreamSourceError("next", error) from error
536
+ if not isinstance(event_type, str):
537
+ shape_error = TypeError(
538
+ "The event source yielded an invalid Responses event."
539
+ )
540
+ raise ResponsesStreamSourceError("next", shape_error) from shape_error
541
+ return False, event
542
+ except asyncio.CancelledError:
543
+ reading.cancel()
544
+ with suppress(BaseException):
545
+ await reading
546
+ raise
547
+ finally:
548
+ if self._active_read is reading:
549
+ self._active_read = None
550
+
551
+ async def _close_source(self) -> None:
552
+ current = asyncio.current_task()
553
+ if current is self._close_task:
554
+ return
555
+ if self._close_task is None:
556
+ self._close_task = self._ensure_loop().create_task(self._run_close_source())
557
+ self._observe(self._close_task)
558
+ await asyncio.shield(self._close_task)
559
+
560
+ async def _run_close_source(self) -> None:
561
+ iterator = self._source_iterator
562
+ target: object = self._source
563
+ try:
564
+ close = getattr(target, "aclose", None)
565
+ if close is None:
566
+ close = getattr(target, "close", None)
567
+ if close is None and iterator is not None and iterator is not target:
568
+ target = iterator
569
+ close = getattr(target, "aclose", None)
570
+ if close is None:
571
+ close = getattr(target, "close", None)
572
+ except BaseException as error:
573
+ raise ResponsesStreamSourceError("return", error) from error
574
+ if close is None:
575
+ return
576
+ try:
577
+ outcome = close()
578
+ if isawaitable(outcome):
579
+ await outcome
580
+ except BaseException as error:
581
+ raise ResponsesStreamSourceError("return", error) from error
582
+
583
+ def _ensure_loop(self) -> asyncio.AbstractEventLoop:
584
+ loop = asyncio.get_running_loop()
585
+ if self._loop is None:
586
+ self._loop = loop
587
+ elif self._loop is not loop:
588
+ raise RuntimeError("A parsed response stream cannot cross event loops.")
589
+ return loop
590
+
591
+ def _ensure_result_future(self) -> asyncio.Future[ResultT]:
592
+ if self._result_future is None:
593
+ future = self._ensure_loop().create_future()
594
+ self._result_future = future
595
+ self._observe(future)
596
+ if self._error is not None:
597
+ future.set_exception(self._error)
598
+ elif self._has_result and self._consumer_done:
599
+ future.set_result(cast(ResultT, self._result_value))
600
+ return self._result_future
601
+
602
+ def _ensure_read_interruption(self) -> asyncio.Future[BaseException]:
603
+ if self._read_interruption is None:
604
+ self._read_interruption = self._ensure_loop().create_future()
605
+ return self._read_interruption
606
+
607
+ @staticmethod
608
+ def _observe(future: asyncio.Future[ObservedT]) -> None:
609
+ future.add_done_callback(_consume_future_exception)
610
+
611
+
612
+ def parse_responses_stream(
613
+ source: AsyncIterable[ResponsesEventLike],
614
+ format: ResponseFormat[EventT, ResultT],
615
+ ) -> ParsedResponsesStream[EventT, ResultT]:
616
+ """Create a lazy, single-consumer parser for a Responses event stream."""
617
+
618
+ return ParsedResponsesStream(source, format)