distributed-a2a 0.2.3__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {distributed_a2a-0.2.3/distributed_a2a.egg-info → distributed_a2a-0.2.5}/PKG-INFO +9 -6
  2. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/file_extractors.py +101 -28
  3. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/mcp_interceptors.py +2 -2
  4. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5/distributed_a2a.egg-info}/PKG-INFO +9 -6
  5. distributed_a2a-0.2.5/distributed_a2a.egg-info/requires.txt +20 -0
  6. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/pyproject.toml +1 -1
  7. distributed_a2a-0.2.5/requirements.txt +11 -0
  8. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/tests/test_mcp_interceptors.py +56 -4
  9. distributed_a2a-0.2.3/distributed_a2a.egg-info/requires.txt +0 -17
  10. distributed_a2a-0.2.3/requirements.txt +0 -8
  11. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/LICENSE +0 -0
  12. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/MANIFEST.in +0 -0
  13. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/README.md +0 -0
  14. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/__init__.py +0 -0
  15. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/agent.py +0 -0
  16. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/client.py +0 -0
  17. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/config.py +0 -0
  18. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/executors.py +0 -0
  19. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/model.py +0 -0
  20. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/py.typed +0 -0
  21. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/registry.py +0 -0
  22. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/registry_server/__init__.py +0 -0
  23. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/registry_server/bootstrap.py +0 -0
  24. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/registry_server/dynamo_db.py +0 -0
  25. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/registry_server/in_memory_registry_storage.py +0 -0
  26. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/registry_server/model.py +0 -0
  27. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/registry_server/storage.py +0 -0
  28. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/router.py +0 -0
  29. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/schemas/agent-schema.json +0 -0
  30. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/schemas/router-agent-schema.json +0 -0
  31. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a/server.py +0 -0
  32. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a.egg-info/SOURCES.txt +0 -0
  33. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a.egg-info/dependency_links.txt +0 -0
  34. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/distributed_a2a.egg-info/top_level.txt +0 -0
  35. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/setup.cfg +0 -0
  36. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/setup.py +0 -0
  37. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/tests/test_app.py +0 -0
  38. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/tests/test_client.py +0 -0
  39. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/tests/test_executor_files.py +0 -0
  40. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/tests/test_file_extractors.py +0 -0
  41. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/tests/test_rejection.py +0 -0
  42. {distributed_a2a-0.2.3 → distributed_a2a-0.2.5}/tests/test_timeout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: distributed_a2a
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: A library for building A2A agents with routing capabilities
5
5
  Home-page: https://github.com/Barra-Technologies/distributed-a2a
6
6
  Author: Fabian Bell
@@ -17,13 +17,16 @@ Classifier: Programming Language :: Python :: 3.14
17
17
  Requires-Python: >=3.14
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: uvicorn==0.41.0
20
+ Requires-Dist: uvicorn==0.49.0
21
21
  Requires-Dist: a2a-sdk==0.3.24
22
- Requires-Dist: langchain==1.3.9
23
- Requires-Dist: langchain_mcp_adapters==0.2.1
24
- Requires-Dist: langchain_openai==1.1.10
22
+ Requires-Dist: langchain==1.3.11
23
+ Requires-Dist: langchain-core==1.4.8
24
+ Requires-Dist: langchain_mcp_adapters==0.3.0
25
+ Requires-Dist: langchain_openai==1.3.3
26
+ Requires-Dist: langgraph==1.2.7
27
+ Requires-Dist: langgraph-checkpoint==4.1.1
25
28
  Requires-Dist: langgraph_dynamodb_checkpoint==0.2.6.4
26
- Requires-Dist: fastapi==0.135.1
29
+ Requires-Dist: fastapi==0.139.0
27
30
  Requires-Dist: pyyaml==6.0.3
28
31
  Provides-Extra: test
29
32
  Requires-Dist: pytest==9.0.2; extra == "test"
@@ -48,37 +48,110 @@ def _extract_from_mcp_blocks(blocks: list[Any]) -> list[tuple[str, FilePart]]:
48
48
  out: list[tuple[str, FilePart]] = []
