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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ai-sdk-stream-python
3
- Version: 0.2.0a2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ai-sdk-stream-python"
3
- version = "0.2.0-a.2"
3
+ version = "0.2.0-a.3"
4
4
  description = "A Python package for AI SDK streaming utilities"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
  ]
@@ -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__ = ["SourceRecord", "StreamRecord", "ToolCallRecord"]
135
+ __all__ = [
136
+ "DataPartRecord",
137
+ "FileRecord",
138
+ "SourceRecord",
139
+ "StreamRecord",
140
+ "ToolCallRecord",
141
+ ]
@@ -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 SourceRecord, StreamRecord, ToolCallRecord
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
- Terminate the stream immediately without a proper finish event.
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 ───────────────────────────────────────────────────
@@ -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
  ]