Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/migrations/v1_0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Constructing messages is simplified in v1.0. The old API required wrapping conte
**Note**:
* When using `File (bytes)` in v1.0, data serialization (via base64 encoding) is not required because A2A now uses Protobuf, which handles it automatically.
* In v1.0, `Part.DataPart.data` is renamed to `Part.data` and is of type `google.protobuf.Value`. Use `ParseDict` to convert a Python dict into a suitable value. See the examples below for more details.
* `google.protobuf.Value` stores every number as a double, so whole numbers in a data Part are read back as floats on every transport (`4326` becomes `4326.0`). This is a fidelity loss, not a spec violation, since JSON does not distinguish the two. On the receiving side, narrow floats for fields you model as integers (for example with `float.is_integer()`) and leave other fields alone.

**Before (v0.3):**
```python
Expand Down
10 changes: 10 additions & 0 deletions src/a2a/helpers/proto_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@ def new_data_part(
) -> Part:
"""Creates a Part with structured data (google.protobuf.Value).

Note:
``google.protobuf.Value`` stores every number as a double, so whole
numbers lose their integer type here and are read back as floats
(``4326`` becomes ``4326.0``). See ``get_data_parts``.

Args:
data: JSON-serializable data to embed (dict, list, str, etc.).
media_type: Optional MIME type of the part content (e.g., "text/plain", "application/json", "image/png").
Expand Down Expand Up @@ -407,6 +412,11 @@ def get_data_parts(parts: Sequence[Part]) -> list[Any]:
Each returned element is the Python object obtained by converting
the ``google.protobuf.Value`` back via ``MessageToDict``.

Note:
Whole numbers come back as floats (``4326`` becomes ``4326.0``)
because ``google.protobuf.Value`` stores numbers as doubles. For
fields you model as integers, narrow floats with ``is_integer()``.

Args:
parts: A sequence of ``Part`` objects.

Expand Down
11 changes: 11 additions & 0 deletions tests/helpers/test_proto_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,17 @@ def test_get_data_parts_empty() -> None:
assert get_data_parts(parts) == []


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please "invert" the tests logic. Assuring that int is converted to float marks it as expected behaviour in SDK (but it isn't)

Instead, write a test which ensures that int is converted to int and mark it as @pytests.mark.xfail (we expect it to fail) with strict flag set to True

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The test now asserts that integers stay integers and is marked xfail with strict=True, with the issue URL as the reason. I also changed the PR body from Fixes to Refs, since the test treats the issue as still open.

@pytest.mark.xfail(
reason='https://github.com/a2aproject/a2a-python/issues/1328',
strict=True,
)
def test_get_data_parts_keeps_whole_numbers_as_ints() -> None:
result = get_data_parts([new_data_part({'id': 4326, 'items': [240]})])
assert result == [{'id': 4326, 'items': [240]}]
assert isinstance(result[0]['id'], int)
assert isinstance(result[0]['items'][0], int)


def test_get_raw_parts() -> None:
parts = [
Part(raw=b'\x89PNG'),
Expand Down
Loading