49
49
  counters: dict[str, int] = {"attachment": 0, "image": 0}
50
50
  for block in blocks:
51
- if isinstance(block, EmbeddedResource) and isinstance(block.resource, BlobResourceContents):
52
- mime_type = block.resource.mimeType or "application/octet-stream"
53
- uri = str(block.resource.uri) if block.resource.uri is not None else ""
54
- kind = "image" if mime_type.startswith("image/") else "attachment"
55
- if uri:
56
- name = _name_from_uri(uri, kind, counters[kind], mime_type)
57
- else:
58
- name = _synthetic_name(kind, counters[kind], mime_type)
59
- counters[kind] += 1
60
- out.append((name, FilePart(file=FileWithBytes(
61
- name=name, mime_type=mime_type, bytes=block.resource.blob,
62
- ))))
63
- elif isinstance(block, ImageContent):
64
- mime_type = block.mimeType or "application/octet-stream"
65
- name = _synthetic_name("image", counters["image"], mime_type)
66
- counters["image"] += 1
67
- out.append((name, FilePart(file=FileWithBytes(
68
- name=name, mime_type=mime_type, bytes=block.data,
69
- ))))
70
- elif isinstance(block, ResourceLink):
71
- mime_type = block.mimeType or "application/octet-stream"
72
- uri = str(block.uri)
73
- kind = "image" if mime_type.startswith("image/") else "attachment"
74
- name = _name_from_uri(uri, kind, counters[kind], mime_type)
75
- counters[kind] += 1
76
- out.append((name, FilePart(file=FileWithUri(
77
- name=name, mime_type=mime_type, uri=uri,
78
- ))))
51
+ result = _mcp_block_to_file_part(block, counters)
52
+ if result is not None:
53
+ out.append(result)
79
54
  return out
80
55
 
81
56
 
