ai-sdk-stream-python 0.2.0a2__tar.gz → 0.2.0a3__tar.gz
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.
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/PKG-INFO +1 -1
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/pyproject.toml +1 -1
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/__init__.py +15 -0
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/collect.py +41 -1
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/context.py +134 -4
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/events.py +48 -0
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/README.md +0 -0
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/py.typed +0 -0
- {ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/state.py +0 -0
{ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/__init__.py
RENAMED
|
@@ -42,12 +42,18 @@ Quickstart::
|
|
|
42
42
|
)
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
+
from .collect import DataPartRecord as DataPartRecord
|
|
46
|
+
from .collect import FileRecord as FileRecord
|
|
45
47
|
from .collect import SourceRecord as SourceRecord
|
|
46
48
|
from .collect import StreamRecord as StreamRecord
|
|
47
49
|
from .collect import ToolCallRecord as ToolCallRecord
|
|
48
50
|
from .context import StreamContext, ToolCallHandle
|
|
49
51
|
from .events import (
|
|
52
|
+
AbortEvent,
|
|
50
53
|
BaseEvent,
|
|
54
|
+
DataEvent,
|
|
55
|
+
ErrorEvent,
|
|
56
|
+
FileEvent,
|
|
51
57
|
FinishEvent,
|
|
52
58
|
FinishStepEvent,
|
|
53
59
|
ReasoningDeltaEvent,
|
|
@@ -77,6 +83,8 @@ __all__ = [
|
|
|
77
83
|
"StreamRecord",
|
|
78
84
|
"ToolCallRecord",
|
|
79
85
|
"SourceRecord",
|
|
86
|
+
"FileRecord",
|
|
87
|
+
"DataPartRecord",
|
|
80
88
|
# Base / union
|
|
81
89
|
"BaseEvent",
|
|
82
90
|
"UIMessageStreamEvent",
|
|
@@ -101,4 +109,11 @@ __all__ = [
|
|
|
101
109
|
"ToolOutputErrorEvent",
|
|
102
110
|
# Sources
|
|
103
111
|
"SourceUrlEvent",
|
|
112
|
+
# Files
|
|
113
|
+
"FileEvent",
|
|
114
|
+
# Custom data parts
|
|
115
|
+
"DataEvent",
|
|
116
|
+
# Error / Abort
|
|
117
|
+
"ErrorEvent",
|
|
118
|
+
"AbortEvent",
|
|
104
119
|
]
|
{ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/collect.py
RENAMED
|
@@ -32,6 +32,23 @@ class SourceRecord:
|
|
|
32
32
|
title: str | None = None
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
@dataclass
|
|
36
|
+
class FileRecord:
|
|
37
|
+
"""A file/image emitted via ``write_file``."""
|
|
38
|
+
|
|
39
|
+
url: str
|
|
40
|
+
media_type: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class DataPartRecord:
|
|
45
|
+
"""A non-transient custom data part emitted via ``write_data``."""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
data: dict[str, Any]
|
|
49
|
+
id: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
35
52
|
@dataclass
|
|
36
53
|
class StreamRecord:
|
|
37
54
|
"""
|
|
@@ -66,6 +83,8 @@ class StreamRecord:
|
|
|
66
83
|
reasoning: str = ""
|
|
67
84
|
tool_calls: list[ToolCallRecord] = field(default_factory=list)
|
|
68
85
|
sources: list[SourceRecord] = field(default_factory=list)
|
|
86
|
+
files: list[FileRecord] = field(default_factory=list)
|
|
87
|
+
data_parts: list[DataPartRecord] = field(default_factory=list)
|
|
69
88
|
finish_reason: str | None = None
|
|
70
89
|
step_count: int = 0
|
|
71
90
|
|
|
@@ -93,9 +112,30 @@ class StreamRecord:
|
|
|
93
112
|
}
|
|
94
113
|
for s in self.sources
|
|
95
114
|
],
|
|
115
|
+
"files": [
|
|
116
|
+
{
|
|
117
|
+
"url": f.url,
|
|
118
|
+
"media_type": f.media_type,
|
|
119
|
+
}
|
|
120
|
+
for f in self.files
|
|
121
|
+
],
|
|
122
|
+
"data_parts": [
|
|
123
|
+
{
|
|
124
|
+
"name": dp.name,
|
|
125
|
+
"data": dp.data,
|
|
126
|
+
"id": dp.id,
|
|
127
|
+
}
|
|
128
|
+
for dp in self.data_parts
|
|
129
|
+
],
|
|
96
130
|
"finish_reason": self.finish_reason,
|
|
97
131
|
"step_count": self.step_count,
|
|
98
132
|
}
|
|
99
133
|
|
|
100
134
|
|
|
101
|
-
__all__ = [
|
|
135
|
+
__all__ = [
|
|
136
|
+
"DataPartRecord",
|
|
137
|
+
"FileRecord",
|
|
138
|
+
"SourceRecord",
|
|
139
|
+
"StreamRecord",
|
|
140
|
+
"ToolCallRecord",
|
|
141
|
+
]
|
{ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/context.py
RENAMED
|
@@ -60,9 +60,19 @@ from typing import Any, ClassVar, Generic, TypeVar
|
|
|
60
60
|
|
|
61
61
|
from pydantic import BaseModel
|
|
62
62
|
|
|
63
|
-
from .collect import
|
|
63
|
+
from .collect import (
|
|
64
|
+
DataPartRecord,
|
|
65
|
+
FileRecord,
|
|
66
|
+
SourceRecord,
|
|
67
|
+
StreamRecord,
|
|
68
|
+
ToolCallRecord,
|
|
69
|
+
)
|
|
64
70
|
from .events import (
|
|
71
|
+
AbortEvent,
|
|
65
72
|
BaseEvent,
|
|
73
|
+
DataEvent,
|
|
74
|
+
ErrorEvent,
|
|
75
|
+
FileEvent,
|
|
66
76
|
FinishEvent,
|
|
67
77
|
FinishStepEvent,
|
|
68
78
|
ReasoningDeltaEvent,
|
|
@@ -75,6 +85,7 @@ from .events import (
|
|
|
75
85
|
TextEndEvent,
|
|
76
86
|
TextStartEvent,
|
|
77
87
|
ToolInputAvailableEvent,
|
|
88
|
+
ToolInputDeltaEvent,
|
|
78
89
|
ToolInputStartEvent,
|
|
79
90
|
ToolOutputAvailableEvent,
|
|
80
91
|
ToolOutputErrorEvent,
|
|
@@ -307,6 +318,63 @@ class StreamContext(Generic[_InfoT]):
|
|
|
307
318
|
)
|
|
308
319
|
return ToolCallHandle(toolCallId=tcid, toolName=tool_name)
|
|
309
320
|
|
|
321
|
+
async def start_tool_input(
|
|
322
|
+
self,
|
|
323
|
+
tool_name: str,
|
|
324
|
+
*,
|
|
325
|
+
tool_call_id: str | None = None,
|
|
326
|
+
) -> ToolCallHandle:
|
|
327
|
+
"""
|
|
328
|
+
Emit ``tool-input-start`` and return a handle for streaming deltas.
|
|
329
|
+
|
|
330
|
+
Use this when tool arguments arrive incrementally (e.g. from an LLM
|
|
331
|
+
stream). Follow with :meth:`stream_tool_input_delta` calls and
|
|
332
|
+
finish with :meth:`finish_tool_input`.
|
|
333
|
+
"""
|
|
334
|
+
await self._ensure_step_open()
|
|
335
|
+
await self._ensure_text_closed()
|
|
336
|
+
await self._ensure_reasoning_closed()
|
|
337
|
+
tcid = tool_call_id or str(uuid.uuid4())
|
|
338
|
+
self._queue.put_nowait(ToolInputStartEvent(toolCallId=tcid, toolName=tool_name))
|
|
339
|
+
if self._record is not None:
|
|
340
|
+
self._record.tool_calls.append(
|
|
341
|
+
ToolCallRecord(tool_call_id=tcid, tool_name=tool_name, input={})
|
|
342
|
+
)
|
|
343
|
+
return ToolCallHandle(toolCallId=tcid, toolName=tool_name)
|
|
344
|
+
|
|
345
|
+
async def stream_tool_input_delta(
|
|
346
|
+
self, tool_call_id: str, input_text_delta: str
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Emit a ``tool-input-delta`` for an in-progress tool call."""
|
|
349
|
+
self.write_event_to_stream(
|
|
350
|
+
ToolInputDeltaEvent(
|
|
351
|
+
toolCallId=tool_call_id, inputTextDelta=input_text_delta
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
async def finish_tool_input(
|
|
356
|
+
self,
|
|
357
|
+
tool_call_id: str,
|
|
358
|
+
tool_name: str,
|
|
359
|
+
input: dict[str, Any],
|
|
360
|
+
) -> None:
|
|
361
|
+
"""
|
|
362
|
+
Emit ``tool-input-available`` to close a streaming tool call.
|
|
363
|
+
|
|
364
|
+
Updates the collected :class:`~collect.ToolCallRecord` with the
|
|
365
|
+
final *input* if collection is enabled.
|
|
366
|
+
"""
|
|
367
|
+
if self._record is not None:
|
|
368
|
+
for tc in self._record.tool_calls:
|
|
369
|
+
if tc.tool_call_id == tool_call_id:
|
|
370
|
+
tc.input = input
|
|
371
|
+
break
|
|
372
|
+
self.write_event_to_stream(
|
|
373
|
+
ToolInputAvailableEvent(
|
|
374
|
+
toolCallId=tool_call_id, toolName=tool_name, input=input
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
|
|
310
378
|
async def complete_tool_call(self, tool_call_id: str, output: Any) -> None:
|
|
311
379
|
"""Emit ``tool-output-available`` with the tool result."""
|
|
312
380
|
if self._record is not None:
|
|
@@ -329,6 +397,51 @@ class StreamContext(Generic[_InfoT]):
|
|
|
329
397
|
ToolOutputErrorEvent(toolCallId=tool_call_id, error=error)
|
|
330
398
|
)
|
|
331
399
|
|
|
400
|
+
async def write_data(
|
|
401
|
+
self,
|
|
402
|
+
name: str,
|
|
403
|
+
data: dict[str, Any],
|
|
404
|
+
*,
|
|
405
|
+
id: str | None = None,
|
|
406
|
+
transient: bool = False,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""
|
|
409
|
+
Emit a custom data part (``data-{name}`` type).
|
|
410
|
+
|
|
411
|
+
*name* is validated: it must be non-empty and contain only
|
|
412
|
+
alphanumeric characters, hyphens, or underscores.
|
|
413
|
+
Non-transient parts are collected in ``ctx.record.data_parts``.
|
|
414
|
+
Transient parts are only available through the ``onData`` callback
|
|
415
|
+
on the frontend; they are not stored in message history.
|
|
416
|
+
"""
|
|
417
|
+
if not name or not all(c.isalnum() or c in "-_" for c in name):
|
|
418
|
+
raise ValueError(
|
|
419
|
+
f"Invalid data part name {name!r}. "
|
|
420
|
+
"Use only alphanumeric characters, hyphens, or underscores."
|
|
421
|
+
)
|
|
422
|
+
await self._ensure_started()
|
|
423
|
+
event = DataEvent(
|
|
424
|
+
type=f"data-{name}",
|
|
425
|
+
data=data,
|
|
426
|
+
id=id,
|
|
427
|
+
transient=transient or None,
|
|
428
|
+
)
|
|
429
|
+
if self._record is not None and not transient:
|
|
430
|
+
self._record.data_parts.append(DataPartRecord(name=name, data=data, id=id))
|
|
431
|
+
self.write_event_to_stream(event)
|
|
432
|
+
|
|
433
|
+
async def write_file(self, url: str, media_type: str) -> None:
|
|
434
|
+
"""
|
|
435
|
+
Emit a ``file`` event (image, PDF, or other file content).
|
|
436
|
+
|
|
437
|
+
Auto-emits ``start`` and ``start-step`` if not yet open.
|
|
438
|
+
On the frontend this produces a ``FileUIPart`` in ``message.parts``.
|
|
439
|
+
"""
|
|
440
|
+
await self._ensure_step_open()
|
|
441
|
+
if self._record is not None:
|
|
442
|
+
self._record.files.append(FileRecord(url=url, media_type=media_type))
|
|
443
|
+
self.write_event_to_stream(FileEvent(url=url, mediaType=media_type))
|
|
444
|
+
|
|
332
445
|
async def write_source(
|
|
333
446
|
self,
|
|
334
447
|
source_id: str,
|
|
@@ -377,16 +490,33 @@ class StreamContext(Generic[_InfoT]):
|
|
|
377
490
|
)
|
|
378
491
|
self._queue.put_nowait(None) # sentinel → stream() yields [DONE]
|
|
379
492
|
|
|
380
|
-
async def abort(self) -> None:
|
|
493
|
+
async def abort(self, reason: str | None = None) -> None:
|
|
381
494
|
"""
|
|
382
|
-
|
|
495
|
+
Emit an ``abort`` event and terminate the stream.
|
|
383
496
|
|
|
384
497
|
Use in error-handling paths where the normal ``finish()`` flow
|
|
385
|
-
cannot be reached.
|
|
498
|
+
cannot be reached. The optional *reason* string is forwarded to
|
|
499
|
+
the frontend so ``useChat`` can surface why the stream stopped.
|
|
500
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
501
|
+
"""
|
|
502
|
+
if self._finished:
|
|
503
|
+
return
|
|
504
|
+
self._finished = True
|
|
505
|
+
self._queue.put_nowait(AbortEvent(reason=reason))
|
|
506
|
+
self._queue.put_nowait(None)
|
|
507
|
+
|
|
508
|
+
async def error(self, error_text: str) -> None:
|
|
509
|
+
"""
|
|
510
|
+
Emit an ``error`` event and terminate the stream.
|
|
511
|
+
|
|
512
|
+
The AI SDK v6 ``useChat`` hook surfaces this via its ``error`` object.
|
|
513
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
386
514
|
"""
|
|
387
515
|
if self._finished:
|
|
388
516
|
return
|
|
517
|
+
await self._ensure_started()
|
|
389
518
|
self._finished = True
|
|
519
|
+
self._queue.put_nowait(ErrorEvent(errorText=error_text))
|
|
390
520
|
self._queue.put_nowait(None)
|
|
391
521
|
|
|
392
522
|
# ── SSE async generator ───────────────────────────────────────────────────
|
{ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/events.py
RENAMED
|
@@ -129,6 +129,47 @@ class SourceUrlEvent(BaseEvent):
|
|
|
129
129
|
title: str | None = None
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
# ── Files ──────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class FileEvent(BaseEvent):
|
|
136
|
+
type: Literal["file"] = "file"
|
|
137
|
+
url: str
|
|
138
|
+
mediaType: str
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── Error ──────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ErrorEvent(BaseEvent):
|
|
145
|
+
type: Literal["error"] = "error"
|
|
146
|
+
errorText: str
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── Custom data parts ─────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class DataEvent(BaseEvent):
|
|
153
|
+
"""Custom data part with a dynamic ``data-{name}`` type.
|
|
154
|
+
|
|
155
|
+
Not included in the ``UIMessageStreamEvent`` discriminated union because
|
|
156
|
+
the ``type`` field is dynamic. Use ``ctx.write_data()`` to emit these.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
type: str # "data-{name}", validated by ctx.write_data()
|
|
160
|
+
data: dict[str, Any]
|
|
161
|
+
id: str | None = None
|
|
162
|
+
transient: bool | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── Abort ──────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class AbortEvent(BaseEvent):
|
|
169
|
+
type: Literal["abort"] = "abort"
|
|
170
|
+
reason: str | None = None
|
|
171
|
+
|
|
172
|
+
|
|
132
173
|
# ── Discriminated union ────────────────────────────────────────────────────────
|
|
133
174
|
|
|
134
175
|
UIMessageStreamEvent = Annotated[
|
|
@@ -146,6 +187,9 @@ UIMessageStreamEvent = Annotated[
|
|
|
146
187
|
| ToolOutputAvailableEvent
|
|
147
188
|
| ToolOutputErrorEvent
|
|
148
189
|
| SourceUrlEvent
|
|
190
|
+
| FileEvent
|
|
191
|
+
| ErrorEvent
|
|
192
|
+
| AbortEvent
|
|
149
193
|
| FinishStepEvent
|
|
150
194
|
| FinishEvent,
|
|
151
195
|
Field(discriminator="type"),
|
|
@@ -170,4 +214,8 @@ __all__ = [
|
|
|
170
214
|
"ToolOutputAvailableEvent",
|
|
171
215
|
"ToolOutputErrorEvent",
|
|
172
216
|
"SourceUrlEvent",
|
|
217
|
+
"FileEvent",
|
|
218
|
+
"DataEvent",
|
|
219
|
+
"ErrorEvent",
|
|
220
|
+
"AbortEvent",
|
|
173
221
|
]
|
|
File without changes
|
{ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/py.typed
RENAMED
|
File without changes
|
{ai_sdk_stream_python-0.2.0a2 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/state.py
RENAMED
|
File without changes
|