Skip to content

Commit 137e0f2

Browse files
authored
Merge branch 'main' into experimental-types-script
2 parents 73b2cc6 + 17e7597 commit 137e0f2

File tree

11 files changed

+402
-247
lines changed

11 files changed

+402
-247
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## [0.3.10](https://github.com/a2aproject/a2a-python/compare/v0.3.9...v0.3.10) (2025-10-21)
4+
5+
6+
### Features
7+
8+
* add `get_artifact_text()` helper method ([9155888](https://github.com/a2aproject/a2a-python/commit/9155888d258ca4d047002997e6674f3f15a67232))
9+
* Add a `ClientFactory.connect()` method for easy client creation ([d585635](https://github.com/a2aproject/a2a-python/commit/d5856359034f4d3d1e4578804727f47a3cd7c322))
10+
11+
12+
### Bug Fixes
13+
14+
* change `MAX_CONTENT_LENGTH` (for file attachment) in json-rpc to be larger size (10mb) ([#518](https://github.com/a2aproject/a2a-python/issues/518)) ([5b81385](https://github.com/a2aproject/a2a-python/commit/5b813856b4b4e07510a4ef41980d388e47c73b8e))
15+
* correct `new_artifact` methods signature ([#503](https://github.com/a2aproject/a2a-python/issues/503)) ([ee026aa](https://github.com/a2aproject/a2a-python/commit/ee026aa356042b9eb212eee59fa5135b280a3077))
16+
17+
18+
### Code Refactoring
19+
20+
* **utils:** move part helpers to their own file ([9155888](https://github.com/a2aproject/a2a-python/commit/9155888d258ca4d047002997e6674f3f15a67232))
21+
322
## [0.3.9](https://github.com/a2aproject/a2a-python/compare/v0.3.8...v0.3.9) (2025-10-15)
423

524

src/a2a/server/apps/jsonrpc/jsonrpc_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
Response = Any
9292
HTTP_413_REQUEST_ENTITY_TOO_LARGE = Any
9393

94-
MAX_CONTENT_LENGTH = 1_000_000
94+
MAX_CONTENT_LENGTH = 10_000_000
9595

9696

9797
class StarletteUserProxy(A2AUser):

src/a2a/utils/__init__.py

Lines changed: 7 additions & 3 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+
get_artifact_text,
45
new_artifact,
56
new_data_artifact,
67
new_text_artifact,
@@ -18,13 +19,15 @@
1819
create_task_obj,
1920
)
2021
from a2a.utils.message import (
21-
get_data_parts,
22-
get_file_parts,
2322
get_message_text,
24-
get_text_parts,
2523
new_agent_parts_message,
2624
new_agent_text_message,
2725
)
26+
from a2a.utils.parts import (
27+
get_data_parts,
28+
get_file_parts,
29+
get_text_parts,
30+
)
2831
from a2a.utils.task import (
2932
completed_task,
3033
new_task,
@@ -41,6 +44,7 @@
4144
'build_text_artifact',
4245
'completed_task',
4346
'create_task_obj',
47+
'get_artifact_text',
4448
'get_data_parts',
4549
'get_file_parts',
4650
'get_message_text',

src/a2a/utils/artifact.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
from typing import Any
66

77
from a2a.types import Artifact, DataPart, Part, TextPart
8+
from a2a.utils.parts import get_text_parts
89

910

1011
def new_artifact(
11-
parts: list[Part], name: str, description: str = ''
12+
parts: list[Part],
13+
name: str,
14+
description: str | None = None,
1215
) -> Artifact:
1316
"""Creates a new Artifact object.
1417
@@ -31,7 +34,7 @@ def new_artifact(
3134
def new_text_artifact(
3235
name: str,
3336
text: str,
34-
description: str = '',
37+
description: str | None = None,
3538
) -> Artifact:
3639
"""Creates a new Artifact object containing only a single TextPart.
3740
@@ -53,7 +56,7 @@ def new_text_artifact(
5356
def new_data_artifact(
5457
name: str,
5558
data: dict[str, Any],
56-
description: str = '',
59+
description: str | None = None,
5760
) -> Artifact:
5861
"""Creates a new Artifact object containing only a single DataPart.
5962
@@ -70,3 +73,16 @@ def new_data_artifact(
7073
name,
7174
description,
7275
)
76+
77+
78+
def get_artifact_text(artifact: Artifact, delimiter: str = '\n') -> str:
79+
"""Extracts and joins all text content from an Artifact's parts.
80+
81+
Args:
82+
artifact: The `Artifact` object.
83+
delimiter: The string to use when joining text from multiple TextParts.
84+
85+
Returns:
86+
A single string containing all text content, or an empty string if no text parts are found.
87+
"""
88+
return delimiter.join(get_text_parts(artifact.parts))

src/a2a/utils/message.py

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,13 @@
22

33
import uuid
44

5-
from typing import Any
6-
75
from a2a.types import (
8-
DataPart,
9-
FilePart,
10-
FileWithBytes,
11-
FileWithUri,
126
Message,
137
Part,
148
Role,
159
TextPart,
1610
)
11+
from a2a.utils.parts import get_text_parts
1712

1813

1914
def new_agent_text_message(
@@ -64,42 +59,6 @@ def new_agent_parts_message(
6459
)
6560

6661

67-
def get_text_parts(parts: list[Part]) -> list[str]:
68-
"""Extracts text content from all TextPart objects in a list of Parts.
69-
70-
Args:
71-
parts: A list of `Part` objects.
72-
73-
Returns:
74-
A list of strings containing the text content from any `TextPart` objects found.
75-
"""
76-
return [part.root.text for part in parts if isinstance(part.root, TextPart)]
77-
78-
79-
def get_data_parts(parts: list[Part]) -> list[dict[str, Any]]:
80-
"""Extracts dictionary data from all DataPart objects in a list of Parts.
81-
82-
Args:
83-
parts: A list of `Part` objects.
84-
85-
Returns:
86-
A list of dictionaries containing the data from any `DataPart` objects found.
87-
"""
88-
return [part.root.data for part in parts if isinstance(part.root, DataPart)]
89-
90-
91-
def get_file_parts(parts: list[Part]) -> list[FileWithBytes | FileWithUri]:
92-
"""Extracts file data from all FilePart objects in a list of Parts.
93-
94-
Args:
95-
parts: A list of `Part` objects.
96-
97-
Returns:
98-
A list of `FileWithBytes` or `FileWithUri` objects containing the file data from any `FilePart` objects found.
99-
"""
100-
return [part.root.file for part in parts if isinstance(part.root, FilePart)]
101-
102-
10362
def get_message_text(message: Message, delimiter: str = '\n') -> str:
10463
"""Extracts and joins all text content from a Message's parts.
10564

src/a2a/utils/parts.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Utility functions for creating and handling A2A Parts objects."""
2+
3+
from typing import Any
4+
5+
from a2a.types import (
6+
DataPart,
7+
FilePart,
8+
FileWithBytes,
9+
FileWithUri,
10+
Part,
11+
TextPart,
12+
)
13+
14+
15+
def get_text_parts(parts: list[Part]) -> list[str]:
16+
"""Extracts text content from all TextPart objects in a list of Parts.
17+
18+
Args:
19+
parts: A list of `Part` objects.
20+
21+
Returns:
22+
A list of strings containing the text content from any `TextPart` objects found.
23+
"""
24+
return [part.root.text for part in parts if isinstance(part.root, TextPart)]
25+
26+
27+
def get_data_parts(parts: list[Part]) -> list[dict[str, Any]]:
28+
"""Extracts dictionary data from all DataPart objects in a list of Parts.
29+
30+
Args:
31+
parts: A list of `Part` objects.
32+
33+
Returns:
34+
A list of dictionaries containing the data from any `DataPart` objects found.
35+
"""
36+
return [part.root.data for part in parts if isinstance(part.root, DataPart)]
37+
38+
39+
def get_file_parts(parts: list[Part]) -> list[FileWithBytes | FileWithUri]:
40+
"""Extracts file data from all FilePart objects in a list of Parts.
41+
42+
Args:
43+
parts: A list of `Part` objects.
44+
45+
Returns:
46+
A list of `FileWithBytes` or `FileWithUri` objects containing the file data from any `FilePart` objects found.
47+
"""
48+
return [part.root.file for part in parts if isinstance(part.root, FilePart)]

tests/server/apps/jsonrpc/test_serialization.py

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,30 +122,18 @@ def test_handle_oversized_payload(agent_card_with_api_key: AgentCard):
122122
app_instance = A2AStarletteApplication(agent_card_with_api_key, handler)
123123
client = TestClient(app_instance.build())
124124

125-
large_string = 'a' * 2_000_000 # 2MB string
125+
large_string = 'a' * 11 * 1_000_000 # 11MB string
126126
payload = {
127127
'jsonrpc': '2.0',
128128
'method': 'test',
129129
'id': 1,
130130
'params': {'data': large_string},
131131
}
132132

133-
# Starlette/FastAPI's default max request size is around 1MB.
134-
# This test will likely fail with a 413 Payload Too Large if the default is not increased.
135-
# If the application is expected to handle larger payloads, the server configuration needs to be adjusted.
136-
# For this test, we expect a 413 or a graceful JSON-RPC error if the app handles it.
137-
138-
try:
139-
response = client.post('/', json=payload)
140-
# If the app handles it gracefully and returns a JSON-RPC error
141-
if response.status_code == 200:
142-
data = response.json()
143-
assert data['error']['code'] == InvalidRequestError().code
144-
else:
145-
assert response.status_code == 413
146-
except Exception as e:
147-
# Depending on server setup, it might just drop the connection for very large payloads
148-
assert isinstance(e, ConnectionResetError | RuntimeError)
133+
response = client.post('/', json=payload)
134+
assert response.status_code == 200
135+
data = response.json()
136+
assert data['error']['code'] == InvalidRequestError().code
149137

150138

151139
def test_handle_unicode_characters(agent_card_with_api_key: AgentCard):

tests/utils/test_artifact.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33

44
from unittest.mock import patch
55

6-
from a2a.types import DataPart, Part, TextPart
6+
from a2a.types import (
7+
Artifact,
8+
DataPart,
9+
Part,
10+
TextPart,
11+
)
712
from a2a.utils.artifact import (
13+
get_artifact_text,
814
new_artifact,
915
new_data_artifact,
1016
new_text_artifact,
@@ -32,7 +38,7 @@ def test_new_artifact_empty_description_if_not_provided(self):
3238
parts = [Part(root=TextPart(text='Another sample'))]
3339
name = 'Artifact_No_Desc'
3440
artifact = new_artifact(parts=parts, name=name)
35-
self.assertEqual(artifact.description, '')
41+
self.assertEqual(artifact.description, None)
3642

3743
def test_new_text_artifact_creates_single_text_part(self):
3844
text = 'This is a text artifact.'
@@ -83,5 +89,71 @@ def test_new_data_artifact_assigns_name_description(self):
8389
self.assertEqual(artifact.description, description)
8490

8591

92+
class TestGetArtifactText(unittest.TestCase):
93+
def test_get_artifact_text_single_part(self):
94+
# Setup
95+
artifact = Artifact(
96+
name='test-artifact',
97+
parts=[Part(root=TextPart(text='Hello world'))],
98+
artifact_id='test-artifact-id',
99+
)
100+
101+
# Exercise
102+
result = get_artifact_text(artifact)
103+
104+
# Verify
105+
assert result == 'Hello world'
106+
107+
def test_get_artifact_text_multiple_parts(self):
108+
# Setup
109+
artifact = Artifact(
110+
name='test-artifact',
111+
parts=[
112+
Part(root=TextPart(text='First line')),
113+
Part(root=TextPart(text='Second line')),
114+
Part(root=TextPart(text='Third line')),
115+
],
116+
artifact_id='test-artifact-id',
117+
)
118+
119+
# Exercise
120+
result = get_artifact_text(artifact)
121+
122+
# Verify - default delimiter is newline
123+
assert result == 'First line\nSecond line\nThird line'
124+
125+
def test_get_artifact_text_custom_delimiter(self):
126+
# Setup
127+
artifact = Artifact(
128+
name='test-artifact',
129+
parts=[
130+
Part(root=TextPart(text='First part')),
131+
Part(root=TextPart(text='Second part')),
132+
Part(root=TextPart(text='Third part')),
133+
],
134+
artifact_id='test-artifact-id',
135+
)
136+
137+
# Exercise
138+
result = get_artifact_text(artifact, delimiter=' | ')
139+
140+
# Verify
141+
assert result == 'First part | Second part | Third part'
142+
143+
def test_get_artifact_text_empty_parts(self):
144+
# Setup
145+
artifact = Artifact(
146+
name='test-artifact',
147+
parts=[],
148+
artifact_id='test-artifact-id',
149+
)
150+
151+
# Exercise
152+
result = get_artifact_text(artifact)
153+
154+
# Verify
155+
assert result == ''
156+
157+
86158
if __name__ == '__main__':
87159
unittest.main()

0 commit comments

Comments
 (0)