Skip to content

Commit 4b8c0c0

Browse files
committed
Add ArtifactStreamer helper for streaming text artifact updates
new_text_artifact generates a fresh UUID on every call, making it unsuitable for streaming where append semantics require a stable artifact ID across chunks. ArtifactStreamer holds a single artifact ID and provides append() and finalize() methods that emit correctly shaped TaskArtifactUpdateEvents with append=True.
1 parent 2974028 commit 4b8c0c0

2 files changed

Lines changed: 87 additions & 10 deletions

File tree

src/a2a/utils/__init__.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Utility functions for the A2A Python SDK."""
22

33
from a2a.utils.artifact import (
4+
ArtifactStreamer,
45
get_artifact_text,
56
new_artifact,
67
new_data_artifact,
@@ -23,22 +24,16 @@
2324
new_agent_parts_message,
2425
new_agent_text_message,
2526
)
26-
from a2a.utils.parts import (
27-
get_data_parts,
28-
get_file_parts,
29-
get_text_parts,
30-
)
31-
from a2a.utils.task import (
32-
completed_task,
33-
new_task,
34-
)
27+
from a2a.utils.parts import get_data_parts, get_file_parts, get_text_parts
28+
from a2a.utils.task import completed_task, new_task
3529

3630

3731
__all__ = [
3832
'AGENT_CARD_WELL_KNOWN_PATH',
3933
'DEFAULT_RPC_URL',
4034
'EXTENDED_AGENT_CARD_PATH',
4135
'PREV_AGENT_CARD_WELL_KNOWN_PATH',
36+
'ArtifactStreamer',
4237
'append_artifact_to_task',
4338
'are_modalities_compatible',
4439
'build_text_artifact',

src/a2a/utils/artifact.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44

55
from typing import Any
66

7-
from a2a.types import Artifact, DataPart, Part, TextPart
7+
from a2a.types import (
8+
Artifact,
9+
DataPart,
10+
Part,
11+
TaskArtifactUpdateEvent,
12+
TextPart,
13+
)
814
from a2a.utils.parts import get_text_parts
915

1016

@@ -86,3 +92,79 @@ def get_artifact_text(artifact: Artifact, delimiter: str = '\n') -> str:
8692
A single string containing all text content, or an empty string if no text parts are found.
8793
"""
8894
return delimiter.join(get_text_parts(artifact.parts))
95+
96+
97+
class ArtifactStreamer:
98+
"""Helper for streaming text into a single artifact across multiple events.
99+
100+
Creates a stable artifact ID on construction so all chunks reference
101+
the same artifact, enabling proper append semantics per the A2A spec.
102+
103+
Example::
104+
105+
streamer = ArtifactStreamer(context_id, task_id, name='response')
106+
107+
async for chunk in llm.stream(prompt):
108+
await event_queue.enqueue_event(streamer.append(chunk))
109+
110+
await event_queue.enqueue_event(streamer.finalize())
111+
112+
Args:
113+
context_id: The context ID associated with the task.
114+
task_id: The task ID associated with the streaming session.
115+
name: A human-readable name for the artifact.
116+
artifact_id: An explicit artifact ID. If omitted a UUID is generated.
117+
"""
118+
119+
def __init__(
120+
self,
121+
context_id: str,
122+
task_id: str,
123+
name: str = 'response',
124+
artifact_id: str | None = None,
125+
) -> None:
126+
self.context_id = context_id
127+
self.task_id = task_id
128+
self.name = name
129+
self.artifact_id = artifact_id or str(uuid.uuid4())
130+
131+
def append(self, text: str) -> TaskArtifactUpdateEvent:
132+
"""Emit a chunk to be appended to the streaming artifact.
133+
134+
Args:
135+
text: The incremental text content for this chunk.
136+
137+
Returns:
138+
A ``TaskArtifactUpdateEvent`` with ``append=True`` and
139+
``last_chunk=False``.
140+
"""
141+
return TaskArtifactUpdateEvent(
142+
context_id=self.context_id,
143+
task_id=self.task_id,
144+
append=True,
145+
last_chunk=False,
146+
artifact=Artifact(
147+
artifact_id=self.artifact_id,
148+
name=self.name,
149+
parts=[Part(root=TextPart(text=text))],
150+
),
151+
)
152+
153+
def finalize(self) -> TaskArtifactUpdateEvent:
154+
"""Signal that the artifact stream is complete.
155+
156+
Returns:
157+
A ``TaskArtifactUpdateEvent`` with ``append=True`` and
158+
``last_chunk=True``.
159+
"""
160+
return TaskArtifactUpdateEvent(
161+
context_id=self.context_id,
162+
task_id=self.task_id,
163+
append=True,
164+
last_chunk=True,
165+
artifact=Artifact(
166+
artifact_id=self.artifact_id,
167+
name=self.name,
168+
parts=[],
169+
),
170+
)

0 commit comments

Comments
 (0)