57
+ def _mcp_block_to_file_part(
58
+ block: Any, counters: dict[str, int],
59
+ ) -> tuple[str, FilePart] | None:
60
+ # Normalise both shapes into (type_tag, extractor-friendly view).
61
+ if isinstance(block, EmbeddedResource) and isinstance(
62
+ block.resource, BlobResourceContents,
63
+ ):
64
+ mime_type = block.resource.mimeType or "application/octet-stream"
65
+ uri = str(block.resource.uri) if block.resource.uri is not None else ""
66
+ return _blob_resource_to_file_part(
67
+ mime_type, uri, block.resource.blob, counters,
68
+ )
69
+ if isinstance(block, ImageContent):
70
+ return _image_bytes_to_file_part(
71
+ block.mimeType or "application/octet-stream", block.data, counters,
72
+ )
73
+ if isinstance(block, ResourceLink):
74
+ return _resource_link_to_file_part(
75
+ block.mimeType or "application/octet-stream",
76
+ str(block.uri),
77
+ counters,
78
+ )
79
+
80
+ if not isinstance(block, dict):
81
+ return None
82
+
83
+ block_type = block.get("type")
84
+ if block_type == "resource":
85
+ resource = block.get("resource")
86
+ if not isinstance(resource, dict):
87
+ return None
88
+ blob = resource.get("blob")
89
+ if not isinstance(blob, str) or not blob:
90
+ return None
91
+ mime_type_str = _mime_type_or_default(resource.get("mimeType"))
92
+ raw_uri = resource.get("uri")
93
+ uri_str = str(raw_uri) if raw_uri else ""
94
+ return _blob_resource_to_file_part(
95
+ mime_type_str, uri_str, blob, counters,
96
+ )
97
+ if block_type == "image":
98
+ data = block.get("data")
99
+ if not isinstance(data, str) or not data:
100
+ return None
101
+ mime_type_str = _mime_type_or_default(block.get("mimeType"))
102
+ return _image_bytes_to_file_part(mime_type_str, data, counters)
103
+ if block_type == "resource_link":
104
+ raw_uri = block.get("uri")
105
+ if not raw_uri:
106
+ return None
107
+ mime_type_str = _mime_type_or_default(block.get("mimeType"))
108
+ return _resource_link_to_file_part(
109
+ mime_type_str, str(raw_uri), counters,
110
+ )
111
+ return None
112
+
113
+
114
+ def _mime_type_or_default(value: Any) -> str:
115
+ if isinstance(value, str) and value:
116
+ return value
117
+ return "application/octet-stream"
118
+
119
+
120
+ def _blob_resource_to_file_part(
121
+ mime_type: str, uri: str, blob: str, counters: dict[str, int],
122
+ ) -> tuple[str, FilePart]:
123
+ kind = "image" if mime_type.startswith("image/") else "attachment"
124
+ if uri:
125
+ name = _name_from_uri(uri, kind, counters[kind], mime_type)
126
+ else:
127
+ name = _synthetic_name(kind, counters[kind], mime_type)
128
+ counters[kind] += 1
129
+ return name, FilePart(file=FileWithBytes(
130
+ name=name, mime_type=mime_type, bytes=blob,
131
+ ))
132
+
133
+
134
+ def _image_bytes_to_file_part(
135
+ mime_type: str, data: str, counters: dict[str, int],
136
+ ) -> tuple[str, FilePart]:
137
+ name = _synthetic_name("image", counters["image"], mime_type)
138
+ counters["image"] += 1
139
+ return name, FilePart(file=FileWithBytes(
140
+ name=name, mime_type=mime_type, bytes=data,
141
+ ))
142
+
143
+
144
+ def _resource_link_to_file_part(
145
+ mime_type: str, uri: str, counters: dict[str, int],
146
+ ) -> tuple[str, FilePart]:
147
+ kind = "image" if mime_type.startswith("image/") else "attachment"
148
+ name = _name_from_uri(uri, kind, counters[kind], mime_type)
149
+ counters[kind] += 1
150
+ return name, FilePart(file=FileWithUri(
151
+ name=name, mime_type=mime_type, uri=uri,
152
+ ))
153
+
154
+
82
155
  def _extract_from_langchain_content_blocks(content: list[Any]) -> list[tuple[str, FilePart]]:
83
156
  out: list[tuple[str, FilePart]] = []
84
157
  pending_name: str | None = None
