Skip to content

Commit 745d998

Browse files
feat(helpers): add non-text Part extractors and artifact update events (#1017)
## Summary - Adds `get_data_parts`, `get_raw_parts`, `get_url_parts` extractors to complement the existing `get_text_parts`, completing the Part extraction API for all content types. - Adds `new_data_artifact_update_event`, `new_raw_artifact_update_event`, `new_url_artifact_update_event` to complement the existing `new_text_artifact_update_event`, completing the streaming event helpers for all content types. - Re-exports all new helpers and the previously unexposed helpers from #1004 (`new_data_part`, `new_raw_part`, `new_url_part`, `new_text_part`, `new_data_message`, `new_raw_message`, `new_url_message`, `new_data_artifact`, `new_raw_artifact`, `new_url_artifact`) in `a2a.helpers.__init__` so they are accessible via `from a2a.helpers import ...`. ## Changes - `src/a2a/helpers/proto_helpers.py`: Added 6 new functions (3 extractors + 3 artifact update events). - `src/a2a/helpers/__init__.py`: Updated imports and `__all__` to expose all 16 new helpers. - `tests/helpers/test_proto_helpers.py`: Added 12 new test cases covering all new functions. ## Testing - All mandatory checks pass: `ruff`, `mypy`, `pyright`, `pytest` (50 helper tests pass, 1675 total). --------- Co-authored-by: Iva Sokolaj <102302011+sokoliva@users.noreply.github.com>
1 parent 1642f6d commit 745d998

3 files changed

Lines changed: 364 additions & 2 deletions

File tree

src/a2a/helpers/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,64 @@
33
from a2a.helpers.agent_card import display_agent_card
44
from a2a.helpers.proto_helpers import (
55
get_artifact_text,
6+
get_data_parts,
67
get_message_text,
8+
get_raw_parts,
79
get_stream_response_text,
810
get_text_parts,
11+
get_url_parts,
912
new_artifact,
13+
new_data_artifact,
14+
new_data_artifact_update_event,
15+
new_data_message,
16+
new_data_part,
1017
new_message,
18+
new_raw_artifact,
19+
new_raw_artifact_update_event,
20+
new_raw_message,
21+
new_raw_part,
1122
new_task,
1223
new_task_from_user_message,
1324
new_text_artifact,
1425
new_text_artifact_update_event,
1526
new_text_message,
27+
new_text_part,
1628
new_text_status_update_event,
29+
new_url_artifact,
30+
new_url_artifact_update_event,
31+
new_url_message,
32+
new_url_part,
1733
)
1834

1935

2036
__all__ = [
2137
'display_agent_card',
2238
'get_artifact_text',
39+
'get_data_parts',
2340
'get_message_text',
41+
'get_raw_parts',
2442
'get_stream_response_text',
2543
'get_text_parts',
44+
'get_url_parts',
2645
'new_artifact',
46+
'new_data_artifact',
47+
'new_data_artifact_update_event',
48+
'new_data_message',
49+
'new_data_part',
2750
'new_message',
51+
'new_raw_artifact',
52+
'new_raw_artifact_update_event',
53+
'new_raw_message',
54+
'new_raw_part',
2855
'new_task',
2956
'new_task_from_user_message',
3057
'new_text_artifact',
3158
'new_text_artifact_update_event',
3259
'new_text_message',
60+
'new_text_part',
3361
'new_text_status_update_event',
62+
'new_url_artifact',
63+
'new_url_artifact_update_event',
64+
'new_url_message',
65+
'new_url_part',
3466
]

src/a2a/helpers/proto_helpers.py

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Any
77

88
from google.protobuf import struct_pb2
9-
from google.protobuf.json_format import ParseDict
9+
from google.protobuf.json_format import MessageToDict, ParseDict
1010

1111
from a2a.types.a2a_pb2 import (
1212
Artifact,
@@ -401,6 +401,45 @@ def get_text_parts(parts: Sequence[Part]) -> list[str]:
401401
return [part.text for part in parts if part.HasField('text')]
402402

403403

404+
def get_data_parts(parts: Sequence[Part]) -> list[Any]:
405+
"""Extracts structured data from all data Parts.
406+
407+
Each returned element is the Python object obtained by converting
408+
the ``google.protobuf.Value`` back via ``MessageToDict``.
409+
410+
Args:
411+
parts: A sequence of ``Part`` objects.
412+
413+
Returns:
414+
A list of deserialized Python objects from any data Parts found.
415+
"""
416+
return [MessageToDict(part.data) for part in parts if part.HasField('data')]
417+
418+
419+
def get_raw_parts(parts: Sequence[Part]) -> list[bytes]:
420+
"""Extracts raw bytes content from all raw Parts.
421+
422+
Args:
423+
parts: A sequence of ``Part`` objects.
424+
425+
Returns:
426+
A list of ``bytes`` from any raw Parts found.
427+
"""
428+
return [part.raw for part in parts if part.HasField('raw')]
429+
430+
431+
def get_url_parts(parts: Sequence[Part]) -> list[str]:
432+
"""Extracts URL strings from all URL Parts.
433+
434+
Args:
435+
parts: A sequence of ``Part`` objects.
436+
437+
Returns:
438+
A list of URL strings from any URL Parts found.
439+
"""
440+
return [part.url for part in parts if part.HasField('url')]
441+
442+
404443
# --- Event & Stream Helpers ---
405444

406445

@@ -447,6 +486,129 @@ def new_text_artifact_update_event( # noqa: PLR0913
447486
)
448487

449488

489+
def new_data_artifact_update_event( # noqa: PLR0913
490+
task_id: str,
491+
context_id: str,
492+
name: str,
493+
data: Any,
494+
media_type: str | None = None,
495+
append: bool = False,
496+
last_chunk: bool = False,
497+
artifact_id: str | None = None,
498+
) -> TaskArtifactUpdateEvent:
499+
"""Creates a TaskArtifactUpdateEvent with a single data artifact.
500+
501+
Args:
502+
task_id: The task ID.
503+
context_id: The context ID.
504+
name: The name of the artifact.
505+
data: JSON-serializable data to embed (dict, list, str, etc.).
506+
media_type: Optional MIME type of the part content.
507+
append: Whether to append to the existing artifact.
508+
last_chunk: Whether this is the last chunk.
509+
artifact_id: Optional artifact ID (auto-generated if not provided).
510+
511+
Returns:
512+
A TaskArtifactUpdateEvent with a single data artifact.
513+
"""
514+
return TaskArtifactUpdateEvent(
515+
task_id=task_id,
516+
context_id=context_id,
517+
artifact=new_data_artifact(
518+
name=name,
519+
data=data,
520+
media_type=media_type,
521+
artifact_id=artifact_id,
522+
),
523+
append=append,
524+
last_chunk=last_chunk,
525+
)
526+
527+
528+
def new_raw_artifact_update_event( # noqa: PLR0913
529+
task_id: str,
530+
context_id: str,
531+
name: str,
532+
raw: bytes,
533+
media_type: str | None = None,
534+
filename: str | None = None,
535+
append: bool = False,
536+
last_chunk: bool = False,
537+
artifact_id: str | None = None,
538+
) -> TaskArtifactUpdateEvent:
539+
"""Creates a TaskArtifactUpdateEvent with a single raw bytes artifact.
540+
541+
Args:
542+
task_id: The task ID.
543+
context_id: The context ID.
544+
name: The name of the artifact.
545+
raw: The raw bytes content.
546+
media_type: Optional MIME type (e.g. 'image/png').
547+
filename: Optional filename.
548+
append: Whether to append to the existing artifact.
549+
last_chunk: Whether this is the last chunk.
550+
artifact_id: Optional artifact ID (auto-generated if not provided).
551+
552+
Returns:
553+
A TaskArtifactUpdateEvent with a single raw artifact.
554+
"""
555+
return TaskArtifactUpdateEvent(
556+
task_id=task_id,
557+
context_id=context_id,
558+
artifact=new_raw_artifact(
559+
name=name,
560+
raw=raw,
561+
media_type=media_type,
562+
filename=filename,
563+
artifact_id=artifact_id,
564+
),
565+
append=append,
566+
last_chunk=last_chunk,
567+
)
568+
569+
570+
def new_url_artifact_update_event( # noqa: PLR0913
571+
task_id: str,
572+
context_id: str,
573+
name: str,
574+
url: str,
575+
media_type: str | None = None,
576+
filename: str | None = None,
577+
append: bool = False,
578+
last_chunk: bool = False,
579+
artifact_id: str | None = None,
580+
) -> TaskArtifactUpdateEvent:
581+
"""Creates a TaskArtifactUpdateEvent with a single URL artifact.
582+
583+
Args:
584+
task_id: The task ID.
585+
context_id: The context ID.
586+
name: The name of the artifact.
587+
url: The URL pointing to the file content.
588+
media_type: Optional MIME type (e.g. 'image/png').
589+
filename: Optional filename.
590+
append: Whether to append to the existing artifact.
591+
last_chunk: Whether this is the last chunk.
592+
artifact_id: Optional artifact ID (auto-generated if not provided).
593+
594+
Returns:
595+
A TaskArtifactUpdateEvent with a single URL artifact.
596+
"""
597+
return TaskArtifactUpdateEvent(
598+
task_id=task_id,
599+
context_id=context_id,
600+
artifact=new_url_artifact(
601+
name=name,
602+
url=url,
603+
media_type=media_type,
604+
filename=filename,
605+
artifact_id=artifact_id,
606+
),
607+
append=append,
608+
last_chunk=last_chunk,
609+
)
610+
611+
450612
def get_stream_response_text(
451613
response: StreamResponse, delimiter: str = '\n'
452614
) -> str:

0 commit comments

Comments
 (0)