a2a-handler 0.3.0__tar.gz → 0.4.0__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.3.0 → a2a_handler-0.4.0}/PKG-INFO +4 -1
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/README.md +2 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/pyproject.toml +39 -2
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/pyproject.toml.orig +32 -2
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/__init__.py +1 -1
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/auth.py +6 -8
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/__init__.py +1 -1
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/_helpers.py +26 -17
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/card.py +11 -11
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/message.py +60 -30
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/server.py +50 -12
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/session.py +3 -5
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/task.py +82 -67
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/common/__init__.py +2 -3
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/mcp/server.py +161 -19
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/servers.py +99 -2
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/service.py +158 -13
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/session.py +3 -5
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/app.py +14 -4
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/commands/palette.py +6 -1
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/artifacts.py +70 -4
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/card.py +3 -3
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/messages.py +22 -11
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/tasks.py +7 -4
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/server/save.py +5 -1
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/server/tab.py +153 -16
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/server/views.py +4 -4
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/turn.py +28 -11
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/webhook.py +1 -1
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/LICENSE +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/mcp.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/cli/schema.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/common/config.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/common/dotenv.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/common/input_validation.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/common/logging.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/common/output.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/mcp/__init__.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/__init__.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/app.tcss +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/commands/__init__.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/commands/screens.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/__init__.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/auth.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/headers.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/input.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/components/logs.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/server/__init__.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/server/session.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/server/tabs.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/server/types.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/tui/server/workspace.py +0 -0
- {a2a_handler-0.3.0 → a2a_handler-0.4.0}/src/a2a_handler/validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: a2a-handler
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
20
|
Classifier: Typing :: Typed
|
|
20
21
|
Requires-Dist: a2a-sdk[http-server]>=1.0.0,<2.0.0
|
|
@@ -42,6 +43,8 @@ Description-Content-Type: text/markdown
|
|
|
42
43
|
# Handler
|
|
43
44
|
|
|
44
45
|
[](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
|
|
46
|
+
[](https://github.com/alDuncanson/Handler/actions/workflows/ci.yml)
|
|
47
|
+
[](https://github.com/alDuncanson/Handler/attestations)
|
|
45
48
|
[](https://a2a-protocol.org/latest/)
|
|
46
49
|
[](https://pypi.org/project/a2a-handler/)
|
|
47
50
|
[](https://pypi.org/project/a2a-handler/)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Handler
|
|
2
2
|
|
|
3
3
|
[](https://github.com/alDuncanson/handler/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/alDuncanson/Handler/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/alDuncanson/Handler/attestations)
|
|
4
6
|
[](https://a2a-protocol.org/latest/)
|
|
5
7
|
[](https://pypi.org/project/a2a-handler/)
|
|
6
8
|
[](https://pypi.org/project/a2a-handler/)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -24,6 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Programming Language :: Python :: 3.11",
|
|
25
25
|
"Programming Language :: Python :: 3.12",
|
|
26
26
|
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
27
28
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
29
|
"Typing :: Typed",
|
|
29
30
|
]
|
|
@@ -71,7 +72,7 @@ dev = [
|
|
|
71
72
|
]
|
|
72
73
|
|
|
73
74
|
[build-system]
|
|
74
|
-
requires = ["uv_build>=0.9.15,<0.
|
|
75
|
+
requires = ["uv_build>=0.9.15,<0.14.0"]
|
|
75
76
|
build-backend = "uv_build"
|
|
76
77
|
|
|
77
78
|
[tool.uv.build-backend]
|
|
@@ -85,13 +86,49 @@ default = true
|
|
|
85
86
|
[tool.pytest.ini_options]
|
|
86
87
|
asyncio_mode = "auto"
|
|
87
88
|
|
|
89
|
+
[tool.ruff.lint]
|
|
90
|
+
select = [
|
|
91
|
+
"E",
|
|
92
|
+
"W",
|
|
93
|
+
"F",
|
|
94
|
+
"I",
|
|
95
|
+
"UP",
|
|
96
|
+
"B",
|
|
97
|
+
"A",
|
|
98
|
+
"C4",
|
|
99
|
+
"SIM",
|
|
100
|
+
"PT",
|
|
101
|
+
"ASYNC",
|
|
102
|
+
"S",
|
|
103
|
+
"LOG",
|
|
104
|
+
"G",
|
|
105
|
+
"PERF",
|
|
106
|
+
"N",
|
|
107
|
+
"ERA",
|
|
108
|
+
"RUF",
|
|
109
|
+
]
|
|
110
|
+
ignore = [
|
|
111
|
+
"E501",
|
|
112
|
+
"S101",
|
|
113
|
+
]
|
|
114
|
+
|
|
88
115
|
[tool.ruff.lint.per-file-ignores]
|
|
89
116
|
"src/a2a_handler/cli/__init__.py" = ["E402"]
|
|
117
|
+
"src/a2a_handler/tui/**" = ["A002"]
|
|
118
|
+
"tests/**" = [
|
|
119
|
+
"B009",
|
|
120
|
+
"B010",
|
|
121
|
+
"S104",
|
|
122
|
+
"S105",
|
|
123
|
+
"S106",
|
|
124
|
+
"S108",
|
|
125
|
+
]
|
|
90
126
|
|
|
91
127
|
[tool.coverage.run]
|
|
92
128
|
source = ["a2a_handler"]
|
|
93
129
|
|
|
94
130
|
[tool.coverage.report]
|
|
131
|
+
fail_under = 88
|
|
95
132
|
show_missing = true
|
|
96
133
|
skip_empty = true
|
|
97
134
|
exclude_lines = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "a2a-handler"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "An A2A Protocol client TUI and CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -27,6 +27,7 @@ classifiers = [
|
|
|
27
27
|
"Programming Language :: Python :: 3.11",
|
|
28
28
|
"Programming Language :: Python :: 3.12",
|
|
29
29
|
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
30
31
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
32
|
"Typing :: Typed",
|
|
32
33
|
]
|
|
@@ -75,7 +76,7 @@ Changelog = "https://github.com/alDuncanson/handler/releases"
|
|
|
75
76
|
handler = "a2a_handler.cli:main"
|
|
76
77
|
|
|
77
78
|
[build-system]
|
|
78
|
-
requires = ["uv_build>=0.9.15,<0.
|
|
79
|
+
requires = ["uv_build>=0.9.15,<0.14.0"]
|
|
79
80
|
build-backend = "uv_build"
|
|
80
81
|
|
|
81
82
|
[tool.uv.build-backend]
|
|
@@ -93,13 +94,42 @@ default = true
|
|
|
93
94
|
[tool.pytest.ini_options]
|
|
94
95
|
asyncio_mode = "auto"
|
|
95
96
|
|
|
97
|
+
[tool.ruff.lint]
|
|
98
|
+
select = [
|
|
99
|
+
"E", "W", # pycodestyle
|
|
100
|
+
"F", # pyflakes
|
|
101
|
+
"I", # isort
|
|
102
|
+
"UP", # pyupgrade
|
|
103
|
+
"B", # bugbear
|
|
104
|
+
"A", # shadowed builtins
|
|
105
|
+
"C4", # comprehensions
|
|
106
|
+
"SIM", # simplify
|
|
107
|
+
"PT", # pytest style
|
|
108
|
+
"ASYNC", # async pitfalls
|
|
109
|
+
"S", # bandit
|
|
110
|
+
"LOG", "G", # logging
|
|
111
|
+
"PERF", # perflint
|
|
112
|
+
"N", # pep8-naming
|
|
113
|
+
"ERA", # commented-out code
|
|
114
|
+
"RUF", # ruff-specific
|
|
115
|
+
]
|
|
116
|
+
ignore = [
|
|
117
|
+
"E501", # line length is ruff format's job
|
|
118
|
+
"S101", # asserts narrow types for ty; Handler never runs under -O
|
|
119
|
+
]
|
|
120
|
+
|
|
96
121
|
[tool.ruff.lint.per-file-ignores]
|
|
97
122
|
"src/a2a_handler/cli/__init__.py" = ["E402"]
|
|
123
|
+
# Textual widgets take `id`, like their base classes.
|
|
124
|
+
"src/a2a_handler/tui/**" = ["A002"]
|
|
125
|
+
# Tests monkeypatch with setattr and hold fixture secrets, paths, and bind addresses.
|
|
126
|
+
"tests/**" = ["B009", "B010", "S104", "S105", "S106", "S108"]
|
|
98
127
|
|
|
99
128
|
[tool.coverage.run]
|
|
100
129
|
source = ["a2a_handler"]
|
|
101
130
|
|
|
102
131
|
[tool.coverage.report]
|
|
132
|
+
fail_under = 88
|
|
103
133
|
show_missing = true
|
|
104
134
|
skip_empty = true
|
|
105
135
|
exclude_lines = [
|
|
@@ -14,6 +14,7 @@ from dataclasses import dataclass, field
|
|
|
14
14
|
from enum import Enum
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
import httpx
|
|
17
18
|
|
|
18
19
|
from a2a_handler.common.input_validation import (
|
|
19
20
|
check_key_file_permissions,
|
|
@@ -21,9 +22,6 @@ from a2a_handler.common.input_validation import (
|
|
|
21
22
|
validate_token_url,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
|
-
import httpx
|
|
25
|
-
|
|
26
|
-
|
|
27
25
|
_TOKEN_EXPIRY_MARGIN = 30 # seconds before expiry to trigger refresh
|
|
28
26
|
|
|
29
27
|
|
|
@@ -82,7 +80,7 @@ class AuthCredentials:
|
|
|
82
80
|
val = getattr(self, secret_field)
|
|
83
81
|
redacted[secret_field] = "***" if val else None
|
|
84
82
|
if self.custom_headers:
|
|
85
|
-
redacted["custom_headers"] =
|
|
83
|
+
redacted["custom_headers"] = dict.fromkeys(self.custom_headers, "***")
|
|
86
84
|
return f"AuthCredentials({redacted})"
|
|
87
85
|
|
|
88
86
|
def to_headers(self) -> dict[str, str]:
|
|
@@ -94,9 +92,9 @@ class AuthCredentials:
|
|
|
94
92
|
headers: dict[str, str] = {}
|
|
95
93
|
if self.custom_headers:
|
|
96
94
|
headers.update(self.custom_headers)
|
|
97
|
-
if self.auth_type == AuthType.BEARER and self.value
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
if (self.auth_type == AuthType.BEARER and self.value) or (
|
|
96
|
+
self.auth_type in TOKEN_FETCHING_AUTH_TYPES and self.value
|
|
97
|
+
):
|
|
100
98
|
headers["Authorization"] = f"Bearer {self.value}"
|
|
101
99
|
elif self.auth_type == AuthType.BASIC and self.username is not None:
|
|
102
100
|
userpass = f"{self.username}:{self.value}".encode()
|
|
@@ -241,7 +239,7 @@ class AuthCredentials:
|
|
|
241
239
|
return data
|
|
242
240
|
|
|
243
241
|
@classmethod
|
|
244
|
-
def from_dict(cls, data: dict) ->
|
|
242
|
+
def from_dict(cls, data: dict) -> AuthCredentials:
|
|
245
243
|
"""Deserialize credentials from storage."""
|
|
246
244
|
custom_headers_raw = data.get("custom_headers")
|
|
247
245
|
custom_headers = (
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
+
from collections.abc import Sequence
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
|
|
7
|
-
import httpx
|
|
8
8
|
import click
|
|
9
|
+
import httpx
|
|
9
10
|
from a2a.client.errors import (
|
|
10
11
|
A2AClientError,
|
|
11
12
|
A2AClientTimeoutError,
|
|
@@ -29,9 +30,12 @@ from a2a_handler.common import Output, get_logger
|
|
|
29
30
|
from a2a_handler.common.input_validation import InputValidationError
|
|
30
31
|
from a2a_handler.servers import (
|
|
31
32
|
ServerDefinition,
|
|
33
|
+
ServerLookupError,
|
|
34
|
+
find_named_server,
|
|
32
35
|
load_server_catalog,
|
|
33
36
|
resolve_server_credentials,
|
|
34
37
|
)
|
|
38
|
+
from a2a_handler.service import SavedPart
|
|
35
39
|
|
|
36
40
|
TIMEOUT = 120
|
|
37
41
|
_TIMEOUT_NONE_VALUES = {"none", "null", "off", "disabled"}
|
|
@@ -235,6 +239,23 @@ def _protocol_error_suggestion(error: A2AError) -> str | None:
|
|
|
235
239
|
return None
|
|
236
240
|
|
|
237
241
|
|
|
242
|
+
def report_saved_parts(saved: Sequence[SavedPart], output: Output) -> None:
|
|
243
|
+
"""Tell the user what --save-artifacts wrote, without disturbing JSON output."""
|
|
244
|
+
if not saved:
|
|
245
|
+
output.text(
|
|
246
|
+
"No file or data parts to save."
|
|
247
|
+
) if not output.is_structured else click.echo(
|
|
248
|
+
"No file or data parts to save.", err=True
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
for item in saved:
|
|
252
|
+
line = f"saved {item.path} ({item.kind}, {item.num_bytes} bytes)"
|
|
253
|
+
if output.is_structured:
|
|
254
|
+
click.echo(line, err=True)
|
|
255
|
+
else:
|
|
256
|
+
output.text(line)
|
|
257
|
+
|
|
258
|
+
|
|
238
259
|
def _resolve_env_secret(env_var: str, label: str) -> str:
|
|
239
260
|
"""Read a secret from an environment variable."""
|
|
240
261
|
value = os.environ.get(env_var)
|
|
@@ -247,22 +268,10 @@ def _resolve_env_secret(env_var: str, label: str) -> str:
|
|
|
247
268
|
|
|
248
269
|
def _find_named_server(server: str) -> ServerDefinition:
|
|
249
270
|
"""Resolve a named server definition, failing closed on ambiguity."""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
*catalog.repository_servers,
|
|
255
|
-
*catalog.global_servers,
|
|
256
|
-
)
|
|
257
|
-
if server_def.name == server
|
|
258
|
-
]
|
|
259
|
-
if len(matches) > 1:
|
|
260
|
-
raise click.UsageError(
|
|
261
|
-
f"Server '{server}' exists in multiple sources; use --url or rename one entry."
|
|
262
|
-
)
|
|
263
|
-
if matches:
|
|
264
|
-
return matches[0]
|
|
265
|
-
raise click.UsageError(f"Server '{server}' not found in servers.toml.")
|
|
271
|
+
try:
|
|
272
|
+
return find_named_server(server, load_server_catalog())
|
|
273
|
+
except ServerLookupError as error:
|
|
274
|
+
raise click.UsageError(str(error)) from error
|
|
266
275
|
|
|
267
276
|
|
|
268
277
|
def resolve_agent_selection(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Card commands for agent card operations."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
from a2a.types import AgentCard
|
|
@@ -47,11 +47,11 @@ def card() -> None:
|
|
|
47
47
|
"--api-key-env", "-k", help="Env var containing API key (overrides saved)"
|
|
48
48
|
)
|
|
49
49
|
def card_get(
|
|
50
|
-
agent_url:
|
|
51
|
-
server_name:
|
|
50
|
+
agent_url: str | None,
|
|
51
|
+
server_name: str | None,
|
|
52
52
|
extended: bool,
|
|
53
|
-
bearer_env:
|
|
54
|
-
api_key_env:
|
|
53
|
+
bearer_env: str | None,
|
|
54
|
+
api_key_env: str | None,
|
|
55
55
|
) -> None:
|
|
56
56
|
"""Retrieve an agent's card.
|
|
57
57
|
|
|
@@ -91,7 +91,7 @@ def card_get(
|
|
|
91
91
|
|
|
92
92
|
except Exception as e:
|
|
93
93
|
handle_client_error(e, resolved_url, output)
|
|
94
|
-
raise click.Abort()
|
|
94
|
+
raise click.Abort() from e
|
|
95
95
|
|
|
96
96
|
asyncio.run(do_get())
|
|
97
97
|
|
|
@@ -119,11 +119,11 @@ def _format_agent_card(card_data: object, output: Output) -> None:
|
|
|
119
119
|
"--api-key-env", "-k", help="Env var containing API key (overrides saved)"
|
|
120
120
|
)
|
|
121
121
|
def card_validate(
|
|
122
|
-
agent_url:
|
|
123
|
-
file_path:
|
|
124
|
-
server_name:
|
|
125
|
-
bearer_env:
|
|
126
|
-
api_key_env:
|
|
122
|
+
agent_url: str | None,
|
|
123
|
+
file_path: str | None,
|
|
124
|
+
server_name: str | None,
|
|
125
|
+
bearer_env: str | None,
|
|
126
|
+
api_key_env: str | None,
|
|
127
127
|
) -> None:
|
|
128
128
|
"""Validate an agent card from URL or file.
|
|
129
129
|
|
|
@@ -4,10 +4,11 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
from collections.abc import Sequence
|
|
6
6
|
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
|
-
from typing import Optional
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
|
+
import httpx
|
|
11
12
|
from a2a.types import Message, Part, Role
|
|
12
13
|
|
|
13
14
|
from a2a_handler.auth import (
|
|
@@ -26,8 +27,8 @@ from a2a_handler.common.input_validation import (
|
|
|
26
27
|
validate_webhook_url,
|
|
27
28
|
)
|
|
28
29
|
from a2a_handler.service import (
|
|
29
|
-
A2AService,
|
|
30
30
|
A2AResponse,
|
|
31
|
+
A2AService,
|
|
31
32
|
StreamEvent,
|
|
32
33
|
attachment_part_from_spec,
|
|
33
34
|
build_data_part,
|
|
@@ -39,6 +40,7 @@ from a2a_handler.service import (
|
|
|
39
40
|
response_context_id,
|
|
40
41
|
response_state,
|
|
41
42
|
response_task_id,
|
|
43
|
+
save_response_parts,
|
|
42
44
|
state_label,
|
|
43
45
|
to_json_dict,
|
|
44
46
|
)
|
|
@@ -49,6 +51,7 @@ from ._helpers import (
|
|
|
49
51
|
build_streaming_http_client,
|
|
50
52
|
handle_client_error,
|
|
51
53
|
handle_validation_error,
|
|
54
|
+
report_saved_parts,
|
|
52
55
|
resolve_agent_selection,
|
|
53
56
|
resolve_selection_credentials,
|
|
54
57
|
)
|
|
@@ -107,6 +110,12 @@ def message() -> None:
|
|
|
107
110
|
help="Return as soon as the agent acknowledges with a task instead of "
|
|
108
111
|
"waiting for completion",
|
|
109
112
|
)
|
|
113
|
+
@click.option(
|
|
114
|
+
"--save-artifacts",
|
|
115
|
+
"save_dir",
|
|
116
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
117
|
+
help="Write the reply's file and data parts into this directory",
|
|
118
|
+
)
|
|
110
119
|
@click.option("--push-url", help="Webhook URL for push notifications")
|
|
111
120
|
@click.option("--push-token", help="Authentication token for push notifications")
|
|
112
121
|
@click.option(
|
|
@@ -130,24 +139,25 @@ def message() -> None:
|
|
|
130
139
|
help="Custom header (repeatable, format: 'Name: Value')",
|
|
131
140
|
)
|
|
132
141
|
def message_send(
|
|
133
|
-
agent_url:
|
|
134
|
-
server_name:
|
|
135
|
-
text:
|
|
142
|
+
agent_url: str | None,
|
|
143
|
+
server_name: str | None,
|
|
144
|
+
text: str | None,
|
|
136
145
|
files: tuple[str, ...],
|
|
137
146
|
data_values: tuple[str, ...],
|
|
138
147
|
stream: bool,
|
|
139
|
-
json_payload:
|
|
140
|
-
context_id:
|
|
141
|
-
task_id:
|
|
148
|
+
json_payload: str | None,
|
|
149
|
+
context_id: str | None,
|
|
150
|
+
task_id: str | None,
|
|
142
151
|
use_session: bool,
|
|
143
152
|
accepted_output_modes: tuple[str, ...],
|
|
144
|
-
history_length:
|
|
153
|
+
history_length: int | None,
|
|
145
154
|
no_wait: bool,
|
|
146
|
-
push_url:
|
|
147
|
-
push_token:
|
|
155
|
+
push_url: str | None,
|
|
156
|
+
push_token: str | None,
|
|
157
|
+
save_dir: Path | None,
|
|
148
158
|
extensions: tuple[str, ...],
|
|
149
|
-
bearer_env:
|
|
150
|
-
api_key_env:
|
|
159
|
+
bearer_env: str | None,
|
|
160
|
+
api_key_env: str | None,
|
|
151
161
|
headers: tuple[str, ...] = (),
|
|
152
162
|
) -> None:
|
|
153
163
|
"""Send a message to an agent and receive a response.
|
|
@@ -341,6 +351,8 @@ def message_send(
|
|
|
341
351
|
attachments=attachments or None,
|
|
342
352
|
accepted_output_modes=accepted_output_modes or None,
|
|
343
353
|
history_length=history_length,
|
|
354
|
+
save_dir=save_dir,
|
|
355
|
+
http_client=http_client,
|
|
344
356
|
)
|
|
345
357
|
else:
|
|
346
358
|
response = await service.send(
|
|
@@ -358,10 +370,15 @@ def message_send(
|
|
|
358
370
|
response_task_id(response),
|
|
359
371
|
)
|
|
360
372
|
_format_response(response, output)
|
|
373
|
+
if save_dir is not None:
|
|
374
|
+
saved = await save_response_parts(
|
|
375
|
+
response, save_dir, http_client=http_client
|
|
376
|
+
)
|
|
377
|
+
report_saved_parts(saved, output)
|
|
361
378
|
|
|
362
379
|
except Exception as e:
|
|
363
380
|
handle_client_error(e, resolved_url, output)
|
|
364
|
-
raise click.Abort()
|
|
381
|
+
raise click.Abort() from e
|
|
365
382
|
|
|
366
383
|
asyncio.run(do_send())
|
|
367
384
|
|
|
@@ -375,9 +392,7 @@ def _build_attachments(
|
|
|
375
392
|
Raises InputValidationError for an unreadable/oversized file or invalid
|
|
376
393
|
JSON, so callers handle it alongside the other input checks.
|
|
377
394
|
"""
|
|
378
|
-
attachments: list[Part] = []
|
|
379
|
-
for file_spec in files:
|
|
380
|
-
attachments.append(attachment_part_from_spec(file_spec))
|
|
395
|
+
attachments: list[Part] = [attachment_part_from_spec(spec) for spec in files]
|
|
381
396
|
for raw_value in data_values:
|
|
382
397
|
try:
|
|
383
398
|
value = json.loads(raw_value)
|
|
@@ -425,6 +440,12 @@ def _build_attachments(
|
|
|
425
440
|
type=int,
|
|
426
441
|
help="History messages the agent should include on the task",
|
|
427
442
|
)
|
|
443
|
+
@click.option(
|
|
444
|
+
"--save-artifacts",
|
|
445
|
+
"save_dir",
|
|
446
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
447
|
+
help="Write the reply's file and data parts into this directory",
|
|
448
|
+
)
|
|
428
449
|
@click.option("--push-url", help="Webhook URL for push notifications")
|
|
429
450
|
@click.option("--push-token", help="Authentication token for push notifications")
|
|
430
451
|
@click.option(
|
|
@@ -450,21 +471,22 @@ def _build_attachments(
|
|
|
450
471
|
@click.pass_context
|
|
451
472
|
def message_stream(
|
|
452
473
|
ctx: click.Context,
|
|
453
|
-
agent_url:
|
|
454
|
-
server_name:
|
|
455
|
-
text:
|
|
474
|
+
agent_url: str | None,
|
|
475
|
+
server_name: str | None,
|
|
476
|
+
text: str | None,
|
|
456
477
|
files: tuple[str, ...],
|
|
457
478
|
data_values: tuple[str, ...],
|
|
458
|
-
context_id:
|
|
459
|
-
task_id:
|
|
479
|
+
context_id: str | None,
|
|
480
|
+
task_id: str | None,
|
|
460
481
|
use_session: bool,
|
|
461
482
|
accepted_output_modes: tuple[str, ...],
|
|
462
|
-
history_length:
|
|
463
|
-
push_url:
|
|
464
|
-
push_token:
|
|
483
|
+
history_length: int | None,
|
|
484
|
+
push_url: str | None,
|
|
485
|
+
push_token: str | None,
|
|
486
|
+
save_dir: Path | None,
|
|
465
487
|
extensions: tuple[str, ...],
|
|
466
|
-
bearer_env:
|
|
467
|
-
api_key_env:
|
|
488
|
+
bearer_env: str | None,
|
|
489
|
+
api_key_env: str | None,
|
|
468
490
|
headers: tuple[str, ...] = (),
|
|
469
491
|
) -> None:
|
|
470
492
|
"""Send a message and stream the response in real-time.
|
|
@@ -492,6 +514,7 @@ def message_stream(
|
|
|
492
514
|
no_wait=False,
|
|
493
515
|
push_url=push_url,
|
|
494
516
|
push_token=push_token,
|
|
517
|
+
save_dir=save_dir,
|
|
495
518
|
extensions=extensions,
|
|
496
519
|
bearer_env=bearer_env,
|
|
497
520
|
api_key_env=api_key_env,
|
|
@@ -502,13 +525,15 @@ def message_stream(
|
|
|
502
525
|
async def _stream_message(
|
|
503
526
|
service: A2AService,
|
|
504
527
|
text: str,
|
|
505
|
-
context_id:
|
|
506
|
-
task_id:
|
|
528
|
+
context_id: str | None,
|
|
529
|
+
task_id: str | None,
|
|
507
530
|
agent_url: str,
|
|
508
531
|
output: Output,
|
|
509
532
|
attachments: Sequence[Part] | None = None,
|
|
510
533
|
accepted_output_modes: tuple[str, ...] | None = None,
|
|
511
|
-
history_length:
|
|
534
|
+
history_length: int | None = None,
|
|
535
|
+
save_dir: Path | None = None,
|
|
536
|
+
http_client: httpx.AsyncClient | None = None,
|
|
512
537
|
) -> None:
|
|
513
538
|
"""Stream a message and handle events."""
|
|
514
539
|
last_context_id: str | None = None
|
|
@@ -578,6 +603,11 @@ async def _stream_message(
|
|
|
578
603
|
output.text()
|
|
579
604
|
else:
|
|
580
605
|
output.text(_format_response_text(last_response))
|
|
606
|
+
if save_dir is not None:
|
|
607
|
+
saved = await save_response_parts(
|
|
608
|
+
last_response, save_dir, http_client=http_client
|
|
609
|
+
)
|
|
610
|
+
report_saved_parts(saved, output)
|
|
581
611
|
else:
|
|
582
612
|
output.error(code="no_response", message="No response received from stream")
|
|
583
613
|
|