Skip to content

Commit f7461fc

Browse files
authored
Merge branch 'main' into auto-update-a2a-types-0a9f629e801d4ae89f94991fc28afe9429c91cbc
2 parents a57f24a + 5268218 commit f7461fc

30 files changed

+943
-557
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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/bin/bash
2+
3+
# Exit immediately if a command exits with a non-zero status.
4+
# Treat unset variables as an error.
5+
set -euo pipefail
6+
7+
A2A_SPEC_REPO="https://github.com/a2aproject/A2A.git" # URL for the A2A spec repo.
8+
A2A_SPEC_BRANCH="main" # Name of the branch with experimental changes.
9+
FEATURE_BRANCH="experimental-types" # Name of the feature branch to create.
10+
ROOT_DIR=$(git rev-parse --show-toplevel)
11+
12+
usage() {
13+
cat <<EOF
14+
Usage: $0 [OPTIONS]
15+
16+
Creates a new feature branch with types generated from unmerged A2A spec changes.
17+
18+
This script clones the A2A spec repository, checks out a specific branch,
19+
and creates a new local feature branch from it.
20+
21+
The script requires uv and buf to be installed.
22+
23+
OPTIONS:
24+
-r, --spec-repo URL for the A2A spec repository.
25+
(Default: "$A2A_SPEC_REPO")
26+
27+
-b, --spec-branch Name of the branch with the experimental changes.
28+
(Default: "$A2A_SPEC_BRANCH")
29+
30+
-f, --feature-branch Name of the new feature branch to create.
31+
(Default: "$FEATURE_BRANCH")
32+
33+
-h, --help Display this help message and exit.
34+
35+
EXAMPLE:
36+
# Run with all default settings:
37+
$0
38+
39+
# Run with custom settings:
40+
$0 -r "https://github.com/spec-fork/A2A.git" -b "spec-change" -f "my-branch"
41+
EOF
42+
}
43+
44+
# Handle command-line arguments.
45+
while [[ $# -gt 0 ]]; do
46+
case $1 in
47+
-h|--help)
48+
usage
49+
exit 0
50+
;;
51+
-r|--spec-repo)
52+
A2A_SPEC_REPO="$2"
53+
shift 2
54+
;;
55+
-b|--spec-branch)
56+
A2A_SPEC_BRANCH="$2"
57+
shift 2
58+
;;
59+
-f|--feature-branch)
60+
FEATURE_BRANCH="$2"
61+
shift 2
62+
;;
63+
*)
64+
echo "Error: Unknown option '$1'" >&2
65+
usage
66+
exit 1
67+
;;
68+
esac
69+
done
70+
71+
72+
TMP_WORK_DIR=$(mktemp -d)
73+
echo "Created a temporary working directory: $TMP_WORK_DIR"
74+
trap 'rm -rf -- "$TMP_WORK_DIR"' EXIT
75+
cd $TMP_WORK_DIR
76+
77+
echo "Cloning the \"$A2A_SPEC_REPO\" repository..."
78+
git clone $A2A_SPEC_REPO spec_repo
79+
cd spec_repo
80+
81+
echo "Checking out the \"$A2A_SPEC_BRANCH\" branch..."
82+
git checkout "$A2A_SPEC_BRANCH"
83+
84+
echo "Invoking the generate_types.sh script..."
85+
GENERATED_FILE="$ROOT_DIR/src/a2a/types.py"
86+
$ROOT_DIR/scripts/generate_types.sh "$GENERATED_FILE" --input-file "$TMP_WORK_DIR/spec_repo/specification/json/a2a.json"
87+
88+
89+
echo "Running buf generate..."
90+
cd "$ROOT_DIR"
91+
buf generate
92+
uv run "$ROOT_DIR/scripts/grpc_gen_post_processor.py"
93+
94+
95+
echo "Committing generated types file to the \"$FEATURE_BRANCH\" branch..."
96+
git checkout -b "$FEATURE_BRANCH"
97+
git add "$GENERATED_FILE" "$ROOT_DIR/src/a2a/grpc"
98+
git commit -m "Experimental types"

scripts/generate_types.sh

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,44 @@
44
# Treat unset variables as an error.
55
set -euo pipefail
66

7-
# Check if an output file path was provided as an argument.
8-
if [ -z "$1" ]; then
9-
echo "Error: Output file path must be provided as the first argument." >&2
7+
REMOTE_URL="https://raw.githubusercontent.com/a2aproject/A2A/refs/heads/main/specification/json/a2a.json"
8+
9+
GENERATED_FILE=""
10+
INPUT_FILE=""
11+
12+
# Parse command-line arguments
13+
while [[ $# -gt 0 ]]; do
14+
case "$1" in
15+
--input-file)
16+
INPUT_FILE="$2"
17+
shift 2
18+
;;
19+
*)
20+
GENERATED_FILE="$1"
21+
shift 1
22+
;;
23+
esac
24+
done
25+
26+
if [ -z "$GENERATED_FILE" ]; then
27+
echo "Error: Output file path must be provided." >&2
28+
echo "Usage: $0 [--input-file <path>] <output-file-path>"
1029
exit 1
1130
fi
1231

13-
REMOTE_URL="https://raw.githubusercontent.com/a2aproject/A2A/refs/heads/main/specification/json/a2a.json"
14-
GENERATED_FILE="$1"
15-
1632
echo "Running datamodel-codegen..."
17-
echo " - Source URL: $REMOTE_URL"
33+
declare -a source_args
34+
if [ -n "$INPUT_FILE" ]; then
35+
echo " - Source File: $INPUT_FILE"
36+
source_args=("--input" "$INPUT_FILE")
37+
else
38+
echo " - Source URL: $REMOTE_URL"
39+
source_args=("--url" "$REMOTE_URL")
40+
fi
1841
echo " - Output File: $GENERATED_FILE"
1942

2043
uv run datamodel-codegen \
21-
--url "$REMOTE_URL" \
44+
"${source_args[@]}" \
2245
--input-file-type jsonschema \
2346
--output "$GENERATED_FILE" \
2447
--target-python-version 3.10 \

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

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

95-
MAX_CONTENT_LENGTH = 1_000_000
95+
MAX_CONTENT_LENGTH = 10_000_000
9696

9797

9898
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/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
1. Run the tests
44
```bash
5-
uv run pytest -v -s client/test_client.py
5+
uv run pytest -v -s client/test_client_factory.py
66
```
77

88
In case of failures, you can cleanup the cache:

0 commit comments

Comments
 (0)