adcp 0.1.2__py3-none-any.whl → 1.0.2__py3-none-any.whl
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.
- adcp/__init__.py +80 -2
- adcp/__main__.py +284 -0
- adcp/client.py +290 -105
- adcp/config.py +82 -0
- adcp/exceptions.py +121 -0
- adcp/protocols/__init__.py +2 -0
- adcp/protocols/a2a.py +201 -87
- adcp/protocols/base.py +11 -0
- adcp/protocols/mcp.py +185 -25
- adcp/types/__init__.py +4 -0
- adcp/types/core.py +76 -1
- adcp/types/generated.py +615 -0
- adcp/types/tasks.py +281 -0
- adcp/utils/__init__.py +2 -0
- adcp/utils/operation_id.py +2 -0
- {adcp-0.1.2.dist-info → adcp-1.0.2.dist-info}/METADATA +184 -8
- adcp-1.0.2.dist-info/RECORD +21 -0
- adcp-1.0.2.dist-info/entry_points.txt +2 -0
- adcp-0.1.2.dist-info/RECORD +0 -15
- {adcp-0.1.2.dist-info → adcp-1.0.2.dist-info}/WHEEL +0 -0
- {adcp-0.1.2.dist-info → adcp-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {adcp-0.1.2.dist-info → adcp-1.0.2.dist-info}/top_level.txt +0 -0
adcp/config.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Configuration management for AdCP CLI."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
CONFIG_DIR = Path.home() / ".adcp"
|
|
10
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ensure_config_dir() -> None:
|
|
14
|
+
"""Ensure config directory exists."""
|
|
15
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_config() -> dict[str, Any]:
|
|
19
|
+
"""Load configuration file."""
|
|
20
|
+
if not CONFIG_FILE.exists():
|
|
21
|
+
return {"agents": {}}
|
|
22
|
+
|
|
23
|
+
with open(CONFIG_FILE) as f:
|
|
24
|
+
return cast(dict[str, Any], json.load(f))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def save_config(config: dict[str, Any]) -> None:
|
|
28
|
+
"""Save configuration file with atomic write."""
|
|
29
|
+
ensure_config_dir()
|
|
30
|
+
|
|
31
|
+
# Write to temporary file first
|
|
32
|
+
temp_file = CONFIG_FILE.with_suffix(".tmp")
|
|
33
|
+
with open(temp_file, "w") as f:
|
|
34
|
+
json.dump(config, f, indent=2)
|
|
35
|
+
|
|
36
|
+
# Atomic rename
|
|
37
|
+
temp_file.replace(CONFIG_FILE)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_agent(
|
|
41
|
+
alias: str, url: str, protocol: str | None = None, auth_token: str | None = None
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Save agent configuration."""
|
|
44
|
+
config = load_config()
|
|
45
|
+
|
|
46
|
+
if "agents" not in config:
|
|
47
|
+
config["agents"] = {}
|
|
48
|
+
|
|
49
|
+
config["agents"][alias] = {
|
|
50
|
+
"agent_uri": url,
|
|
51
|
+
"protocol": protocol or "mcp",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if auth_token:
|
|
55
|
+
config["agents"][alias]["auth_token"] = auth_token
|
|
56
|
+
|
|
57
|
+
save_config(config)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_agent(alias: str) -> dict[str, Any] | None:
|
|
61
|
+
"""Get agent configuration by alias."""
|
|
62
|
+
config = load_config()
|
|
63
|
+
result = config.get("agents", {}).get(alias)
|
|
64
|
+
return cast(dict[str, Any], result) if result is not None else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def list_agents() -> dict[str, Any]:
|
|
68
|
+
"""List all saved agents."""
|
|
69
|
+
config = load_config()
|
|
70
|
+
return cast(dict[str, Any], config.get("agents", {}))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def remove_agent(alias: str) -> bool:
|
|
74
|
+
"""Remove agent configuration."""
|
|
75
|
+
config = load_config()
|
|
76
|
+
|
|
77
|
+
if alias in config.get("agents", {}):
|
|
78
|
+
del config["agents"][alias]
|
|
79
|
+
save_config(config)
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
return False
|
adcp/exceptions.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Exception hierarchy for AdCP client."""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ADCPError(Exception):
|
|
7
|
+
"""Base exception for all AdCP client errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
agent_id: str | None = None,
|
|
13
|
+
agent_uri: str | None = None,
|
|
14
|
+
suggestion: str | None = None,
|
|
15
|
+
):
|
|
16
|
+
"""Initialize exception with context."""
|
|
17
|
+
self.message = message
|
|
18
|
+
self.agent_id = agent_id
|
|
19
|
+
self.agent_uri = agent_uri
|
|
20
|
+
self.suggestion = suggestion
|
|
21
|
+
|
|
22
|
+
full_message = message
|
|
23
|
+
if agent_id:
|
|
24
|
+
full_message = f"[Agent: {agent_id}] {full_message}"
|
|
25
|
+
if agent_uri:
|
|
26
|
+
full_message = f"{full_message}\n URI: {agent_uri}"
|
|
27
|
+
if suggestion:
|
|
28
|
+
full_message = f"{full_message}\n 💡 {suggestion}"
|
|
29
|
+
|
|
30
|
+
super().__init__(full_message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ADCPConnectionError(ADCPError):
|
|
34
|
+
"""Connection to agent failed."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str, agent_id: str | None = None, agent_uri: str | None = None):
|
|
37
|
+
"""Initialize connection error."""
|
|
38
|
+
suggestion = (
|
|
39
|
+
"Check that the agent URI is correct and the agent is running.\n"
|
|
40
|
+
" Try testing with: python -m adcp test --config <agent-id>"
|
|
41
|
+
)
|
|
42
|
+
super().__init__(message, agent_id, agent_uri, suggestion)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ADCPAuthenticationError(ADCPError):
|
|
46
|
+
"""Authentication failed (401, 403)."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str, agent_id: str | None = None, agent_uri: str | None = None):
|
|
49
|
+
"""Initialize authentication error."""
|
|
50
|
+
suggestion = (
|
|
51
|
+
"Check that your auth_token is valid and not expired.\n"
|
|
52
|
+
" Verify auth_type ('bearer' vs 'token') and auth_header are correct.\n"
|
|
53
|
+
" Some agents (like Optable) require auth_type='bearer' and "
|
|
54
|
+
"auth_header='Authorization'"
|
|
55
|
+
)
|
|
56
|
+
super().__init__(message, agent_id, agent_uri, suggestion)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ADCPTimeoutError(ADCPError):
|
|
60
|
+
"""Request timed out."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
message: str,
|
|
65
|
+
agent_id: str | None = None,
|
|
66
|
+
agent_uri: str | None = None,
|
|
67
|
+
timeout: float | None = None,
|
|
68
|
+
):
|
|
69
|
+
"""Initialize timeout error."""
|
|
70
|
+
suggestion = (
|
|
71
|
+
f"The request took longer than {timeout}s." if timeout else "The request timed out."
|
|
72
|
+
)
|
|
73
|
+
suggestion += "\n Try increasing the timeout value or check if the agent is overloaded."
|
|
74
|
+
super().__init__(message, agent_id, agent_uri, suggestion)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ADCPProtocolError(ADCPError):
|
|
78
|
+
"""Protocol-level error (malformed response, unexpected format)."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, message: str, agent_id: str | None = None, protocol: str | None = None):
|
|
81
|
+
"""Initialize protocol error."""
|
|
82
|
+
suggestion = (
|
|
83
|
+
f"The agent returned an unexpected {protocol} response format."
|
|
84
|
+
if protocol
|
|
85
|
+
else "Unexpected response format."
|
|
86
|
+
)
|
|
87
|
+
suggestion += "\n Enable debug mode to see the full request/response."
|
|
88
|
+
super().__init__(message, agent_id, None, suggestion)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ADCPToolNotFoundError(ADCPError):
|
|
92
|
+
"""Requested tool not found on agent."""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self, tool_name: str, agent_id: str | None = None, available_tools: list[str] | None = None
|
|
96
|
+
):
|
|
97
|
+
"""Initialize tool not found error."""
|
|
98
|
+
message = f"Tool '{tool_name}' not found on agent"
|
|
99
|
+
suggestion = "List available tools with: python -m adcp list-tools --config <agent-id>"
|
|
100
|
+
if available_tools:
|
|
101
|
+
tools_list = ", ".join(available_tools[:5])
|
|
102
|
+
if len(available_tools) > 5:
|
|
103
|
+
tools_list += f", ... ({len(available_tools)} total)"
|
|
104
|
+
suggestion = f"Available tools: {tools_list}"
|
|
105
|
+
super().__init__(message, agent_id, None, suggestion)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ADCPWebhookError(ADCPError):
|
|
109
|
+
"""Webhook handling error."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ADCPWebhookSignatureError(ADCPWebhookError):
|
|
113
|
+
"""Webhook signature verification failed."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, message: str = "Invalid webhook signature", agent_id: str | None = None):
|
|
116
|
+
"""Initialize webhook signature error."""
|
|
117
|
+
suggestion = (
|
|
118
|
+
"Verify that the webhook_secret matches the secret configured on the agent.\n"
|
|
119
|
+
" Webhook signatures use HMAC-SHA256 for security."
|
|
120
|
+
)
|
|
121
|
+
super().__init__(message, agent_id, None, suggestion)
|
adcp/protocols/__init__.py
CHANGED
adcp/protocols/a2a.py
CHANGED
|
@@ -1,21 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""A2A protocol adapter using HTTP client.
|
|
2
4
|
|
|
3
5
|
The official a2a-sdk is primarily for building A2A servers. For client functionality,
|
|
4
6
|
we implement the A2A protocol using HTTP requests as per the A2A specification.
|
|
5
7
|
"""
|
|
6
8
|
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
7
11
|
from typing import Any
|
|
8
12
|
from uuid import uuid4
|
|
9
13
|
|
|
10
14
|
import httpx
|
|
11
15
|
|
|
16
|
+
from adcp.exceptions import (
|
|
17
|
+
ADCPAuthenticationError,
|
|
18
|
+
ADCPConnectionError,
|
|
19
|
+
ADCPTimeoutError,
|
|
20
|
+
)
|
|
12
21
|
from adcp.protocols.base import ProtocolAdapter
|
|
13
|
-
from adcp.types.core import TaskResult, TaskStatus
|
|
22
|
+
from adcp.types.core import AgentConfig, DebugInfo, TaskResult, TaskStatus
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
14
25
|
|
|
15
26
|
|
|
16
27
|
class A2AAdapter(ProtocolAdapter):
|
|
17
28
|
"""Adapter for A2A protocol following the Agent2Agent specification."""
|
|
18
29
|
|
|
30
|
+
def __init__(self, agent_config: AgentConfig):
|
|
31
|
+
"""Initialize A2A adapter with reusable HTTP client."""
|
|
32
|
+
super().__init__(agent_config)
|
|
33
|
+
self._client: httpx.AsyncClient | None = None
|
|
34
|
+
|
|
35
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
36
|
+
"""Get or create the HTTP client with connection pooling."""
|
|
37
|
+
if self._client is None:
|
|
38
|
+
# Configure connection pooling for better performance
|
|
39
|
+
limits = httpx.Limits(
|
|
40
|
+
max_keepalive_connections=10,
|
|
41
|
+
max_connections=20,
|
|
42
|
+
keepalive_expiry=30.0,
|
|
43
|
+
)
|
|
44
|
+
self._client = httpx.AsyncClient(limits=limits)
|
|
45
|
+
logger.debug(
|
|
46
|
+
f"Created HTTP client with connection pooling for agent {self.agent_config.id}"
|
|
47
|
+
)
|
|
48
|
+
return self._client
|
|
49
|
+
|
|
50
|
+
async def close(self) -> None:
|
|
51
|
+
"""Close the HTTP client and clean up resources."""
|
|
52
|
+
if self._client is not None:
|
|
53
|
+
logger.debug(f"Closing A2A adapter client for agent {self.agent_config.id}")
|
|
54
|
+
await self._client.aclose()
|
|
55
|
+
self._client = None
|
|
56
|
+
|
|
19
57
|
async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
20
58
|
"""
|
|
21
59
|
Call a tool using A2A protocol.
|
|
@@ -23,80 +61,117 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
23
61
|
A2A uses a tasks/send endpoint to initiate tasks. The agent responds with
|
|
24
62
|
task status and may require multiple roundtrips for completion.
|
|
25
63
|
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
start_time = time.time() if self.agent_config.debug else None
|
|
65
|
+
client = await self._get_client()
|
|
66
|
+
|
|
67
|
+
headers = {"Content-Type": "application/json"}
|
|
68
|
+
|
|
69
|
+
if self.agent_config.auth_token:
|
|
70
|
+
# Support custom auth headers and types
|
|
71
|
+
if self.agent_config.auth_type == "bearer":
|
|
72
|
+
headers[self.agent_config.auth_header] = f"Bearer {self.agent_config.auth_token}"
|
|
73
|
+
else:
|
|
74
|
+
headers[self.agent_config.auth_header] = self.agent_config.auth_token
|
|
75
|
+
|
|
76
|
+
# Construct A2A message
|
|
77
|
+
message = {
|
|
78
|
+
"role": "user",
|
|
79
|
+
"parts": [
|
|
80
|
+
{
|
|
81
|
+
"type": "text",
|
|
82
|
+
"text": self._format_tool_request(tool_name, params),
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# A2A uses message/send endpoint
|
|
88
|
+
url = f"{self.agent_config.agent_uri}/message/send"
|
|
89
|
+
|
|
90
|
+
request_data = {
|
|
91
|
+
"message": message,
|
|
92
|
+
"context_id": str(uuid4()),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
debug_info = None
|
|
96
|
+
if self.agent_config.debug:
|
|
97
|
+
debug_request = {
|
|
98
|
+
"url": url,
|
|
99
|
+
"method": "POST",
|
|
100
|
+
"headers": {
|
|
101
|
+
k: v
|
|
102
|
+
if k.lower() not in ("authorization", self.agent_config.auth_header.lower())
|
|
103
|
+
else "***"
|
|
104
|
+
for k, v in headers.items()
|
|
105
|
+
},
|
|
106
|
+
"body": request_data,
|
|
41
107
|
}
|
|
42
108
|
|
|
43
|
-
|
|
44
|
-
|
|
109
|
+
try:
|
|
110
|
+
response = await client.post(
|
|
111
|
+
url,
|
|
112
|
+
json=request_data,
|
|
113
|
+
headers=headers,
|
|
114
|
+
timeout=self.agent_config.timeout,
|
|
115
|
+
)
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
|
|
118
|
+
data = response.json()
|
|
119
|
+
|
|
120
|
+
if self.agent_config.debug and start_time:
|
|
121
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
122
|
+
debug_info = DebugInfo(
|
|
123
|
+
request=debug_request,
|
|
124
|
+
response={"status": response.status_code, "body": data},
|
|
125
|
+
duration_ms=duration_ms,
|
|
126
|
+
)
|
|
45
127
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
128
|
+
# Parse A2A response format
|
|
129
|
+
# A2A tasks have lifecycle: submitted, working, completed, failed, input-required
|
|
130
|
+
task_status = data.get("task", {}).get("status")
|
|
50
131
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
132
|
+
if task_status in ("completed", "working"):
|
|
133
|
+
# Extract the result from the response message
|
|
134
|
+
result_data = self._extract_result(data)
|
|
135
|
+
|
|
136
|
+
return TaskResult[Any](
|
|
137
|
+
status=TaskStatus.COMPLETED,
|
|
138
|
+
data=result_data,
|
|
139
|
+
success=True,
|
|
140
|
+
metadata={"task_id": data.get("task", {}).get("id")},
|
|
141
|
+
debug_info=debug_info,
|
|
57
142
|
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
data = response.json()
|
|
61
|
-
|
|
62
|
-
# Parse A2A response format
|
|
63
|
-
# A2A tasks have lifecycle: submitted, working, completed, failed, input-required
|
|
64
|
-
task_status = data.get("task", {}).get("status")
|
|
65
|
-
|
|
66
|
-
if task_status in ("completed", "working"):
|
|
67
|
-
# Extract the result from the response message
|
|
68
|
-
result_data = self._extract_result(data)
|
|
69
|
-
|
|
70
|
-
return TaskResult[Any](
|
|
71
|
-
status=TaskStatus.COMPLETED,
|
|
72
|
-
data=result_data,
|
|
73
|
-
success=True,
|
|
74
|
-
metadata={"task_id": data.get("task", {}).get("id")},
|
|
75
|
-
)
|
|
76
|
-
elif task_status == "failed":
|
|
77
|
-
return TaskResult[Any](
|
|
78
|
-
status=TaskStatus.FAILED,
|
|
79
|
-
error=data.get("message", {})
|
|
80
|
-
.get("parts", [{}])[0]
|
|
81
|
-
.get("text", "Task failed"),
|
|
82
|
-
success=False,
|
|
83
|
-
)
|
|
84
|
-
else:
|
|
85
|
-
# Handle other states (submitted, input-required)
|
|
86
|
-
return TaskResult[Any](
|
|
87
|
-
status=TaskStatus.SUBMITTED,
|
|
88
|
-
data=data,
|
|
89
|
-
success=True,
|
|
90
|
-
metadata={"task_id": data.get("task", {}).get("id")},
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
except httpx.HTTPError as e:
|
|
143
|
+
elif task_status == "failed":
|
|
94
144
|
return TaskResult[Any](
|
|
95
145
|
status=TaskStatus.FAILED,
|
|
96
|
-
error=
|
|
146
|
+
error=data.get("message", {}).get("parts", [{}])[0].get("text", "Task failed"),
|
|
97
147
|
success=False,
|
|
148
|
+
debug_info=debug_info,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
# Handle other states (submitted, input-required)
|
|
152
|
+
return TaskResult[Any](
|
|
153
|
+
status=TaskStatus.SUBMITTED,
|
|
154
|
+
data=data,
|
|
155
|
+
success=True,
|
|
156
|
+
metadata={"task_id": data.get("task", {}).get("id")},
|
|
157
|
+
debug_info=debug_info,
|
|
98
158
|
)
|
|
99
159
|
|
|
160
|
+
except httpx.HTTPError as e:
|
|
161
|
+
if self.agent_config.debug and start_time:
|
|
162
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
163
|
+
debug_info = DebugInfo(
|
|
164
|
+
request=debug_request,
|
|
165
|
+
response={"error": str(e)},
|
|
166
|
+
duration_ms=duration_ms,
|
|
167
|
+
)
|
|
168
|
+
return TaskResult[Any](
|
|
169
|
+
status=TaskStatus.FAILED,
|
|
170
|
+
error=str(e),
|
|
171
|
+
success=False,
|
|
172
|
+
debug_info=debug_info,
|
|
173
|
+
)
|
|
174
|
+
|
|
100
175
|
def _format_tool_request(self, tool_name: str, params: dict[str, Any]) -> str:
|
|
101
176
|
"""Format tool request as natural language for A2A."""
|
|
102
177
|
# For AdCP tools, we format as a structured request
|
|
@@ -135,25 +210,64 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
135
210
|
their capabilities through the agent card. For AdCP, we rely on the
|
|
136
211
|
standard AdCP tool set.
|
|
137
212
|
"""
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
213
|
+
client = await self._get_client()
|
|
214
|
+
|
|
215
|
+
headers = {"Content-Type": "application/json"}
|
|
216
|
+
|
|
217
|
+
if self.agent_config.auth_token:
|
|
218
|
+
# Support custom auth headers and types
|
|
219
|
+
if self.agent_config.auth_type == "bearer":
|
|
220
|
+
headers[self.agent_config.auth_header] = f"Bearer {self.agent_config.auth_token}"
|
|
221
|
+
else:
|
|
222
|
+
headers[self.agent_config.auth_header] = self.agent_config.auth_token
|
|
223
|
+
|
|
224
|
+
# Try to fetch agent card from standard A2A location
|
|
225
|
+
# A2A spec uses /.well-known/agent.json for agent card
|
|
226
|
+
url = f"{self.agent_config.agent_uri}/.well-known/agent.json"
|
|
227
|
+
|
|
228
|
+
logger.debug(f"Fetching A2A agent card for {self.agent_config.id} from {url}")
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
response = await client.get(url, headers=headers, timeout=self.agent_config.timeout)
|
|
232
|
+
response.raise_for_status()
|
|
233
|
+
|
|
234
|
+
data = response.json()
|
|
235
|
+
|
|
236
|
+
# Extract skills from agent card
|
|
237
|
+
skills = data.get("skills", [])
|
|
238
|
+
tool_names = [skill.get("name", "") for skill in skills if skill.get("name")]
|
|
239
|
+
|
|
240
|
+
logger.info(f"Found {len(tool_names)} tools from A2A agent {self.agent_config.id}")
|
|
241
|
+
return tool_names
|
|
242
|
+
|
|
243
|
+
except httpx.HTTPStatusError as e:
|
|
244
|
+
status_code = e.response.status_code
|
|
245
|
+
if status_code in (401, 403):
|
|
246
|
+
logger.error(f"Authentication failed for A2A agent {self.agent_config.id}")
|
|
247
|
+
raise ADCPAuthenticationError(
|
|
248
|
+
f"Authentication failed: HTTP {status_code}",
|
|
249
|
+
agent_id=self.agent_config.id,
|
|
250
|
+
agent_uri=self.agent_config.agent_uri,
|
|
251
|
+
) from e
|
|
252
|
+
else:
|
|
253
|
+
logger.error(f"HTTP {status_code} error fetching agent card: {e}")
|
|
254
|
+
raise ADCPConnectionError(
|
|
255
|
+
f"Failed to fetch agent card: HTTP {status_code}",
|
|
256
|
+
agent_id=self.agent_config.id,
|
|
257
|
+
agent_uri=self.agent_config.agent_uri,
|
|
258
|
+
) from e
|
|
259
|
+
except httpx.TimeoutException as e:
|
|
260
|
+
logger.error(f"Timeout fetching agent card for {self.agent_config.id}")
|
|
261
|
+
raise ADCPTimeoutError(
|
|
262
|
+
f"Timeout fetching agent card: {e}",
|
|
263
|
+
agent_id=self.agent_config.id,
|
|
264
|
+
agent_uri=self.agent_config.agent_uri,
|
|
265
|
+
timeout=self.agent_config.timeout,
|
|
266
|
+
) from e
|
|
267
|
+
except httpx.HTTPError as e:
|
|
268
|
+
logger.error(f"HTTP error fetching agent card: {e}")
|
|
269
|
+
raise ADCPConnectionError(
|
|
270
|
+
f"Failed to fetch agent card: {e}",
|
|
271
|
+
agent_id=self.agent_config.id,
|
|
272
|
+
agent_uri=self.agent_config.agent_uri,
|
|
273
|
+
) from e
|
adcp/protocols/base.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""Base protocol adapter interface."""
|
|
2
4
|
|
|
3
5
|
from abc import ABC, abstractmethod
|
|
@@ -36,3 +38,12 @@ class ProtocolAdapter(ABC):
|
|
|
36
38
|
List of tool names
|
|
37
39
|
"""
|
|
38
40
|
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def close(self) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Close the adapter and clean up resources.
|
|
46
|
+
|
|
47
|
+
Implementations should close any open connections, clients, or other resources.
|
|
48
|
+
"""
|
|
49
|
+
pass
|