@@ -26,12 +26,12 @@ async def hide_binary_content_from_llm(
26
26
  return result
27
27
 
28
28
  text_blocks: list[TextContent] = []
29
- non_text_blocks: list[Any] = []
29
+ non_text_blocks: list[dict[str, Any]] = []
30
30
  for block in result.content:
31
31
  if isinstance(block, TextContent):
32
32
  text_blocks.append(block)
33
33
  else:
34
- non_text_blocks.append(block)
34
+ non_text_blocks.append(block.model_dump(mode="json"))
35
35
 
36
36
  merged_structured: dict[str, Any] = (
37
37
  dict(result.structuredContent) if result.structuredContent else {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: distributed_a2a
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: A library for building A2A agents with routing capabilities
5
5
  Home-page: https://github.com/Barra-Technologies/distributed-a2a
6
6
  Author: Fabian Bell
@@ -17,13 +17,16 @@ Classifier: Programming Language :: Python :: 3.14
17
17
  Requires-Python: >=3.14
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: uvicorn==0.41.0
20
+ Requires-Dist: uvicorn==0.49.0
21
21
  Requires-Dist: a2a-sdk==0.3.24
22
- Requires-Dist: langchain==1.3.9
23
- Requires-Dist: langchain_mcp_adapters==0.2.1
24
- Requires-Dist: langchain_openai==1.1.10
22
+ Requires-Dist: langchain==1.3.11
23
+ Requires-Dist: langchain-core==1.4.8
24
+ Requires-Dist: langchain_mcp_adapters==0.3.0
25
+ Requires-Dist: langchain_openai==1.3.3
26
+ Requires-Dist: langgraph==1.2.7
27
+ Requires-Dist: langgraph-checkpoint==4.1.1
25
28
  Requires-Dist: langgraph_dynamodb_checkpoint==0.2.6.4
26
- Requires-Dist: fastapi==0.135.1
29
+ Requires-Dist: fastapi==0.139.0
27
30
  Requires-Dist: pyyaml==6.0.3
28
31
  Provides-Extra: test
29
32
  Requires-Dist: pytest==9.0.2; extra == "test"
@@ -0,0 +1,20 @@
1
+ uvicorn==0.49.0
2
+ a2a-sdk==0.3.24
3
+ langchain==1.3.11
4
+ langchain-core==1.4.8
5
+ langchain_mcp_adapters==0.3.0
6
+ langchain_openai==1.3.3
7
+ langgraph==1.2.7
8
+ langgraph-checkpoint==4.1.1
9
+ langgraph_dynamodb_checkpoint==0.2.6.4
10
+ fastapi==0.139.0
11
+ pyyaml==6.0.3
12
+
13
+ [ci]
14
+ toml-cli==0.8.2
15
+
16
+ [test]
17
+ pytest==9.0.2
18
+ pytest-asyncio==1.3.0
19
+ toml-cli==0.8.2
20
+ mypy==1.19.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "distributed_a2a"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "A library for building A2A agents with routing capabilities"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -0,0 +1,11 @@
1
+ uvicorn==0.49.0
2
+ a2a-sdk==0.3.24
3
+ langchain==1.3.11
4
+ langchain-core==1.4.8
5
+ langchain_mcp_adapters==0.3.0
6
+ langchain_openai==1.3.3
7
+ langgraph==1.2.7
8
+ langgraph-checkpoint==4.1.1
9
+ langgraph_dynamodb_checkpoint==0.2.6.4
10
+ fastapi==0.139.0
11
+ pyyaml==6.0.3
@@ -7,6 +7,7 @@ from langchain_core.messages import ToolMessage
7
7
  from langchain_core.tools import StructuredTool
8
8
  from langchain_mcp_adapters.interceptors import MCPToolCallRequest
9
9
  from langchain_mcp_adapters.tools import _convert_call_tool_result
10
+ from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
10
11
  from mcp.types import (BlobResourceContents, CallToolResult, EmbeddedResource,
11
12
  ImageContent, TextContent)
12
13
  from pydantic import AnyUrl
@@ -72,7 +73,11 @@ async def test_mixed_result_moves_binary_into_structured_content() -> None:
72
73
  assert isinstance(result.content[0], TextContent)
73
74
  assert result.content[0].text == '{"filename": "alice.docx"}'
74
75
  assert result.structuredContent is not None
75
- assert result.structuredContent[NON_TEXT_CONTENT_KEY] == [embedded]
76
+ # Non-text blocks are stored as JSON-safe dicts (not raw pydantic
77
+ # instances) so downstream LangGraph msgpack checkpointing works.
78
+ assert result.structuredContent[NON_TEXT_CONTENT_KEY] == [
79
+ embedded.model_dump(mode="json"),
80
+ ]
76
81
 
77
82
 
78
83
  @pytest.mark.asyncio
@@ -89,7 +94,9 @@ async def test_binary_only_result_produces_empty_content_list() -> None:
89
94
  "Binary-only tool output should leave content empty — the model "
90
95
  "receives no text, and the block is only reachable via artifact."
91
96
  )
92
- assert result.structuredContent == {NON_TEXT_CONTENT_KEY: [embedded]}
97
+ assert result.structuredContent == {
98
+ NON_TEXT_CONTENT_KEY: [embedded.model_dump(mode="json")],
99
+ }
93
100
 
94
101
 
95
102
  @pytest.mark.asyncio
@@ -129,7 +136,7 @@ async def test_preserves_existing_structured_content() -> None:
129
136
  assert result.structuredContent == {
130
137
  "foo": 1,
131
138
  "nested": {"bar": 2},
132
- NON_TEXT_CONTENT_KEY: [embedded],
139
+ NON_TEXT_CONTENT_KEY: [embedded.model_dump(mode="json")],
133
140
  }
134
141
  assert original.structuredContent == {"foo": 1, "nested": {"bar": 2}}
135
142
 
@@ -151,7 +158,9 @@ async def test_image_content_is_hidden() -> None:
151
158
  result = await hide_binary_content_from_llm(_request(), _make_handler(original))
152
159
  assert isinstance(result, CallToolResult)
153
160
  assert result.content == [TextContent(type="text", text="see below")]
154
- assert result.structuredContent == {NON_TEXT_CONTENT_KEY: [image]}
161
+ assert result.structuredContent == {
162
+ NON_TEXT_CONTENT_KEY: [image.model_dump(mode="json")],
163
+ }
155
164
 
156
165
 
157
166
  def test_adapter_forwards_structured_content_into_tool_message_artifact() -> None:
@@ -195,3 +204,46 @@ def test_base_tool_invoke_sets_tool_call_id_when_content_is_not_tool_message() -
195
204
  assert result.artifact == {
196
205
  "structured_content": {NON_TEXT_CONTENT_KEY: ["placeholder"]},
197
206
  }
207
+
208
+
209
+ @pytest.mark.asyncio
210
+ async def test_tool_message_from_interceptor_is_msgpack_serializable() -> None:
211
+ embedded = _embedded_docx()
212
+ image = ImageContent(type="image", data="AA==", mimeType="image/png")
213
+ original = CallToolResult(
214
+ content=[
215
+ TextContent(type="text", text='{"filename": "alice.docx"}'),
216
+ embedded,
217
+ image,
218
+ ],
219
+ isError=False,
220
+ )
221
+ stashed = await hide_binary_content_from_llm(
222
+ _request(), _make_handler(original),
223
+ )
224
+ assert isinstance(stashed, CallToolResult)
225
+
226
+ content, artifact = _convert_call_tool_result(stashed)
227
+ tool_msg = ToolMessage(
228
+ content=content, # type: ignore[arg-type]
229
+ tool_call_id="call-1",
230
+ name="render_file",
231
+ artifact=artifact,
232
+ )
233
+
234
+ serde = JsonPlusSerializer()
235
+ kind, blob = serde.dumps_typed({"messages": [tool_msg]})
236
+ assert kind == "msgpack"
237
+ loaded = serde.loads_typed((kind, blob))
238
+ assert isinstance(loaded, dict)
239
+ loaded_messages = loaded["messages"]
240
+ assert len(loaded_messages) == 1
241
+ assert isinstance(loaded_messages[0], ToolMessage)
242
+ assert loaded_messages[0].tool_call_id == "call-1"
243
+ assert loaded_messages[0].artifact is not None
244
+ non_text = loaded_messages[0].artifact["structured_content"][
245
+ NON_TEXT_CONTENT_KEY
246
+ ]
247
+ assert len(non_text) == 2
248
+ assert non_text[0]["type"] == "resource"
249
+ assert non_text[1]["type"] == "image"
@@ -1,17 +0,0 @@
1
- uvicorn==0.41.0
2
- a2a-sdk==0.3.24
3
- langchain==1.3.9
4
- langchain_mcp_adapters==0.2.1
5
- langchain_openai==1.1.10
6
- langgraph_dynamodb_checkpoint==0.2.6.4
7
- fastapi==0.135.1
8
- pyyaml==6.0.3
9
-
10
- [ci]
11
- toml-cli==0.8.2
12
-
13
- [test]
14
- pytest==9.0.2
15
- pytest-asyncio==1.3.0
16
- toml-cli==0.8.2
17
- mypy==1.19.1
@@ -1,8 +0,0 @@
1
- uvicorn==0.41.0
2
- a2a-sdk==0.3.24
3
- langchain==1.3.9
4
- langchain_mcp_adapters==0.2.1
5
- langchain_openai==1.1.10
6
- langgraph_dynamodb_checkpoint==0.2.6.4
7
- fastapi==0.135.1
8
- pyyaml==6.0.3
File without changes