Skip to content

Commit 301ef43

Browse files
authored
Merge branch '1.0-dev' into ishymko/extensions
2 parents 76829dd + 5f3ea29 commit 301ef43

30 files changed

Lines changed: 638 additions & 1483 deletions

samples/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
import httpx
1111

1212
from a2a.client import A2ACardResolver, ClientConfig, create_client
13+
from a2a.helpers import get_artifact_text, get_message_text
14+
from a2a.helpers.agent_card import display_agent_card
1315
from a2a.types import Message, Part, Role, SendMessageRequest, TaskState
14-
from a2a.utils import get_artifact_text, get_message_text
15-
from a2a.utils.agent_card import display_agent_card
1616

1717

1818
async def _handle_stream(

scripts/test_minimal_install.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,13 @@
5050
'a2a.server.tasks',
5151
'a2a.types',
5252
'a2a.utils',
53-
'a2a.utils.artifact',
5453
'a2a.utils.constants',
5554
'a2a.utils.error_handlers',
5655
'a2a.utils.helpers',
57-
'a2a.utils.message',
58-
'a2a.utils.parts',
5956
'a2a.utils.proto_utils',
6057
'a2a.utils.task',
58+
'a2a.helpers.agent_card',
59+
'a2a.helpers.proto_helpers',
6160
]
6261

6362

src/a2a/client/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
A2AClientTimeoutError,
2323
AgentCardResolutionError,
2424
)
25-
from a2a.client.helpers import create_text_message_object
2625
from a2a.client.interceptors import ClientCallInterceptor
2726

2827

@@ -41,6 +40,5 @@
4140
'CredentialService',
4241
'InMemoryContextCredentialStore',
4342
'create_client',
44-
'create_text_message_object',
4543
'minimal_agent_card',
4644
]

src/a2a/client/helpers.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"""Helper functions for the A2A client."""
22

33
from typing import Any
4-
from uuid import uuid4
54

65
from google.protobuf.json_format import ParseDict
76

8-
from a2a.types.a2a_pb2 import AgentCard, Message, Part, Role
7+
from a2a.types.a2a_pb2 import AgentCard
98

109

1110
def parse_agent_card(agent_card_data: dict[str, Any]) -> AgentCard:
@@ -111,20 +110,3 @@ def _handle_security_compatibility(agent_card_data: dict[str, Any]) -> None:
111110
new_scheme_wrapper = {mapped_name: scheme.copy()}
112111
scheme.clear()
113112
scheme.update(new_scheme_wrapper)
114-
115-
116-
def create_text_message_object(
117-
role: Role = Role.ROLE_USER, content: str = ''
118-
) -> Message:
119-
"""Create a Message object containing a single text Part.
120-
121-
Args:
122-
role: The role of the message sender (user or agent). Defaults to Role.ROLE_USER.
123-
content: The text content of the message. Defaults to an empty string.
124-
125-
Returns:
126-
A `Message` object with a new UUID message_id.
127-
"""
128-
return Message(
129-
role=role, parts=[Part(text=content)], message_id=str(uuid4())
130-
)

