ai-sdk-stream-python 0.2.0a1__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.0a1 → ai_sdk_stream_python-0.2.0a3}/PKG-INFO +32 -3
- {ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/README.md +31 -2
- {ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/pyproject.toml +1 -1
- {ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/__init__.py +15 -0
- {ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/collect.py +41 -1
- {ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/context.py +159 -6
- {ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/events.py +48 -0
- {ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/py.typed +0 -0
- {ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ai-sdk-stream-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.0a3
|
|
4
4
|
Summary: A Python package for AI SDK streaming utilities
|
|
5
5
|
Author: Shloimy Wiesel
|
|
6
6
|
Requires-Dist: fastapi>=0.128.8
|
|
@@ -63,6 +63,7 @@ async def my_work(ctx):
|
|
|
63
63
|
| **Typed events** | All 16 v6 protocol events as Pydantic models |
|
|
64
64
|
| **Lifecycle auto-management** | `start`, `start-step`, `text-start` etc. are emitted automatically |
|
|
65
65
|
| **Shared state** | `ctx.store.get/set()` — dot-path key-value store shared across modules |
|
|
66
|
+
| **Custom information** | `ctx.info` — typed, read-only Pydantic model for request-scoped metadata |
|
|
66
67
|
| **Pass as parameter** | `ctx` flows through your services like a logger or DB session |
|
|
67
68
|
| **Stream collection** | `collect=True` records all emitted content into `ctx.record` for DB persistence |
|
|
68
69
|
| **Low-level escape hatch** | `ctx.write(event)` / `ctx.write_event_to_stream(ev)` for raw control |
|
|
@@ -123,6 +124,33 @@ plan = await ctx.store.get("user.plan", default="free") # with default
|
|
|
123
124
|
|
|
124
125
|
The store uses an `asyncio.Lock` — safe to use across concurrent coroutines.
|
|
125
126
|
|
|
127
|
+
### Custom information (`ctx.info`)
|
|
128
|
+
|
|
129
|
+
Pass a Pydantic model at construction time to carry static, read-only request-scoped data (e.g. `user_id`, `rate_limit`, `tenant_id`) through every service layer without threading extra arguments:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from pydantic import BaseModel
|
|
133
|
+
from ai_sdk_stream_python import StreamContext
|
|
134
|
+
|
|
135
|
+
class RequestInfo(BaseModel):
|
|
136
|
+
user_id: str
|
|
137
|
+
rate_limit: int
|
|
138
|
+
|
|
139
|
+
# Typed constructor — IDE infers ctx.info as RequestInfo
|
|
140
|
+
ctx: StreamContext[RequestInfo] = StreamContext(
|
|
141
|
+
custom_information=RequestInfo(user_id="u_42", rate_limit=100)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# In any service layer that receives ctx:
|
|
145
|
+
if ctx.info is not None:
|
|
146
|
+
print(ctx.info.user_id) # "u_42"
|
|
147
|
+
print(ctx.info.rate_limit) # 100
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- `ctx.info` is **read-only** (no setter). For mutable runtime state use `ctx.store`.
|
|
151
|
+
- Defaults to `None` when `custom_information` is not passed.
|
|
152
|
+
- `StreamContext` is generic — annotate as `StreamContext[YourModel]` for full IDE support.
|
|
153
|
+
|
|
126
154
|
### Writing to the stream
|
|
127
155
|
|
|
128
156
|
```python
|
|
@@ -226,6 +254,7 @@ ctx.message_id # the message ID in the start event
|
|
|
226
254
|
ctx.current_text_id # ID of open text part, or None
|
|
227
255
|
ctx.current_reasoning_id # ID of open reasoning part, or None
|
|
228
256
|
ctx.is_finished # True after finish()/abort()
|
|
257
|
+
ctx.info # custom_information model, or None
|
|
229
258
|
ctx.response_headers # dict with x-vercel-ai-ui-message-stream: v1
|
|
230
259
|
```
|
|
231
260
|
|
|
@@ -367,9 +396,9 @@ npm run dev
|
|
|
367
396
|
uv run pytest tests/ -v
|
|
368
397
|
```
|
|
369
398
|
|
|
370
|
-
|
|
399
|
+
56 tests covering: basic lifecycle, reasoning ↔ text transitions, tool calls,
|
|
371
400
|
multi-step flows, source events, edge cases (double finish, abort, write after finish),
|
|
372
|
-
StateStore integration,
|
|
401
|
+
StateStore integration, stream collection (`collect=True`), and custom information (`ctx.info`).
|
|
373
402
|
|
|
374
403
|
---
|
|
375
404
|
|
|
@@ -53,6 +53,7 @@ async def my_work(ctx):
|
|
|
53
53
|
| **Typed events** | All 16 v6 protocol events as Pydantic models |
|
|
54
54
|
| **Lifecycle auto-management** | `start`, `start-step`, `text-start` etc. are emitted automatically |
|
|
55
55
|
| **Shared state** | `ctx.store.get/set()` — dot-path key-value store shared across modules |
|
|
56
|
+
| **Custom information** | `ctx.info` — typed, read-only Pydantic model for request-scoped metadata |
|
|
56
57
|
| **Pass as parameter** | `ctx` flows through your services like a logger or DB session |
|
|
57
58
|
| **Stream collection** | `collect=True` records all emitted content into `ctx.record` for DB persistence |
|
|
58
59
|
| **Low-level escape hatch** | `ctx.write(event)` / `ctx.write_event_to_stream(ev)` for raw control |
|
|
@@ -113,6 +114,33 @@ plan = await ctx.store.get("user.plan", default="free") # with default
|
|
|
113
114
|
|
|
114
115
|
The store uses an `asyncio.Lock` — safe to use across concurrent coroutines.
|
|
115
116
|
|
|
117
|
+
### Custom information (`ctx.info`)
|
|
118
|
+
|
|
119
|
+
Pass a Pydantic model at construction time to carry static, read-only request-scoped data (e.g. `user_id`, `rate_limit`, `tenant_id`) through every service layer without threading extra arguments:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from pydantic import BaseModel
|
|
123
|
+
from ai_sdk_stream_python import StreamContext
|
|
124
|
+
|
|
125
|
+
class RequestInfo(BaseModel):
|
|
126
|
+
user_id: str
|
|
127
|
+
rate_limit: int
|
|
128
|
+
|
|
129
|
+
# Typed constructor — IDE infers ctx.info as RequestInfo
|
|
130
|
+
ctx: StreamContext[RequestInfo] = StreamContext(
|
|
131
|
+
custom_information=RequestInfo(user_id="u_42", rate_limit=100)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# In any service layer that receives ctx:
|
|
135
|
+
if ctx.info is not None:
|
|
136
|
+
print(ctx.info.user_id) # "u_42"
|
|
137
|
+
print(ctx.info.rate_limit) # 100
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- `ctx.info` is **read-only** (no setter). For mutable runtime state use `ctx.store`.
|
|
141
|
+
- Defaults to `None` when `custom_information` is not passed.
|
|
142
|
+
- `StreamContext` is generic — annotate as `StreamContext[YourModel]` for full IDE support.
|
|
143
|
+
|
|
116
144
|
### Writing to the stream
|
|
117
145
|
|
|
118
146
|
```python
|
|
@@ -216,6 +244,7 @@ ctx.message_id # the message ID in the start event
|
|
|
216
244
|
ctx.current_text_id # ID of open text part, or None
|
|
217
245
|
ctx.current_reasoning_id # ID of open reasoning part, or None
|
|
218
246
|
ctx.is_finished # True after finish()/abort()
|
|
247
|
+
ctx.info # custom_information model, or None
|
|
219
248
|
ctx.response_headers # dict with x-vercel-ai-ui-message-stream: v1
|
|
220
249
|
```
|
|
221
250
|
|
|
@@ -357,9 +386,9 @@ npm run dev
|
|
|
357
386
|
uv run pytest tests/ -v
|
|
358
387
|
```
|
|
359
388
|
|
|
360
|
-
|
|
389
|
+
56 tests covering: basic lifecycle, reasoning ↔ text transitions, tool calls,
|
|
361
390
|
multi-step flows, source events, edge cases (double finish, abort, write after finish),
|
|
362
|
-
StateStore integration,
|
|
391
|
+
StateStore integration, stream collection (`collect=True`), and custom information (`ctx.info`).
|
|
363
392
|
|
|
364
393
|
---
|
|
365
394
|
|
{ai_sdk_stream_python-0.2.0a1 → 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.0a1 → 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.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/context.py
RENAMED
|
@@ -56,11 +56,23 @@ import asyncio
|
|
|
56
56
|
import uuid
|
|
57
57
|
from collections.abc import AsyncGenerator
|
|
58
58
|
from dataclasses import dataclass
|
|
59
|
-
from typing import Any, ClassVar
|
|
59
|
+
from typing import Any, ClassVar, Generic, TypeVar
|
|
60
60
|
|
|
61
|
-
from
|
|
61
|
+
from pydantic import BaseModel
|
|
62
|
+
|
|
63
|
+
from .collect import (
|
|
64
|
+
DataPartRecord,
|
|
65
|
+
FileRecord,
|
|
66
|
+
SourceRecord,
|
|
67
|
+
StreamRecord,
|
|
68
|
+
ToolCallRecord,
|
|
69
|
+
)
|
|
62
70
|
from .events import (
|
|
71
|
+
AbortEvent,
|
|
63
72
|
BaseEvent,
|
|
73
|
+
DataEvent,
|
|
74
|
+
ErrorEvent,
|
|
75
|
+
FileEvent,
|
|
64
76
|
FinishEvent,
|
|
65
77
|
FinishStepEvent,
|
|
66
78
|
ReasoningDeltaEvent,
|
|
@@ -73,12 +85,15 @@ from .events import (
|
|
|
73
85
|
TextEndEvent,
|
|
74
86
|
TextStartEvent,
|
|
75
87
|
ToolInputAvailableEvent,
|
|
88
|
+
ToolInputDeltaEvent,
|
|
76
89
|
ToolInputStartEvent,
|
|
77
90
|
ToolOutputAvailableEvent,
|
|
78
91
|
ToolOutputErrorEvent,
|
|
79
92
|
)
|
|
80
93
|
from .state import StateStore
|
|
81
94
|
|
|
95
|
+
_InfoT = TypeVar("_InfoT", bound=BaseModel)
|
|
96
|
+
|
|
82
97
|
|
|
83
98
|
@dataclass
|
|
84
99
|
class ToolCallHandle:
|
|
@@ -88,7 +103,7 @@ class ToolCallHandle:
|
|
|
88
103
|
toolName: str
|
|
89
104
|
|
|
90
105
|
|
|
91
|
-
class StreamContext:
|
|
106
|
+
class StreamContext(Generic[_InfoT]):
|
|
92
107
|
"""
|
|
93
108
|
A stateful context for producing a Vercel AI SDK v6 UIMessageStream.
|
|
94
109
|
|
|
@@ -100,6 +115,11 @@ class StreamContext:
|
|
|
100
115
|
----------
|
|
101
116
|
store : StateStore
|
|
102
117
|
Async key-value store shared across the background task and any caller.
|
|
118
|
+
info : _InfoT | None
|
|
119
|
+
Static, read-only metadata supplied at construction time (e.g. user_id,
|
|
120
|
+
rate_limit). Pass any Pydantic ``BaseModel`` instance as
|
|
121
|
+
``custom_information=`` — it is available unchanged for the entire
|
|
122
|
+
lifetime of the context. Defaults to ``None`` when not provided.
|
|
103
123
|
"""
|
|
104
124
|
|
|
105
125
|
response_headers: ClassVar[dict[str, str]] = {
|
|
@@ -114,10 +134,12 @@ class StreamContext:
|
|
|
114
134
|
message_id: str | None = None,
|
|
115
135
|
*,
|
|
116
136
|
collect: bool = False,
|
|
137
|
+
custom_information: _InfoT | None = None,
|
|
117
138
|
) -> None:
|
|
118
139
|
self._message_id: str = message_id or str(uuid.uuid4())
|
|
119
140
|
self._queue: asyncio.Queue[BaseEvent | None] = asyncio.Queue()
|
|
120
141
|
self.store: StateStore = StateStore()
|
|
142
|
+
self._info: _InfoT | None = custom_information
|
|
121
143
|
|
|
122
144
|
# Lifecycle state
|
|
123
145
|
self._started: bool = False
|
|
@@ -151,6 +173,18 @@ class StreamContext:
|
|
|
151
173
|
def is_finished(self) -> bool:
|
|
152
174
|
return self._finished
|
|
153
175
|
|
|
176
|
+
@property
|
|
177
|
+
def info(self) -> _InfoT | None:
|
|
178
|
+
"""
|
|
179
|
+
Static, read-only metadata supplied at construction time.
|
|
180
|
+
|
|
181
|
+
Returns the ``custom_information`` value passed to ``__init__``, or
|
|
182
|
+
``None`` if none was provided. Useful for carrying request-scoped
|
|
183
|
+
data (e.g. ``user_id``, ``rate_limit``) through service layers without
|
|
184
|
+
threading extra function arguments.
|
|
185
|
+
"""
|
|
186
|
+
return self._info
|
|
187
|
+
|
|
154
188
|
@property
|
|
155
189
|
def record(self) -> StreamRecord | None:
|
|
156
190
|
"""
|
|
@@ -284,6 +318,63 @@ class StreamContext:
|
|
|
284
318
|
)
|
|
285
319
|
return ToolCallHandle(toolCallId=tcid, toolName=tool_name)
|
|
286
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
|
+
|
|
287
378
|
async def complete_tool_call(self, tool_call_id: str, output: Any) -> None:
|
|
288
379
|
"""Emit ``tool-output-available`` with the tool result."""
|
|
289
380
|
if self._record is not None:
|
|
@@ -306,6 +397,51 @@ class StreamContext:
|
|
|
306
397
|
ToolOutputErrorEvent(toolCallId=tool_call_id, error=error)
|
|
307
398
|
)
|
|
308
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
|
+
|
|
309
445
|
async def write_source(
|
|
310
446
|
self,
|
|
311
447
|
source_id: str,
|
|
@@ -354,16 +490,33 @@ class StreamContext:
|
|
|
354
490
|
)
|
|
355
491
|
self._queue.put_nowait(None) # sentinel → stream() yields [DONE]
|
|
356
492
|
|
|
357
|
-
async def abort(self) -> None:
|
|
493
|
+
async def abort(self, reason: str | None = None) -> None:
|
|
358
494
|
"""
|
|
359
|
-
|
|
495
|
+
Emit an ``abort`` event and terminate the stream.
|
|
360
496
|
|
|
361
497
|
Use in error-handling paths where the normal ``finish()`` flow
|
|
362
|
-
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.
|
|
363
514
|
"""
|
|
364
515
|
if self._finished:
|
|
365
516
|
return
|
|
517
|
+
await self._ensure_started()
|
|
366
518
|
self._finished = True
|
|
519
|
+
self._queue.put_nowait(ErrorEvent(errorText=error_text))
|
|
367
520
|
self._queue.put_nowait(None)
|
|
368
521
|
|
|
369
522
|
# ── SSE async generator ───────────────────────────────────────────────────
|
{ai_sdk_stream_python-0.2.0a1 → 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
|
]
|
{ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/py.typed
RENAMED
|
File without changes
|
{ai_sdk_stream_python-0.2.0a1 → ai_sdk_stream_python-0.2.0a3}/src/ai_sdk_stream_python/state.py
RENAMED
|
File without changes
|