a2a-handler 0.1.24__tar.gz → 0.1.26__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.
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/PKG-INFO +5 -5
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/README.md +1 -1
- a2a_handler-0.1.26/pyproject.toml +95 -0
- a2a_handler-0.1.24/pyproject.toml → a2a_handler-0.1.26/pyproject.toml.orig +4 -4
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/_helpers.py +6 -10
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/card.py +2 -2
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/message.py +55 -51
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/mcp/server.py +2 -1
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/__init__.py +3 -7
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/app.py +18 -18
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/card.py +47 -23
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/service.py +253 -164
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/artifacts.py +3 -1
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/card.py +3 -2
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/messages.py +7 -17
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/tasks.py +58 -69
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/tab.py +11 -10
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/validation.py +62 -33
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/webhook.py +4 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/LICENSE +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/auth.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/mcp.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/schema.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/server.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/session.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/cli/task.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/config.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/dotenv.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/input_validation.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/logging.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/common/output.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/mcp/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/agent.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/ollama.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/tools/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/tools/a2a_docs.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/tools/handler_docs.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/server/tools/source.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/servers.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/session.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/app.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/app.tcss +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/commands/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/commands/palette.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/commands/screens.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/auth.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/headers.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/input.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/components/logs.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/__init__.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/save.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/session.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/tabs.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/types.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/views.py +0 -0
- {a2a_handler-0.1.24 → a2a_handler-0.1.26}/src/a2a_handler/tui/server/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-handler
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: An A2A Protocol client TUI and CLI.
|
|
5
5
|
Keywords: a2a,a2a-protocol,agent,agent-to-agent,ai,cli,tui
|
|
6
6
|
Author: Al Duncanson
|
|
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
|
-
Requires-Dist: a2a-sdk>=0.
|
|
21
|
+
Requires-Dist: a2a-sdk[http-server]>=1.0.0,<2.0.0
|
|
22
22
|
Requires-Dist: click>=8.0.0
|
|
23
|
-
Requires-Dist: google-adk>=2.
|
|
23
|
+
Requires-Dist: google-adk[mcp]>=2.5.0
|
|
24
24
|
Requires-Dist: httpx>=0.27.0
|
|
25
25
|
Requires-Dist: litellm>=1.0.0
|
|
26
26
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -28,7 +28,7 @@ Requires-Dist: rich>=13.0.0
|
|
|
28
28
|
Requires-Dist: textual>=0.47.0
|
|
29
29
|
Requires-Dist: uvicorn>=0.30.0
|
|
30
30
|
Requires-Dist: textual-serve>=1.1.0
|
|
31
|
-
Requires-Dist: mcp[cli]>=1.27.1
|
|
31
|
+
Requires-Dist: mcp[cli]>=1.27.1,<2
|
|
32
32
|
Requires-Dist: truststore>=0.9.0
|
|
33
33
|
Requires-Dist: platformdirs>=4.0.0
|
|
34
34
|
Requires-Python: >=3.11
|
|
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
|
|
|
42
42
|
# Handler
|
|
43
43
|
|
|
44
44
|
[](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
|
|
45
|
-
[](https://a2a-protocol.org/latest/)
|
|
46
46
|
[](https://pypi.org/project/a2a-handler/)
|
|
47
47
|
[](https://pypi.org/project/a2a-handler/)
|
|
48
48
|
[](https://pepy.tech/projects/a2a-handler)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Handler
|
|
2
2
|
|
|
3
3
|
[](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
|
|
4
|
-
[](https://a2a-protocol.org/latest/)
|
|
5
5
|
[](https://pypi.org/project/a2a-handler/)
|
|
6
6
|
[](https://pypi.org/project/a2a-handler/)
|
|
7
7
|
[](https://pepy.tech/projects/a2a-handler)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "a2a-handler"
|
|
3
|
+
version = "0.1.26"
|
|
4
|
+
description = "An A2A Protocol client TUI and CLI."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "GPL-3.0-or-later"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"a2a",
|
|
11
|
+
"a2a-protocol",
|
|
12
|
+
"agent",
|
|
13
|
+
"agent-to-agent",
|
|
14
|
+
"ai",
|
|
15
|
+
"cli",
|
|
16
|
+
"tui",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"a2a-sdk[http-server]>=1.0.0,<2.0.0",
|
|
33
|
+
"click>=8.0.0",
|
|
34
|
+
"google-adk[mcp]>=2.5.0",
|
|
35
|
+
"httpx>=0.27.0",
|
|
36
|
+
"litellm>=1.0.0",
|
|
37
|
+
"python-dotenv>=1.0.0",
|
|
38
|
+
"rich>=13.0.0",
|
|
39
|
+
"textual>=0.47.0",
|
|
40
|
+
"uvicorn>=0.30.0",
|
|
41
|
+
"textual-serve>=1.1.0",
|
|
42
|
+
"mcp[cli]>=1.27.1,<2",
|
|
43
|
+
"truststore>=0.9.0",
|
|
44
|
+
"platformdirs>=4.0.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[[project.authors]]
|
|
48
|
+
name = "Al Duncanson"
|
|
49
|
+
email = "alDuncanson@proton.me"
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/alDuncanson/handler"
|
|
53
|
+
Repository = "https://github.com/alDuncanson/handler"
|
|
54
|
+
Documentation = "https://github.com/alDuncanson/handler/tree/main/docs"
|
|
55
|
+
Issues = "https://github.com/alDuncanson/handler/issues"
|
|
56
|
+
Changelog = "https://github.com/alDuncanson/handler/releases"
|
|
57
|
+
|
|
58
|
+
[project.scripts]
|
|
59
|
+
handler = "a2a_handler.cli:main"
|
|
60
|
+
|
|
61
|
+
[dependency-groups]
|
|
62
|
+
dev = [
|
|
63
|
+
"ruff>=0.8.0",
|
|
64
|
+
"textual-dev>=1.8.0",
|
|
65
|
+
"pytest>=8.0.0",
|
|
66
|
+
"pytest-asyncio>=0.23.0",
|
|
67
|
+
"ty>=0.0.1a27",
|
|
68
|
+
"pytest-cov>=7.1.0",
|
|
69
|
+
"pytest-textual-snapshot>=1.0.0",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[build-system]
|
|
73
|
+
requires = ["uv_build>=0.9.15,<0.10.0"]
|
|
74
|
+
build-backend = "uv_build"
|
|
75
|
+
|
|
76
|
+
[tool.uv.build-backend]
|
|
77
|
+
module-root = "src"
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
asyncio_mode = "auto"
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.per-file-ignores]
|
|
83
|
+
"src/a2a_handler/cli/__init__.py" = ["E402"]
|
|
84
|
+
|
|
85
|
+
[tool.coverage.run]
|
|
86
|
+
source = ["a2a_handler"]
|
|
87
|
+
|
|
88
|
+
[tool.coverage.report]
|
|
89
|
+
show_missing = true
|
|
90
|
+
skip_empty = true
|
|
91
|
+
exclude_lines = [
|
|
92
|
+
"pragma: no cover",
|
|
93
|
+
"if TYPE_CHECKING:",
|
|
94
|
+
"if __name__ == .__main__.",
|
|
95
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.26"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -32,9 +32,9 @@ classifiers = [
|
|
|
32
32
|
"Typing :: Typed",
|
|
33
33
|
]
|
|
34
34
|
dependencies = [
|
|
35
|
-
"a2a-sdk>=0.
|
|
35
|
+
"a2a-sdk[http-server]>=1.0.0,<2.0.0",
|
|
36
36
|
"click>=8.0.0",
|
|
37
|
-
"google-adk>=2.
|
|
37
|
+
"google-adk[mcp]>=2.5.0",
|
|
38
38
|
"httpx>=0.27.0",
|
|
39
39
|
"litellm>=1.0.0",
|
|
40
40
|
"python-dotenv>=1.0.0",
|
|
@@ -42,7 +42,7 @@ dependencies = [
|
|
|
42
42
|
"textual>=0.47.0",
|
|
43
43
|
"uvicorn>=0.30.0",
|
|
44
44
|
"textual-serve>=1.1.0",
|
|
45
|
-
"mcp[cli]>=1.27.1",
|
|
45
|
+
"mcp[cli]>=1.27.1,<2",
|
|
46
46
|
"truststore>=0.9.0",
|
|
47
47
|
"platformdirs>=4.0.0",
|
|
48
48
|
]
|
|
@@ -8,8 +8,8 @@ import httpx
|
|
|
8
8
|
import click
|
|
9
9
|
from a2a.client.errors import (
|
|
10
10
|
A2AClientError,
|
|
11
|
-
A2AClientHTTPError,
|
|
12
11
|
A2AClientTimeoutError,
|
|
12
|
+
AgentCardResolutionError,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
from a2a_handler.auth import (
|
|
@@ -162,15 +162,11 @@ def handle_client_error(e: Exception, agent_url: str, output: Output | None) ->
|
|
|
162
162
|
message = "Request timed out"
|
|
163
163
|
error_code = "request_timeout"
|
|
164
164
|
suggestion = "Retry the request or increase timeout settings"
|
|
165
|
-
elif isinstance(e,
|
|
166
|
-
log.error("
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
suggestion = "Verify the agent URL and that the server is reachable"
|
|
171
|
-
else:
|
|
172
|
-
message = str(e)
|
|
173
|
-
error_code = "a2a_http_error"
|
|
165
|
+
elif isinstance(e, AgentCardResolutionError):
|
|
166
|
+
log.error("Agent card resolution failed for %s: %s", agent_url, e)
|
|
167
|
+
message = f"Could not resolve agent card: {e}"
|
|
168
|
+
error_code = "agent_card_resolution_error"
|
|
169
|
+
suggestion = "Verify the agent URL and that the server exposes an agent card"
|
|
174
170
|
details = {"agent_url": agent_url}
|
|
175
171
|
elif isinstance(e, A2AClientError):
|
|
176
172
|
log.error("A2A client error: %s", e)
|
|
@@ -8,7 +8,7 @@ from a2a.types import AgentCard
|
|
|
8
8
|
|
|
9
9
|
from a2a_handler.common import Output, get_logger
|
|
10
10
|
from a2a_handler.common.input_validation import InputValidationError, validate_agent_url
|
|
11
|
-
from a2a_handler.service import A2AService
|
|
11
|
+
from a2a_handler.service import A2AService, to_json_dict
|
|
12
12
|
from a2a_handler.validation import (
|
|
13
13
|
ValidationResult,
|
|
14
14
|
ValidationSource,
|
|
@@ -90,7 +90,7 @@ def _format_agent_card(card_data: object, output: Output) -> None:
|
|
|
90
90
|
"""Format and display an agent card as JSON."""
|
|
91
91
|
card_dict: dict[str, Any]
|
|
92
92
|
if isinstance(card_data, AgentCard):
|
|
93
|
-
card_dict = card_data
|
|
93
|
+
card_dict = to_json_dict(card_data)
|
|
94
94
|
elif isinstance(card_data, dict):
|
|
95
95
|
card_dict = {str(key): value for key, value in card_data.items()}
|
|
96
96
|
else:
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
+
from collections.abc import Sequence
|
|
5
6
|
from dataclasses import replace
|
|
6
7
|
from typing import Any
|
|
7
8
|
from typing import Optional
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
|
-
from a2a.types import
|
|
11
|
+
from a2a.types import Message, Part, Role
|
|
11
12
|
|
|
12
13
|
from a2a_handler.auth import (
|
|
13
14
|
AuthCredentials,
|
|
@@ -28,10 +29,16 @@ from a2a_handler.service import (
|
|
|
28
29
|
A2AService,
|
|
29
30
|
A2AResponse,
|
|
30
31
|
StreamEvent,
|
|
32
|
+
part_data,
|
|
33
|
+
part_file,
|
|
34
|
+
part_kind,
|
|
35
|
+
part_text,
|
|
31
36
|
protocol_dump,
|
|
32
37
|
response_context_id,
|
|
33
38
|
response_state,
|
|
34
39
|
response_task_id,
|
|
40
|
+
state_label,
|
|
41
|
+
to_json_dict,
|
|
35
42
|
)
|
|
36
43
|
from a2a_handler.session import get_session, update_session
|
|
37
44
|
|
|
@@ -409,7 +416,7 @@ def _format_response_text(response: A2AResponse) -> str:
|
|
|
409
416
|
lines: list[str] = []
|
|
410
417
|
state = response_state(response)
|
|
411
418
|
if state:
|
|
412
|
-
lines.append(f"State: {state
|
|
419
|
+
lines.append(f"State: {state_label(state)}")
|
|
413
420
|
context_id = response_context_id(response)
|
|
414
421
|
if context_id:
|
|
415
422
|
lines.append(f"Context ID: {context_id}")
|
|
@@ -435,7 +442,7 @@ def _format_response_parts(response: A2AResponse) -> str:
|
|
|
435
442
|
|
|
436
443
|
if not formatted_parts and response.history:
|
|
437
444
|
for message in response.history:
|
|
438
|
-
if message.role == Role.
|
|
445
|
+
if message.role == Role.ROLE_AGENT:
|
|
439
446
|
formatted = _format_message_parts(message)
|
|
440
447
|
if formatted:
|
|
441
448
|
formatted_parts.append(formatted)
|
|
@@ -448,10 +455,10 @@ def _format_stream_event_text(event: StreamEvent) -> str:
|
|
|
448
455
|
if event.message:
|
|
449
456
|
return _format_message_parts(event.message)
|
|
450
457
|
|
|
451
|
-
if event.status and event.status.status
|
|
458
|
+
if event.status and event.status.status.HasField("message"):
|
|
452
459
|
return _format_message_parts(event.status.status.message)
|
|
453
460
|
|
|
454
|
-
if event.artifact and event.artifact.artifact:
|
|
461
|
+
if event.artifact and event.artifact.artifact.parts:
|
|
455
462
|
return _format_parts(event.artifact.artifact.parts)
|
|
456
463
|
|
|
457
464
|
if event.event_type == "task" and event.task:
|
|
@@ -466,7 +473,7 @@ def _format_stream_event_summaries(event: StreamEvent) -> list[str]:
|
|
|
466
473
|
|
|
467
474
|
if event.state:
|
|
468
475
|
summaries.append(
|
|
469
|
-
_format_event_summary("task", event.state
|
|
476
|
+
_format_event_summary("task", state_label(event.state), event.task_id)
|
|
470
477
|
)
|
|
471
478
|
|
|
472
479
|
parts = _stream_event_parts(event)
|
|
@@ -481,39 +488,40 @@ def _format_stream_event_summaries(event: StreamEvent) -> list[str]:
|
|
|
481
488
|
|
|
482
489
|
def _stream_event_parts(event: StreamEvent) -> list[Part]:
|
|
483
490
|
"""Return the parts carried by a stream event, if any."""
|
|
484
|
-
if event.message and event.message.role == Role.
|
|
485
|
-
return event.message.parts
|
|
486
|
-
if event.status and event.status.status
|
|
491
|
+
if event.message and event.message.role == Role.ROLE_AGENT:
|
|
492
|
+
return list(event.message.parts)
|
|
493
|
+
if event.status and event.status.status.HasField("message"):
|
|
487
494
|
message = event.status.status.message
|
|
488
|
-
if message.role == Role.
|
|
489
|
-
return message.parts
|
|
490
|
-
if event.artifact and event.artifact.artifact:
|
|
491
|
-
return event.artifact.artifact.parts
|
|
495
|
+
if message.role == Role.ROLE_AGENT:
|
|
496
|
+
return list(message.parts)
|
|
497
|
+
if event.artifact and event.artifact.artifact.parts:
|
|
498
|
+
return list(event.artifact.artifact.parts)
|
|
492
499
|
return []
|
|
493
500
|
|
|
494
501
|
|
|
495
502
|
def _format_part_event_summaries(part: Part, task_id: str | None) -> list[str]:
|
|
496
503
|
"""Summarize a stream part without rendering its full content."""
|
|
497
|
-
|
|
498
|
-
if
|
|
504
|
+
kind = part_kind(part)
|
|
505
|
+
if kind == "text":
|
|
499
506
|
return [_format_event_summary("message", "text", task_id)]
|
|
500
|
-
if
|
|
501
|
-
|
|
507
|
+
if kind == "data":
|
|
508
|
+
data = part_data(part)
|
|
509
|
+
if _is_tool_call_data(data):
|
|
502
510
|
return [
|
|
503
511
|
_format_event_summary(
|
|
504
|
-
"tool call", str(
|
|
512
|
+
"tool call", str(data.get("name", "unknown")), task_id
|
|
505
513
|
)
|
|
506
514
|
]
|
|
507
|
-
if _is_tool_response_data(
|
|
515
|
+
if _is_tool_response_data(data):
|
|
508
516
|
return [
|
|
509
517
|
_format_event_summary(
|
|
510
|
-
"tool result", str(
|
|
518
|
+
"tool result", str(data.get("name", "unknown")), task_id
|
|
511
519
|
)
|
|
512
520
|
]
|
|
513
521
|
return [_format_event_summary("message", "data", task_id)]
|
|
514
|
-
if
|
|
522
|
+
if kind == "file":
|
|
515
523
|
return [_format_event_summary("message", "file", task_id)]
|
|
516
|
-
return [_format_event_summary("message",
|
|
524
|
+
return [_format_event_summary("message", kind, task_id)]
|
|
517
525
|
|
|
518
526
|
|
|
519
527
|
def _format_event_summary(kind: str, detail: str, task_id: str | None) -> str:
|
|
@@ -542,12 +550,12 @@ def _stream_text_delta(emitted_text: str, event_text: str) -> str:
|
|
|
542
550
|
|
|
543
551
|
def _format_message_parts(message: Message) -> str:
|
|
544
552
|
"""Format a protocol message's parts, skipping echoed user messages."""
|
|
545
|
-
if message.role != Role.
|
|
553
|
+
if message.role != Role.ROLE_AGENT:
|
|
546
554
|
return ""
|
|
547
555
|
return _format_parts(message.parts)
|
|
548
556
|
|
|
549
557
|
|
|
550
|
-
def _format_parts(parts:
|
|
558
|
+
def _format_parts(parts: Sequence[Part] | None) -> str:
|
|
551
559
|
"""Format A2A message/artifact parts for human-readable CLI output."""
|
|
552
560
|
if not parts:
|
|
553
561
|
return ""
|
|
@@ -562,50 +570,46 @@ def _format_parts(parts: list[Part] | None) -> str:
|
|
|
562
570
|
|
|
563
571
|
def _format_part(part: Part) -> str:
|
|
564
572
|
"""Format a single A2A part for human-readable CLI output."""
|
|
565
|
-
|
|
566
|
-
if
|
|
567
|
-
return
|
|
568
|
-
if
|
|
569
|
-
return _format_data_part(
|
|
570
|
-
if
|
|
571
|
-
return _format_file_part(
|
|
573
|
+
kind = part_kind(part)
|
|
574
|
+
if kind == "text":
|
|
575
|
+
return part_text(part)
|
|
576
|
+
if kind == "data":
|
|
577
|
+
return _format_data_part(part_data(part))
|
|
578
|
+
if kind == "file":
|
|
579
|
+
return _format_file_part(part)
|
|
572
580
|
|
|
573
|
-
return "```json\n" + json.dumps(part
|
|
581
|
+
return "```json\n" + json.dumps(to_json_dict(part), indent=2) + "\n```"
|
|
574
582
|
|
|
575
583
|
|
|
576
|
-
def _format_data_part(
|
|
584
|
+
def _format_data_part(data: Any) -> str:
|
|
577
585
|
"""Format structured data parts while hiding internal tool chatter."""
|
|
578
|
-
data = part.data
|
|
579
586
|
if _is_tool_call_data(data) or _is_tool_response_data(data):
|
|
580
587
|
return ""
|
|
581
588
|
return "```json\n" + json.dumps(data, indent=2, default=str) + "\n```"
|
|
582
589
|
|
|
583
590
|
|
|
584
|
-
def _is_tool_call_data(data:
|
|
591
|
+
def _is_tool_call_data(data: Any) -> bool:
|
|
585
592
|
"""Return whether a data part looks like an internal tool call."""
|
|
586
|
-
return {"id", "name", "args"}.issubset(data)
|
|
593
|
+
return isinstance(data, dict) and {"id", "name", "args"}.issubset(data)
|
|
587
594
|
|
|
588
595
|
|
|
589
|
-
def _is_tool_response_data(data:
|
|
596
|
+
def _is_tool_response_data(data: Any) -> bool:
|
|
590
597
|
"""Return whether a data part looks like an internal tool response."""
|
|
591
|
-
return {"id", "name", "response"}.issubset(data)
|
|
598
|
+
return isinstance(data, dict) and {"id", "name", "response"}.issubset(data)
|
|
592
599
|
|
|
593
600
|
|
|
594
|
-
def _format_file_part(part:
|
|
601
|
+
def _format_file_part(part: Part) -> str:
|
|
595
602
|
"""Format file parts without dumping inline bytes into the terminal."""
|
|
596
|
-
|
|
597
|
-
name =
|
|
598
|
-
mime_type = getattr(file_part, "mime_type", None)
|
|
599
|
-
uri = getattr(file_part, "uri", None)
|
|
600
|
-
inline_bytes = getattr(file_part, "bytes", None)
|
|
603
|
+
info = part_file(part)
|
|
604
|
+
name = info.get("name") or "unnamed file"
|
|
601
605
|
|
|
602
606
|
details = [name]
|
|
603
|
-
if
|
|
604
|
-
details.append(
|
|
605
|
-
if uri:
|
|
606
|
-
details.append(uri)
|
|
607
|
-
elif
|
|
608
|
-
details.append(f"inline bytes, {
|
|
607
|
+
if info.get("media_type"):
|
|
608
|
+
details.append(info["media_type"])
|
|
609
|
+
if info.get("uri"):
|
|
610
|
+
details.append(info["uri"])
|
|
611
|
+
elif info.get("num_bytes"):
|
|
612
|
+
details.append(f"inline bytes, {info['num_bytes']} bytes")
|
|
609
613
|
return "[file: " + ", ".join(details) + "]"
|
|
610
614
|
|
|
611
615
|
|
|
@@ -617,7 +621,7 @@ def _stream_event_dump(event: StreamEvent) -> dict[str, object]:
|
|
|
617
621
|
if event.task_id:
|
|
618
622
|
data["taskId"] = event.task_id
|
|
619
623
|
if event.state:
|
|
620
|
-
data["state"] = event.state
|
|
624
|
+
data["state"] = state_label(event.state)
|
|
621
625
|
if event.text:
|
|
622
626
|
data["text"] = event.text
|
|
623
627
|
if event.message:
|
|
@@ -28,6 +28,7 @@ from a2a_handler.service import (
|
|
|
28
28
|
push_config_dump,
|
|
29
29
|
response_context_id,
|
|
30
30
|
response_task_id,
|
|
31
|
+
to_json_dict,
|
|
31
32
|
)
|
|
32
33
|
from a2a_handler.session import (
|
|
33
34
|
clear_session,
|
|
@@ -220,7 +221,7 @@ def create_mcp_server() -> FastMCP:
|
|
|
220
221
|
service = A2AService(http_client, agent_url, credentials=credentials)
|
|
221
222
|
card = await service.get_card()
|
|
222
223
|
|
|
223
|
-
return card
|
|
224
|
+
return to_json_dict(card)
|
|
224
225
|
|
|
225
226
|
@mcp.tool()
|
|
226
227
|
async def send_message(
|
|
@@ -60,13 +60,9 @@ def run_server(
|
|
|
60
60
|
agent = create_llm_agent(model=effective_model)
|
|
61
61
|
agent_card = build_agent_card(agent, host, port, require_auth=require_auth)
|
|
62
62
|
|
|
63
|
-
streaming_enabled = (
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
push_notifications_enabled = (
|
|
67
|
-
agent_card.capabilities.push_notifications if agent_card.capabilities else False
|
|
68
|
-
)
|
|
69
|
-
auth_enabled = agent_card.security_schemes is not None
|
|
63
|
+
streaming_enabled = bool(agent_card.capabilities.streaming)
|
|
64
|
+
push_notifications_enabled = bool(agent_card.capabilities.push_notifications)
|
|
65
|
+
auth_enabled = bool(agent_card.security_schemes)
|
|
70
66
|
|
|
71
67
|
logger.info(
|
|
72
68
|
"Agent card capabilities: streaming=%s, push_notifications=%s, auth=%s",
|
|
@@ -5,15 +5,16 @@ from collections.abc import AsyncIterator, Awaitable, Callable
|
|
|
5
5
|
from contextlib import asynccontextmanager
|
|
6
6
|
|
|
7
7
|
import httpx
|
|
8
|
-
from a2a.server.
|
|
8
|
+
from a2a.server.context import ServerCallContext
|
|
9
9
|
from a2a.server.request_handlers import DefaultRequestHandler
|
|
10
|
+
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
|
|
10
11
|
from a2a.server.tasks import (
|
|
11
12
|
BasePushNotificationSender,
|
|
12
13
|
InMemoryPushNotificationConfigStore,
|
|
13
14
|
InMemoryTaskStore,
|
|
14
15
|
)
|
|
15
|
-
from a2a.types import AgentCard,
|
|
16
|
-
from a2a.utils.errors import
|
|
16
|
+
from a2a.types import AgentCard, TaskPushNotificationConfig
|
|
17
|
+
from a2a.utils.errors import InvalidParamsError
|
|
17
18
|
from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
|
|
18
19
|
from google.adk.agents.llm_agent import Agent
|
|
19
20
|
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
|
@@ -48,7 +49,8 @@ class ValidatingPushNotificationConfigStore(InMemoryPushNotificationConfigStore)
|
|
|
48
49
|
async def set_info(
|
|
49
50
|
self,
|
|
50
51
|
task_id: str,
|
|
51
|
-
notification_config:
|
|
52
|
+
notification_config: TaskPushNotificationConfig,
|
|
53
|
+
context: ServerCallContext,
|
|
52
54
|
) -> None:
|
|
53
55
|
"""Validate webhook settings before storing push configuration."""
|
|
54
56
|
try:
|
|
@@ -57,13 +59,11 @@ class ValidatingPushNotificationConfigStore(InMemoryPushNotificationConfigStore)
|
|
|
57
59
|
if notification_config.token:
|
|
58
60
|
reject_control_chars(notification_config.token, "webhook_token")
|
|
59
61
|
except InputValidationError as error:
|
|
60
|
-
raise
|
|
61
|
-
error
|
|
62
|
-
message=f"{error.code}: {error.message}",
|
|
63
|
-
)
|
|
62
|
+
raise InvalidParamsError(
|
|
63
|
+
message=f"{error.code}: {error.message}",
|
|
64
64
|
) from error
|
|
65
65
|
|
|
66
|
-
await super().set_info(task_id, notification_config)
|
|
66
|
+
await super().set_info(task_id, notification_config, context)
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def generate_api_key() -> str:
|
|
@@ -186,6 +186,7 @@ def create_a2a_application(
|
|
|
186
186
|
request_handler = DefaultRequestHandler(
|
|
187
187
|
agent_executor=agent_executor,
|
|
188
188
|
task_store=task_store,
|
|
189
|
+
agent_card=agent_card,
|
|
189
190
|
push_config_store=push_notification_config_store,
|
|
190
191
|
push_sender=push_notification_sender,
|
|
191
192
|
)
|
|
@@ -196,13 +197,13 @@ def create_a2a_application(
|
|
|
196
197
|
Middleware(APIKeyAuthMiddleware, api_key=api_key) # type: ignore[arg-type]
|
|
197
198
|
)
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
|
|
200
|
+
# Serve the agent card plus a JSON-RPC endpoint. ``enable_v0_3_compat``
|
|
201
|
+
# lets legacy v0.3 clients call the same endpoint as v1.0 clients.
|
|
202
|
+
routes = [
|
|
203
|
+
*create_agent_card_routes(agent_card),
|
|
204
|
+
*create_jsonrpc_routes(request_handler, rpc_url="/", enable_v0_3_compat=True),
|
|
205
|
+
]
|
|
206
|
+
logger.info("A2A routes configured with push notification support")
|
|
206
207
|
|
|
207
208
|
async def cleanup_http_client() -> None:
|
|
208
209
|
await http_client.aclose()
|
|
@@ -210,12 +211,11 @@ def create_a2a_application(
|
|
|
210
211
|
|
|
211
212
|
@asynccontextmanager
|
|
212
213
|
async def lifespan(application: Starlette) -> AsyncIterator[None]:
|
|
213
|
-
await setup_a2a_routes(application)
|
|
214
214
|
try:
|
|
215
215
|
yield
|
|
216
216
|
finally:
|
|
217
217
|
await cleanup_http_client()
|
|
218
218
|
|
|
219
|
-
application = Starlette(middleware=middleware, lifespan=lifespan)
|
|
219
|
+
application = Starlette(routes=routes, middleware=middleware, lifespan=lifespan)
|
|
220
220
|
|
|
221
221
|
return application
|
|
@@ -5,10 +5,17 @@ from importlib.metadata import version as package_version
|
|
|
5
5
|
from a2a.types import (
|
|
6
6
|
AgentCapabilities,
|
|
7
7
|
AgentCard,
|
|
8
|
+
AgentInterface,
|
|
8
9
|
AgentSkill,
|
|
9
10
|
APIKeySecurityScheme,
|
|
10
|
-
|
|
11
|
+
SecurityRequirement,
|
|
11
12
|
SecurityScheme,
|
|
13
|
+
StringList,
|
|
14
|
+
)
|
|
15
|
+
from a2a.utils.constants import (
|
|
16
|
+
PROTOCOL_VERSION_0_3,
|
|
17
|
+
PROTOCOL_VERSION_1_0,
|
|
18
|
+
TransportProtocol,
|
|
12
19
|
)
|
|
13
20
|
from google.adk.agents.llm_agent import Agent
|
|
14
21
|
|
|
@@ -25,6 +32,9 @@ def build_agent_card(
|
|
|
25
32
|
) -> AgentCard:
|
|
26
33
|
"""Build an AgentCard with streaming and push notification capabilities.
|
|
27
34
|
|
|
35
|
+
The card advertises both A2A v1.0 and v0.3 JSON-RPC interfaces so that
|
|
36
|
+
both current and legacy clients can connect to the built-in server.
|
|
37
|
+
|
|
28
38
|
Args:
|
|
29
39
|
agent: The ADK agent
|
|
30
40
|
host: Host address for the RPC URL
|
|
@@ -111,30 +121,44 @@ def build_agent_card(
|
|
|
111
121
|
|
|
112
122
|
logger.debug("Building agent card with RPC URL: %s", rpc_endpoint_url)
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
# Advertise both protocol versions on the same JSON-RPC endpoint so v0.3
|
|
125
|
+
# clients keep working alongside v1.0 clients.
|
|
126
|
+
supported_interfaces = [
|
|
127
|
+
AgentInterface(
|
|
128
|
+
url=rpc_endpoint_url,
|
|
129
|
+
protocol_binding=TransportProtocol.JSONRPC.value,
|
|
130
|
+
protocol_version=PROTOCOL_VERSION_1_0,
|
|
131
|
+
),
|
|
132
|
+
AgentInterface(
|
|
133
|
+
url=rpc_endpoint_url,
|
|
134
|
+
protocol_binding=TransportProtocol.JSONRPC.value,
|
|
135
|
+
protocol_version=PROTOCOL_VERSION_0_3,
|
|
136
|
+
),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
card_kwargs: dict = {
|
|
140
|
+
"name": agent.name,
|
|
141
|
+
"description": agent.description or "Handler A2A agent",
|
|
142
|
+
"version": package_version("a2a-handler"),
|
|
143
|
+
"supported_interfaces": supported_interfaces,
|
|
144
|
+
"capabilities": agent_capabilities,
|
|
145
|
+
"skills": skills,
|
|
146
|
+
"default_input_modes": ["text/plain"],
|
|
147
|
+
"default_output_modes": ["text/plain"],
|
|
148
|
+
}
|
|
116
149
|
|
|
117
150
|
if require_auth:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
card_kwargs["security_schemes"] = {
|
|
152
|
+
"apiKey": SecurityScheme(
|
|
153
|
+
api_key_security_scheme=APIKeySecurityScheme(
|
|
154
|
+
name="X-API-Key",
|
|
155
|
+
location="header",
|
|
156
|
+
)
|
|
123
157
|
)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
158
|
+
}
|
|
159
|
+
card_kwargs["security_requirements"] = [
|
|
160
|
+
SecurityRequirement(schemes={"apiKey": StringList()})
|
|
161
|
+
]
|
|
127
162
|
logger.info("API key authentication enabled")
|
|
128
163
|
|
|
129
|
-
return AgentCard(
|
|
130
|
-
name=agent.name,
|
|
131
|
-
description=agent.description or "Handler A2A agent",
|
|
132
|
-
url=rpc_endpoint_url,
|
|
133
|
-
version=package_version("a2a-handler"),
|
|
134
|
-
capabilities=agent_capabilities,
|
|
135
|
-
skills=skills,
|
|
136
|
-
default_input_modes=["text/plain"],
|
|
137
|
-
default_output_modes=["text/plain"],
|
|
138
|
-
security_schemes=security_schemes,
|
|
139
|
-
security=security,
|
|
140
|
-
)
|
|
164
|
+
return AgentCard(**card_kwargs)
|