Skip to content

Commit 9155888

Browse files
refactor(utils): move part helpers & add artifact text extractor (#517)
# Description This pull request refactors utility functions for handling `Part` objects by moving them from `src/a2a/utils/message.py` to a new dedicated module `src/a2a/utils/parts.py`. It also introduces a new helper function for extracting text from `Artifacts` and updates imports and exports to reflect these changes. The goal is to improve code organization and clarity by grouping similar functionality. **Refactoring and organization:** * Moved the functions `get_text_parts`, `get_data_parts`, and `get_file_parts` from `src/a2a/utils/message.py` into a new module `src/a2a/utils/parts.py`, providing better separation of concerns for part-handling utilities. [[1]](diffhunk://#diff-75cd067f29c32392a29e62de13b907467f6ed491a66bfdb56cdae0eafe70b2fdL67-L102) [[2]](diffhunk://#diff-02db7e35da2780f67c1bf288d503c6f7702e1f85542023b230bd798cee46cb21R1-R50) * Updated imports in `src/a2a/utils/__init__.py` to re-export these part-handling functions from the new `parts` module, and removed their previous import from the `message` module. [[1]](diffhunk://#diff-58d75f7b41efab15332246cd45437bba43a3bed62236bd5a9f8dfb50991f849aR7-R17) [[2]](diffhunk://#diff-58d75f7b41efab15332246cd45437bba43a3bed62236bd5a9f8dfb50991f849aL20-L27) [[3]](diffhunk://#diff-58d75f7b41efab15332246cd45437bba43a3bed62236bd5a9f8dfb50991f849aR47) **New functionality:** * Added the new function `get_artifact_text` to `src/a2a/utils/artifact.py` for extracting and joining all text content from an artifact's parts, using the refactored `get_text_parts`. [[1]](diffhunk://#diff-c47436a54fc84fd5eb8eed08c4e86fdb76e4a5a753191393712ae86f2b8f0f04R74-R86) [[2]](diffhunk://#diff-c47436a54fc84fd5eb8eed08c4e86fdb76e4a5a753191393712ae86f2b8f0f04R8) [[3]](diffhunk://#diff-58d75f7b41efab15332246cd45437bba43a3bed62236bd5a9f8dfb50991f849aR7-R17) [[4]](diffhunk://#diff-58d75f7b41efab15332246cd45437bba43a3bed62236bd5a9f8dfb50991f849aR47) These changes improve maintainability by clearly separating message-related and part-related utilities, and by introducing a helper for artifact text extraction. --- BEGIN_COMMIT_OVERRIDE refactor(utils): move part helpers to their own file feat: add `get_artifact_text()` helper method Release-As: 0.3.10 END_COMMIT_OVERRIDE
1 parent 5b81385 commit 9155888

File tree

7 files changed

+327
-223
lines changed

7 files changed

+327
-223
lines changed

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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
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(
@@ -70,3 +71,16 @@ def new_data_artifact(
7071
name,
7172
description,
7273
)
74+
75+
76+
def get_artifact_text(artifact: Artifact, delimiter: str = '\n') -> str:
77+
"""Extracts and joins all text content from an Artifact's parts.
78+
79+
Args:
80+
artifact: The `Artifact` object.
81+
delimiter: The string to use when joining text from multiple TextParts.
82+
83+
Returns:
84+
A single string containing all text content, or an empty string if no text parts are found.
85+
"""
86+
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/utils/test_artifact.py

Lines changed: 73 additions & 1 deletion
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,
@@ -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)