agent0-sdk 0.2.0__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.
- agent0_sdk/__init__.py +52 -0
- agent0_sdk/core/agent.py +860 -0
- agent0_sdk/core/contracts.py +490 -0
- agent0_sdk/core/endpoint_crawler.py +270 -0
- agent0_sdk/core/feedback_manager.py +923 -0
- agent0_sdk/core/indexer.py +1016 -0
- agent0_sdk/core/ipfs_client.py +355 -0
- agent0_sdk/core/models.py +311 -0
- agent0_sdk/core/sdk.py +842 -0
- agent0_sdk/core/subgraph_client.py +813 -0
- agent0_sdk/core/web3_client.py +192 -0
- agent0_sdk-0.2.0.dist-info/METADATA +308 -0
- agent0_sdk-0.2.0.dist-info/RECORD +27 -0
- agent0_sdk-0.2.0.dist-info/WHEEL +5 -0
- agent0_sdk-0.2.0.dist-info/licenses/LICENSE +22 -0
- agent0_sdk-0.2.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/config.py +46 -0
- tests/conftest.py +22 -0
- tests/test_feedback.py +417 -0
- tests/test_models.py +224 -0
- tests/test_real_public_servers.py +103 -0
- tests/test_registration.py +267 -0
- tests/test_registrationIpfs.py +227 -0
- tests/test_sdk.py +238 -0
- tests/test_search.py +271 -0
- tests/test_transfer.py +255 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Endpoint Crawler for MCP and A2A Servers
|
|
3
|
+
Automatically fetches capabilities (tools, prompts, resources, skills) from endpoints
|
|
4
|
+
when an agent is registered. Uses soft failure - never blocks registration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import requests
|
|
9
|
+
import json
|
|
10
|
+
from typing import Optional, Dict, Any, List
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# JSON-RPC helpers
|
|
16
|
+
def create_jsonrpc_request(method: str, params: Dict = None, request_id: int = 1):
|
|
17
|
+
"""Create a JSON-RPC request."""
|
|
18
|
+
payload = {
|
|
19
|
+
"jsonrpc": "2.0",
|
|
20
|
+
"method": method,
|
|
21
|
+
"id": request_id
|
|
22
|
+
}
|
|
23
|
+
if params:
|
|
24
|
+
payload["params"] = params
|
|
25
|
+
return payload
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EndpointCrawler:
|
|
29
|
+
"""Crawls MCP and A2A endpoints to fetch capabilities."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, timeout: int = 5):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the endpoint crawler.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
timeout: Request timeout in seconds (default: 5)
|
|
37
|
+
"""
|
|
38
|
+
self.timeout = timeout
|
|
39
|
+
|
|
40
|
+
def fetch_mcp_capabilities(self, endpoint: str) -> Optional[Dict[str, Any]]:
|
|
41
|
+
"""
|
|
42
|
+
Fetch MCP capabilities (tools, prompts, resources) from an MCP server.
|
|
43
|
+
|
|
44
|
+
MCP Protocol uses JSON-RPC over HTTP POST. Tries JSON-RPC first,
|
|
45
|
+
then falls back to static agentcard.json.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
endpoint: MCP endpoint URL (must be http:// or https://)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict with keys: 'mcpTools', 'mcpPrompts', 'mcpResources'
|
|
52
|
+
Returns None if unable to fetch
|
|
53
|
+
"""
|
|
54
|
+
# Ensure endpoint is HTTP/HTTPS
|
|
55
|
+
if not endpoint.startswith(('http://', 'https://')):
|
|
56
|
+
logger.warning(f"MCP endpoint must be HTTP/HTTPS, got: {endpoint}")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Try JSON-RPC approach first (for real MCP servers)
|
|
60
|
+
capabilities = self._fetch_via_jsonrpc(endpoint)
|
|
61
|
+
if capabilities:
|
|
62
|
+
return capabilities
|
|
63
|
+
|
|
64
|
+
# Fallback to static agentcard.json
|
|
65
|
+
try:
|
|
66
|
+
agentcard_url = f"{endpoint}/agentcard.json"
|
|
67
|
+
logger.debug(f"Attempting to fetch MCP capabilities from {agentcard_url}")
|
|
68
|
+
|
|
69
|
+
response = requests.get(agentcard_url, timeout=self.timeout, allow_redirects=True)
|
|
70
|
+
|
|
71
|
+
if response.status_code == 200:
|
|
72
|
+
data = response.json()
|
|
73
|
+
|
|
74
|
+
# Extract capabilities from agentcard
|
|
75
|
+
capabilities = {
|
|
76
|
+
'mcpTools': self._extract_list(data, 'tools'),
|
|
77
|
+
'mcpPrompts': self._extract_list(data, 'prompts'),
|
|
78
|
+
'mcpResources': self._extract_list(data, 'resources')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if any(capabilities.values()):
|
|
82
|
+
logger.info(f"Successfully fetched MCP capabilities from {endpoint}")
|
|
83
|
+
return capabilities
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.debug(f"Could not fetch MCP capabilities from {endpoint}: {e}")
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def _fetch_via_jsonrpc(self, http_url: str) -> Optional[Dict[str, Any]]:
|
|
91
|
+
"""Try to fetch capabilities via JSON-RPC."""
|
|
92
|
+
try:
|
|
93
|
+
# Try to call list_tools, list_resources, list_prompts
|
|
94
|
+
tools = self._jsonrpc_call(http_url, "tools/list")
|
|
95
|
+
resources = self._jsonrpc_call(http_url, "resources/list")
|
|
96
|
+
prompts = self._jsonrpc_call(http_url, "prompts/list")
|
|
97
|
+
|
|
98
|
+
mcp_tools = []
|
|
99
|
+
mcp_resources = []
|
|
100
|
+
mcp_prompts = []
|
|
101
|
+
|
|
102
|
+
# Extract names from tools
|
|
103
|
+
if tools and isinstance(tools, dict) and "tools" in tools:
|
|
104
|
+
for tool in tools["tools"]:
|
|
105
|
+
if isinstance(tool, dict) and "name" in tool:
|
|
106
|
+
mcp_tools.append(tool["name"])
|
|
107
|
+
|
|
108
|
+
# Extract names from resources
|
|
109
|
+
if resources and isinstance(resources, dict) and "resources" in resources:
|
|
110
|
+
for resource in resources["resources"]:
|
|
111
|
+
if isinstance(resource, dict) and "name" in resource:
|
|
112
|
+
mcp_resources.append(resource["name"])
|
|
113
|
+
|
|
114
|
+
# Extract names from prompts
|
|
115
|
+
if prompts and isinstance(prompts, dict) and "prompts" in prompts:
|
|
116
|
+
for prompt in prompts["prompts"]:
|
|
117
|
+
if isinstance(prompt, dict) and "name" in prompt:
|
|
118
|
+
mcp_prompts.append(prompt["name"])
|
|
119
|
+
|
|
120
|
+
if mcp_tools or mcp_resources or mcp_prompts:
|
|
121
|
+
logger.info(f"Successfully fetched MCP capabilities via JSON-RPC")
|
|
122
|
+
return {
|
|
123
|
+
'mcpTools': mcp_tools,
|
|
124
|
+
'mcpResources': mcp_resources,
|
|
125
|
+
'mcpPrompts': mcp_prompts
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.debug(f"JSON-RPC approach failed: {e}")
|
|
130
|
+
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def _jsonrpc_call(self, url: str, method: str, params: Dict = None) -> Optional[Dict[str, Any]]:
|
|
134
|
+
"""Make a JSON-RPC call and return the result. Handles SSE format."""
|
|
135
|
+
try:
|
|
136
|
+
payload = create_jsonrpc_request(method, params or {})
|
|
137
|
+
headers = {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
'Accept': 'application/json, text/event-stream'
|
|
140
|
+
}
|
|
141
|
+
response = requests.post(url, json=payload, timeout=self.timeout, headers=headers, stream=True)
|
|
142
|
+
|
|
143
|
+
if response.status_code == 200:
|
|
144
|
+
# Check if response is SSE format
|
|
145
|
+
content_type = response.headers.get('content-type', '')
|
|
146
|
+
if 'text/event-stream' in content_type or 'event: message' in response.text[:200]:
|
|
147
|
+
# Parse SSE format
|
|
148
|
+
result = self._parse_sse_response(response.text)
|
|
149
|
+
if result:
|
|
150
|
+
return result
|
|
151
|
+
else:
|
|
152
|
+
# Regular JSON response
|
|
153
|
+
result = response.json()
|
|
154
|
+
if "result" in result:
|
|
155
|
+
return result["result"]
|
|
156
|
+
return result
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.debug(f"JSON-RPC call {method} failed: {e}")
|
|
159
|
+
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def _parse_sse_response(self, sse_text: str) -> Optional[Dict[str, Any]]:
|
|
163
|
+
"""Parse Server-Sent Events (SSE) format response."""
|
|
164
|
+
try:
|
|
165
|
+
# Look for "data:" lines containing JSON
|
|
166
|
+
for line in sse_text.split('\n'):
|
|
167
|
+
if line.startswith('data: '):
|
|
168
|
+
json_str = line[6:] # Remove "data: " prefix
|
|
169
|
+
data = json.loads(json_str)
|
|
170
|
+
if "result" in data:
|
|
171
|
+
return data["result"]
|
|
172
|
+
return data
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.debug(f"Failed to parse SSE response: {e}")
|
|
175
|
+
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def fetch_a2a_capabilities(self, endpoint: str) -> Optional[Dict[str, Any]]:
|
|
179
|
+
"""
|
|
180
|
+
Fetch A2A capabilities (skills) from an A2A server.
|
|
181
|
+
|
|
182
|
+
A2A Protocol uses agent cards to describe agent capabilities.
|
|
183
|
+
Tries multiple well-known paths: agentcard.json, .well-known/agent.json
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
endpoint: A2A endpoint URL (must be http:// or https://)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Dict with key: 'a2aSkills'
|
|
190
|
+
Returns None if unable to fetch
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
# Ensure endpoint is HTTP/HTTPS
|
|
194
|
+
if not endpoint.startswith(('http://', 'https://')):
|
|
195
|
+
logger.warning(f"A2A endpoint must be HTTP/HTTPS, got: {endpoint}")
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
# Try multiple well-known paths for A2A agent cards
|
|
199
|
+
agentcard_urls = [
|
|
200
|
+
f"{endpoint}/agentcard.json",
|
|
201
|
+
f"{endpoint}/.well-known/agent.json",
|
|
202
|
+
f"{endpoint.rstrip('/')}/.well-known/agent.json"
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
for agentcard_url in agentcard_urls:
|
|
206
|
+
logger.debug(f"Attempting to fetch A2A capabilities from {agentcard_url}")
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
response = requests.get(agentcard_url, timeout=self.timeout, allow_redirects=True)
|
|
210
|
+
|
|
211
|
+
if response.status_code == 200:
|
|
212
|
+
data = response.json()
|
|
213
|
+
|
|
214
|
+
# Extract skills from agentcard
|
|
215
|
+
skills = self._extract_list(data, 'skills')
|
|
216
|
+
|
|
217
|
+
if skills:
|
|
218
|
+
logger.info(f"Successfully fetched A2A capabilities from {endpoint}")
|
|
219
|
+
return {'a2aSkills': skills}
|
|
220
|
+
except requests.exceptions.RequestException:
|
|
221
|
+
# Try next URL
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.debug(f"Unexpected error fetching A2A capabilities from {endpoint}: {e}")
|
|
226
|
+
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def _extract_list(self, data: Dict[str, Any], key: str) -> List[str]:
|
|
230
|
+
"""
|
|
231
|
+
Extract a list of strings from nested JSON data.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
data: JSON data dictionary
|
|
235
|
+
key: Key to extract (e.g., 'tools', 'prompts', 'resources', 'skills')
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of string names/IDs
|
|
239
|
+
"""
|
|
240
|
+
result = []
|
|
241
|
+
|
|
242
|
+
# Try top-level key
|
|
243
|
+
if key in data and isinstance(data[key], list):
|
|
244
|
+
for item in data[key]:
|
|
245
|
+
if isinstance(item, str):
|
|
246
|
+
result.append(item)
|
|
247
|
+
elif isinstance(item, dict):
|
|
248
|
+
# For objects, try to extract name/id field
|
|
249
|
+
for name_field in ['name', 'id', 'identifier', 'title']:
|
|
250
|
+
if name_field in item and isinstance(item[name_field], str):
|
|
251
|
+
result.append(item[name_field])
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
# Try nested in 'capabilities' or 'abilities'
|
|
255
|
+
if not result:
|
|
256
|
+
for container_key in ['capabilities', 'abilities', 'features']:
|
|
257
|
+
if container_key in data and isinstance(data[container_key], dict):
|
|
258
|
+
if key in data[container_key] and isinstance(data[container_key][key], list):
|
|
259
|
+
for item in data[container_key][key]:
|
|
260
|
+
if isinstance(item, str):
|
|
261
|
+
result.append(item)
|
|
262
|
+
elif isinstance(item, dict):
|
|
263
|
+
for name_field in ['name', 'id', 'identifier', 'title']:
|
|
264
|
+
if name_field in item and isinstance(item[name_field], str):
|
|
265
|
+
result.append(item[name_field])
|
|
266
|
+
break
|
|
267
|
+
if result:
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
return result
|