src/a2a/helpers/__init__.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Helper functions for the A2A Python SDK."""
2+
3+
from a2a.helpers.agent_card import display_agent_card
4+
from a2a.helpers.proto_helpers import (
5+
get_artifact_text,
6+
get_message_text,
7+
get_stream_response_text,
8+
get_text_parts,
9+
new_artifact,
10+
new_message,
11+
new_task,
12+
new_task_from_user_message,
13+
new_text_artifact,
14+
new_text_artifact_update_event,
15+
new_text_message,
16+
new_text_status_update_event,
17+
)
18+
19+
20+
__all__ = [
21+
'display_agent_card',
22+
'get_artifact_text',
23+
'get_message_text',
24+
'get_stream_response_text',
25+
'get_text_parts',
26+
'new_artifact',
27+
'new_message',
28+
'new_task',
29+
'new_task_from_user_message',
30+
'new_text_artifact',
31+
'new_text_artifact_update_event',
32+
'new_text_message',
33+
'new_text_status_update_event',
34+
]

src/a2a/helpers/proto_helpers.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""Unified helper functions for creating and handling A2A types."""
2+
3+
import uuid
4+
5+
from collections.abc import Sequence
6+
7+
from a2a.types.a2a_pb2 import (
8+
Artifact,
9+
Message,
10+
Part,
11+
Role,
12+
StreamResponse,
13+
Task,
14+
TaskArtifactUpdateEvent,
15+
TaskState,
16+
TaskStatus,
17+
TaskStatusUpdateEvent,
18+
)
19+
20+
21+
# --- Message Helpers ---
22+
23+
24+
def new_message(
25+
parts: list[Part],
26+
role: Role = Role.ROLE_AGENT,
27+
context_id: str | None = None,
28+
task_id: str | None = None,
29+
) -> Message:
30+
"""Creates a new message containing a list of Parts."""
31+
return Message(
32+
role=role,
33+
parts=parts,
34+
message_id=str(uuid.uuid4()),
35+
task_id=task_id,
36+
context_id=context_id,
37+
)
38+
39+
40+
def new_text_message(
41+
text: str,
42+
context_id: str | None = None,
43+
task_id: str | None = None,
44+
role: Role = Role.ROLE_AGENT,
45+
) -> Message:
46+
"""Creates a new message containing a single text Part."""
47+
return new_message(
48+
parts=[Part(text=text)],
49+
role=role,
50+
task_id=task_id,
51+
context_id=context_id,
52+
)
53+
54+
55+
def get_message_text(message: Message, delimiter: str = '\n') -> str:
56+
"""Extracts and joins all text content from a Message's parts."""
57+
return delimiter.join(get_text_parts(message.parts))
58+
59+
60+
# --- Artifact Helpers ---
61+
62+
63+
def new_artifact(
64+
parts: list[Part],
65+
name: str,
66+
description: str | None = None,
67+
artifact_id: str | None = None,
68+
) -> Artifact:
69+
"""Creates a new Artifact object."""
70+
return Artifact(
71+
artifact_id=artifact_id or str(uuid.uuid4()),
72+
parts=parts,
73+
name=name,
74+
description=description,
75+
)
76+
77+
78+
def new_text_artifact(
79+
name: str,
80+
text: str,
81+
description: str | None = None,
82+
artifact_id: str | None = None,
83+
) -> Artifact:
84+
"""Creates a new Artifact object containing only a single text Part."""
85+
return new_artifact(
86+
[Part(text=text)],
87+
name,
88+
description,
89+
artifact_id=artifact_id,
90+
)
91+
92+
93+
def get_artifact_text(artifact: Artifact, delimiter: str = '\n') -> str:
94+
"""Extracts and joins all text content from an Artifact's parts."""
95+
return delimiter.join(get_text_parts(artifact.parts))
96+
97+
98+
# --- Task Helpers ---
99+
100+
101+
def new_task_from_user_message(user_message: Message) -> Task:
102+
"""Creates a new Task object from an initial user message."""
103+
if user_message.role != Role.ROLE_USER:
104+
raise ValueError('Message must be from a user')
105+
if not user_message.parts:
106+
raise ValueError('Message parts cannot be empty')
107+
for part in user_message.parts:
108+
if part.HasField('text') and not part.text:
109+
raise ValueError('Message.text cannot be empty')
110+
111+
return Task(
112+
status=TaskStatus(state=TaskState.TASK_STATE_SUBMITTED),
113+
id=user_message.task_id or str(uuid.uuid4()),
114+
context_id=user_message.context_id or str(uuid.uuid4()),
115+
history=[user_message],
116+
)
117+
118+
119+
def new_task(
120+
task_id: str,
121+
context_id: str,
122+
state: TaskState,
123+
artifacts: list[Artifact] | None = None,
124+
history: list[Message] | None = None,
125+
) -> Task:
126+
"""Creates a Task object with a specified status."""
127+
if history is None:
128+
history = []
129+
if artifacts is None:
130+
artifacts = []
131+
132+
return Task(
133+
status=TaskStatus(state=state),
134+
id=task_id,
135+
context_id=context_id,
136+
artifacts=artifacts,
137+
history=history,
138+
)
139+
140+
141+
# --- Part Helpers ---
142+
143+
144+
def get_text_parts(parts: Sequence[Part]) -> list[str]:
145+
"""Extracts text content from all text Parts."""
146+
return [part.text for part in parts if part.HasField('text')]
147+
148+
149+
# --- Event & Stream Helpers ---
150+
151+
152+
def new_text_status_update_event(
153+
task_id: str,
154+
context_id: str,
155+
state: TaskState,
156+
text: str,
157+
) -> TaskStatusUpdateEvent:
158+
"""Creates a TaskStatusUpdateEvent with a single text message."""
159+
return TaskStatusUpdateEvent(
160+
task_id=task_id,
161+
context_id=context_id,
162+
status=TaskStatus(
163+
state=state,
164+
message=new_text_message(
165+
text=text,
166+
role=Role.ROLE_AGENT,
167+
context_id=context_id,
168+
task_id=task_id,
169+
),
170+
),
171+
)
172+
173+
174+
def new_text_artifact_update_event( # noqa: PLR0913
175+
task_id: str,
176+
context_id: str,
177+
name: str,
178+
text: str,
179+
append: bool = False,
180+
last_chunk: bool = False,
181+
artifact_id: str | None = None,
182+
) -> TaskArtifactUpdateEvent:
183+
"""Creates a TaskArtifactUpdateEvent with a single text artifact."""
184+
return TaskArtifactUpdateEvent(
185+
task_id=task_id,
186+
context_id=context_id,
187+
artifact=new_text_artifact(
188+
name=name, text=text, artifact_id=artifact_id
189+
),
190+
append=append,
191+
last_chunk=last_chunk,
192+
)
193+
194+
195+
def get_stream_response_text(
196+
response: StreamResponse, delimiter: str = '\n'
197+
) -> str:
198+
"""Extracts text content from a StreamResponse."""
199+
if response.HasField('message'):
200+
return get_message_text(response.message, delimiter)
201+
if response.HasField('task'):
202+
texts = [
203+
get_artifact_text(a, delimiter) for a in response.task.artifacts
204+
]
205+
return delimiter.join(t for t in texts if t)
206+
if response.HasField('status_update'):
207+
if response.status_update.status.HasField('message'):
208+
return get_message_text(
209+
response.status_update.status.message, delimiter
210+
)
211+
return ''
212+
if response.HasField('artifact_update'):
213+
return get_artifact_text(response.artifact_update.artifact, delimiter)
214+
return ''

src/a2a/server/agent_execution/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any
22

3+
from a2a.helpers.proto_helpers import get_message_text
34
from a2a.server.context import ServerCallContext
45
from a2a.server.id_generator import (
56
IDGenerator,
@@ -12,7 +13,6 @@
1213
SendMessageRequest,
1314
Task,
1415
)
15-
from a2a.utils import get_message_text
1616
from a2a.utils.errors import InvalidParamsError
1717

1818

0 commit comments

Comments
 (0)