agent0-sdk 1.4.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 +57 -0
- agent0_sdk/core/agent.py +1187 -0
- agent0_sdk/core/contracts.py +547 -0
- agent0_sdk/core/endpoint_crawler.py +330 -0
- agent0_sdk/core/feedback_manager.py +1052 -0
- agent0_sdk/core/indexer.py +1837 -0
- agent0_sdk/core/ipfs_client.py +357 -0
- agent0_sdk/core/models.py +303 -0
- agent0_sdk/core/oasf_validator.py +98 -0
- agent0_sdk/core/sdk.py +1005 -0
- agent0_sdk/core/subgraph_client.py +853 -0
- agent0_sdk/core/transaction_handle.py +71 -0
- agent0_sdk/core/value_encoding.py +91 -0
- agent0_sdk/core/web3_client.py +399 -0
- agent0_sdk/taxonomies/all_domains.json +1565 -0
- agent0_sdk/taxonomies/all_skills.json +1030 -0
- agent0_sdk-1.4.0.dist-info/METADATA +403 -0
- agent0_sdk-1.4.0.dist-info/RECORD +21 -0
- agent0_sdk-1.4.0.dist-info/WHEEL +5 -0
- agent0_sdk-1.4.0.dist-info/licenses/LICENSE +22 -0
- agent0_sdk-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IPFS client for decentralized storage with support for multiple providers:
|
|
3
|
+
- Local IPFS nodes
|
|
4
|
+
- Pinata IPFS pinning service
|
|
5
|
+
- Filecoin Pin service
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import subprocess
|
|
13
|
+
import tempfile
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import ipfshttpclient
|
|
20
|
+
except ImportError:
|
|
21
|
+
ipfshttpclient = None
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IPFSClient:
|
|
27
|
+
"""Client for IPFS operations supporting multiple providers (local IPFS, Pinata, Filecoin Pin)."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
url: Optional[str] = None,
|
|
32
|
+
filecoin_pin_enabled: bool = False,
|
|
33
|
+
filecoin_private_key: Optional[str] = None,
|
|
34
|
+
pinata_enabled: bool = False,
|
|
35
|
+
pinata_jwt: Optional[str] = None
|
|
36
|
+
):
|
|
37
|
+
"""Initialize IPFS client.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
url: IPFS node URL (e.g., "http://localhost:5001")
|
|
41
|
+
filecoin_pin_enabled: Enable Filecoin Pin integration
|
|
42
|
+
filecoin_private_key: Private key for Filecoin Pin operations
|
|
43
|
+
pinata_enabled: Enable Pinata integration
|
|
44
|
+
pinata_jwt: JWT token for Pinata authentication
|
|
45
|
+
"""
|
|
46
|
+
self.url = url
|
|
47
|
+
self.filecoin_pin_enabled = filecoin_pin_enabled
|
|
48
|
+
self.filecoin_private_key = filecoin_private_key
|
|
49
|
+
self.pinata_enabled = pinata_enabled
|
|
50
|
+
self.pinata_jwt = pinata_jwt
|
|
51
|
+
self.client = None
|
|
52
|
+
|
|
53
|
+
if pinata_enabled:
|
|
54
|
+
self._verify_pinata_jwt()
|
|
55
|
+
elif filecoin_pin_enabled:
|
|
56
|
+
self._verify_filecoin_pin_installation()
|
|
57
|
+
elif url and ipfshttpclient:
|
|
58
|
+
self.client = ipfshttpclient.connect(url)
|
|
59
|
+
elif url and not ipfshttpclient:
|
|
60
|
+
raise ImportError(
|
|
61
|
+
"IPFS dependencies not installed. Install with: pip install ipfshttpclient"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _verify_pinata_jwt(self):
|
|
65
|
+
"""Verify Pinata JWT is provided."""
|
|
66
|
+
if not self.pinata_jwt:
|
|
67
|
+
raise ValueError("pinata_jwt is required when pinata_enabled=True")
|
|
68
|
+
logger.debug("Pinata JWT configured")
|
|
69
|
+
|
|
70
|
+
def _verify_filecoin_pin_installation(self):
|
|
71
|
+
"""Verify filecoin-pin CLI is installed."""
|
|
72
|
+
try:
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
['filecoin-pin', '--version'],
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
check=True
|
|
78
|
+
)
|
|
79
|
+
logger.debug(f"Filecoin Pin CLI found: {result.stdout.strip()}")
|
|
80
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"filecoin-pin CLI not found. "
|
|
83
|
+
"Install it from: https://github.com/filecoin-project/filecoin-pin?tab=readme-ov-file#cli"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _pin_to_filecoin(self, file_path: str) -> str:
|
|
87
|
+
"""Pin file to Filecoin using filecoin-pin CLI following the official guide."""
|
|
88
|
+
# Check if environment file exists (as per guide)
|
|
89
|
+
env_file = os.path.expanduser("~/.filecoin-pin-env")
|
|
90
|
+
if not os.path.exists(env_file):
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
"Filecoin Pin environment file not found. Please run:\n"
|
|
93
|
+
" 1. cast wallet new\n"
|
|
94
|
+
" 2. Create ~/.filecoin-pin-env with PRIVATE_KEY and WALLET_ADDRESS\n"
|
|
95
|
+
" 3. Get testnet tokens from https://faucet.calibnet.chainsafe-fil.io/\n"
|
|
96
|
+
" 4. filecoin-pin payments setup --auto (one-time setup)"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Load environment from file (as per guide)
|
|
100
|
+
env = os.environ.copy()
|
|
101
|
+
try:
|
|
102
|
+
with open(env_file, 'r') as f:
|
|
103
|
+
for line in f:
|
|
104
|
+
if line.startswith('export '):
|
|
105
|
+
key, value = line[7:].strip().split('=', 1)
|
|
106
|
+
env[key] = value.strip('"\'')
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise RuntimeError(f"Error loading Filecoin Pin environment: {e}")
|
|
109
|
+
|
|
110
|
+
if 'PRIVATE_KEY' not in env:
|
|
111
|
+
raise RuntimeError("PRIVATE_KEY not found in environment file")
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
import time
|
|
115
|
+
cmd = ['filecoin-pin', 'add', '--bare', file_path]
|
|
116
|
+
logger.debug(f"Running Filecoin CLI command: {' '.join(cmd)}")
|
|
117
|
+
|
|
118
|
+
start_time = time.time()
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
cmd,
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
check=True,
|
|
124
|
+
env=env
|
|
125
|
+
)
|
|
126
|
+
elapsed_time = time.time() - start_time
|
|
127
|
+
logger.debug(f"Filecoin CLI completed in {elapsed_time:.2f} seconds")
|
|
128
|
+
|
|
129
|
+
# Parse the output to extract Root CID
|
|
130
|
+
lines = result.stdout.strip().split('\n')
|
|
131
|
+
for line in lines:
|
|
132
|
+
if 'Root CID:' in line:
|
|
133
|
+
return line.split('Root CID:')[1].strip()
|
|
134
|
+
|
|
135
|
+
# Fallback: return the first line if parsing fails
|
|
136
|
+
return lines[0] if lines else "unknown"
|
|
137
|
+
|
|
138
|
+
except subprocess.CalledProcessError as e:
|
|
139
|
+
# Handle specific error cases from the guide
|
|
140
|
+
stderr_lower = e.stderr.lower()
|
|
141
|
+
if "insufficient fil" in stderr_lower or "balance" in stderr_lower:
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
f"Insufficient FIL for gas fees: {e.stderr}\n"
|
|
144
|
+
"Get test FIL from: https://faucet.calibnet.chainsafe-fil.io/\n"
|
|
145
|
+
"Then run: filecoin-pin payments setup --auto (one-time setup)"
|
|
146
|
+
)
|
|
147
|
+
elif "payment" in stderr_lower or "setup" in stderr_lower:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
f"Payment setup required: {e.stderr}\n"
|
|
150
|
+
"Run: filecoin-pin payments setup --auto (one-time setup)"
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
raise RuntimeError(f"Filecoin Pin 'add' command failed: {e.stderr}")
|
|
154
|
+
|
|
155
|
+
def _pin_to_local_ipfs(self, data: str, **kwargs) -> str:
|
|
156
|
+
"""Pin data to local IPFS node."""
|
|
157
|
+
if not self.client:
|
|
158
|
+
raise RuntimeError("No IPFS client available")
|
|
159
|
+
result = self.client.add_str(data, **kwargs)
|
|
160
|
+
# add_str returns the CID directly as a string
|
|
161
|
+
return result if isinstance(result, str) else result['Hash']
|
|
162
|
+
|
|
163
|
+
def _pin_to_pinata(self, data: str, file_name: str = "file.json") -> str:
|
|
164
|
+
"""Pin data to Pinata using JWT authentication with v3 API."""
|
|
165
|
+
import requests
|
|
166
|
+
import tempfile
|
|
167
|
+
import os
|
|
168
|
+
|
|
169
|
+
# Pinata v3 API endpoint for uploading files
|
|
170
|
+
url = "https://uploads.pinata.cloud/v3/files"
|
|
171
|
+
|
|
172
|
+
# Pinata authentication using JWT
|
|
173
|
+
headers = {
|
|
174
|
+
"Authorization": f"Bearer {self.pinata_jwt}"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Create a temporary file with the data
|
|
178
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
179
|
+
f.write(data)
|
|
180
|
+
temp_path = f.name
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
logger.debug("Pinning to Pinata v3 (public)")
|
|
184
|
+
|
|
185
|
+
# Prepare the file for upload with public network setting
|
|
186
|
+
with open(temp_path, 'rb') as file:
|
|
187
|
+
files = {
|
|
188
|
+
'file': (file_name, file, 'application/json')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Add network parameter to make file public
|
|
192
|
+
data = {
|
|
193
|
+
'network': 'public'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
response = requests.post(url, headers=headers, files=files, data=data)
|
|
197
|
+
|
|
198
|
+
response.raise_for_status()
|
|
199
|
+
result = response.json()
|
|
200
|
+
|
|
201
|
+
# v3 API returns different structure - CID is nested in data
|
|
202
|
+
cid = None
|
|
203
|
+
if 'data' in result and 'cid' in result['data']:
|
|
204
|
+
cid = result['data']['cid']
|
|
205
|
+
elif 'cid' in result:
|
|
206
|
+
cid = result['cid']
|
|
207
|
+
elif 'IpfsHash' in result:
|
|
208
|
+
cid = result['IpfsHash']
|
|
209
|
+
|
|
210
|
+
if not cid:
|
|
211
|
+
error_msg = f"No CID returned from Pinata. Response: {result}"
|
|
212
|
+
logger.error(error_msg)
|
|
213
|
+
raise ValueError(error_msg)
|
|
214
|
+
logger.debug(f"Pinned to Pinata v3: {cid}")
|
|
215
|
+
return cid
|
|
216
|
+
except requests.exceptions.HTTPError as e:
|
|
217
|
+
error_details = ""
|
|
218
|
+
if hasattr(e, 'response') and e.response is not None:
|
|
219
|
+
error_details = f" Response: {e.response.text}"
|
|
220
|
+
error_msg = f"Failed to pin to Pinata: HTTP {e}{error_details}"
|
|
221
|
+
logger.error(error_msg)
|
|
222
|
+
raise RuntimeError(error_msg)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
error_msg = f"Failed to pin to Pinata: {e}"
|
|
225
|
+
logger.error(error_msg)
|
|
226
|
+
raise RuntimeError(error_msg)
|
|
227
|
+
finally:
|
|
228
|
+
# Clean up temporary file
|
|
229
|
+
try:
|
|
230
|
+
os.unlink(temp_path)
|
|
231
|
+
except:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
def add(self, data: str, **kwargs) -> str:
|
|
235
|
+
"""Add data to IPFS and return CID."""
|
|
236
|
+
file_name = kwargs.pop("file_name", None)
|
|
237
|
+
if self.pinata_enabled:
|
|
238
|
+
return self._pin_to_pinata(data, file_name=file_name or "file.json")
|
|
239
|
+
elif self.filecoin_pin_enabled:
|
|
240
|
+
# Create temporary file for Filecoin Pin
|
|
241
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
242
|
+
f.write(data)
|
|
243
|
+
temp_path = f.name
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
cid = self._pin_to_filecoin(temp_path)
|
|
247
|
+
return cid
|
|
248
|
+
finally:
|
|
249
|
+
os.unlink(temp_path)
|
|
250
|
+
else:
|
|
251
|
+
return self._pin_to_local_ipfs(data, **kwargs)
|
|
252
|
+
|
|
253
|
+
def add_file(self, filepath: str, **kwargs) -> str:
|
|
254
|
+
"""Add file to IPFS and return CID."""
|
|
255
|
+
file_name = kwargs.pop("file_name", None)
|
|
256
|
+
if self.pinata_enabled:
|
|
257
|
+
# Read file and send to Pinata
|
|
258
|
+
with open(filepath, 'r') as f:
|
|
259
|
+
data = f.read()
|
|
260
|
+
return self._pin_to_pinata(data, file_name=file_name or "file.json")
|
|
261
|
+
elif self.filecoin_pin_enabled:
|
|
262
|
+
return self._pin_to_filecoin(filepath)
|
|
263
|
+
else:
|
|
264
|
+
if not self.client:
|
|
265
|
+
raise RuntimeError("No IPFS client available")
|
|
266
|
+
result = self.client.add(filepath, **kwargs)
|
|
267
|
+
return result['Hash']
|
|
268
|
+
|
|
269
|
+
def get(self, cid: str) -> str:
|
|
270
|
+
"""Get data from IPFS by CID."""
|
|
271
|
+
# Extract CID from IPFS URL if needed
|
|
272
|
+
if cid.startswith("ipfs://"):
|
|
273
|
+
cid = cid[7:] # Remove "ipfs://" prefix
|
|
274
|
+
|
|
275
|
+
# Pinata and Filecoin Pin both use IPFS gateways for retrieval
|
|
276
|
+
if self.pinata_enabled or self.filecoin_pin_enabled:
|
|
277
|
+
# Use IPFS gateways for retrieval
|
|
278
|
+
import requests
|
|
279
|
+
try:
|
|
280
|
+
# Try multiple gateways for reliability, prioritizing Pinata v3 gateway
|
|
281
|
+
gateways = [
|
|
282
|
+
f"https://gateway.pinata.cloud/ipfs/{cid}",
|
|
283
|
+
f"https://ipfs.io/ipfs/{cid}",
|
|
284
|
+
f"https://dweb.link/ipfs/{cid}"
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
for gateway in gateways:
|
|
288
|
+
try:
|
|
289
|
+
response = requests.get(gateway, timeout=10)
|
|
290
|
+
response.raise_for_status()
|
|
291
|
+
return response.text
|
|
292
|
+
except Exception:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
raise RuntimeError(f"Failed to retrieve data from all IPFS gateways")
|
|
296
|
+
except Exception as e:
|
|
297
|
+
raise RuntimeError(f"Failed to retrieve data from IPFS gateway: {e}")
|
|
298
|
+
else:
|
|
299
|
+
if not self.client:
|
|
300
|
+
raise RuntimeError("No IPFS client available")
|
|
301
|
+
return self.client.cat(cid).decode('utf-8')
|
|
302
|
+
|
|
303
|
+
def get_json(self, cid: str) -> Dict[str, Any]:
|
|
304
|
+
"""Get JSON data from IPFS by CID."""
|
|
305
|
+
data = self.get(cid)
|
|
306
|
+
return json.loads(data)
|
|
307
|
+
|
|
308
|
+
def pin(self, cid: str) -> Dict[str, Any]:
|
|
309
|
+
"""Pin a CID to local node."""
|
|
310
|
+
if self.filecoin_pin_enabled:
|
|
311
|
+
# Filecoin Pin automatically pins data, so this is a no-op
|
|
312
|
+
return {"pinned": [cid]}
|
|
313
|
+
else:
|
|
314
|
+
if not self.client:
|
|
315
|
+
raise RuntimeError("No IPFS client available")
|
|
316
|
+
return self.client.pin.add(cid)
|
|
317
|
+
|
|
318
|
+
def unpin(self, cid: str) -> Dict[str, Any]:
|
|
319
|
+
"""Unpin a CID from local node."""
|
|
320
|
+
if self.filecoin_pin_enabled:
|
|
321
|
+
# Filecoin Pin doesn't support unpinning in the same way
|
|
322
|
+
# This is a no-op for Filecoin Pin
|
|
323
|
+
return {"unpinned": [cid]}
|
|
324
|
+
else:
|
|
325
|
+
if not self.client:
|
|
326
|
+
raise RuntimeError("No IPFS client available")
|
|
327
|
+
return self.client.pin.rm(cid)
|
|
328
|
+
|
|
329
|
+
def add_json(self, data: Dict[str, Any], **kwargs) -> str:
|
|
330
|
+
"""Add JSON data to IPFS and return CID."""
|
|
331
|
+
json_str = json.dumps(data, indent=2)
|
|
332
|
+
return self.add(json_str, **kwargs)
|
|
333
|
+
|
|
334
|
+
def addRegistrationFile(self, registrationFile: "RegistrationFile", chainId: Optional[int] = None, identityRegistryAddress: Optional[str] = None, **kwargs) -> str:
|
|
335
|
+
"""Add registration file to IPFS and return CID."""
|
|
336
|
+
data = registrationFile.to_dict(chain_id=chainId, identity_registry_address=identityRegistryAddress)
|
|
337
|
+
return self.add_json(data, file_name="agent-registration.json", **kwargs)
|
|
338
|
+
|
|
339
|
+
def getRegistrationFile(self, cid: str) -> "RegistrationFile":
|
|
340
|
+
"""Get registration file from IPFS by CID."""
|
|
341
|
+
from .models import RegistrationFile
|
|
342
|
+
data = self.get_json(cid)
|
|
343
|
+
return RegistrationFile.from_dict(data)
|
|
344
|
+
|
|
345
|
+
def addFeedbackFile(self, feedbackData: Dict[str, Any], **kwargs) -> str:
|
|
346
|
+
"""Add feedback file to IPFS and return CID."""
|
|
347
|
+
return self.add_json(feedbackData, file_name="feedback.json", **kwargs)
|
|
348
|
+
|
|
349
|
+
def getFeedbackFile(self, cid: str) -> Dict[str, Any]:
|
|
350
|
+
"""Get feedback file from IPFS by CID."""
|
|
351
|
+
return self.get_json(cid)
|
|
352
|
+
|
|
353
|
+
def close(self):
|
|
354
|
+
"""Close IPFS client connection."""
|
|
355
|
+
if hasattr(self.client, 'close'):
|
|
356
|
+
self.client.close()
|
|
357
|
+
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core data models for the Agent0 SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union, Literal
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Type aliases
|
|
15
|
+
AgentId = str # "chainId:tokenId" (e.g., "8453:1234") or just tokenId when chain is implicit
|
|
16
|
+
ChainId = int
|
|
17
|
+
Address = str # 0x-hex
|
|
18
|
+
URI = str # https://... or ipfs://...
|
|
19
|
+
CID = str # IPFS CID (if used)
|
|
20
|
+
Timestamp = int # unix seconds
|
|
21
|
+
IdemKey = str # idempotency key for write ops
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EndpointType(Enum):
|
|
25
|
+
"""Types of endpoints that agents can advertise."""
|
|
26
|
+
MCP = "MCP"
|
|
27
|
+
A2A = "A2A"
|
|
28
|
+
ENS = "ENS"
|
|
29
|
+
DID = "DID"
|
|
30
|
+
WALLET = "wallet"
|
|
31
|
+
OASF = "OASF"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TrustModel(Enum):
|
|
35
|
+
"""Trust models supported by the SDK."""
|
|
36
|
+
REPUTATION = "reputation"
|
|
37
|
+
CRYPTO_ECONOMIC = "crypto-economic"
|
|
38
|
+
TEE_ATTESTATION = "tee-attestation"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Endpoint:
|
|
43
|
+
"""Represents an agent endpoint."""
|
|
44
|
+
type: EndpointType
|
|
45
|
+
value: str # endpoint value (URL, name, DID, ENS)
|
|
46
|
+
meta: Dict[str, Any] = field(default_factory=dict) # optional metadata
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class RegistrationFile:
|
|
51
|
+
"""Agent registration file structure."""
|
|
52
|
+
agentId: Optional[AgentId] = None # None until minted
|
|
53
|
+
agentURI: Optional[URI] = None # where this file is (or will be) published
|
|
54
|
+
name: str = ""
|
|
55
|
+
description: str = ""
|
|
56
|
+
image: Optional[URI] = None
|
|
57
|
+
walletAddress: Optional[Address] = None
|
|
58
|
+
walletChainId: Optional[int] = None # Chain ID for the wallet address
|
|
59
|
+
endpoints: List[Endpoint] = field(default_factory=list)
|
|
60
|
+
trustModels: List[Union[TrustModel, str]] = field(default_factory=list)
|
|
61
|
+
owners: List[Address] = field(default_factory=list) # from chain (read-only, hydrated)
|
|
62
|
+
operators: List[Address] = field(default_factory=list) # from chain (read-only, hydrated)
|
|
63
|
+
active: bool = False # SDK extension flag
|
|
64
|
+
x402support: bool = False # Binary flag for x402 payment support
|
|
65
|
+
metadata: Dict[str, Any] = field(default_factory=dict) # arbitrary, SDK-managed
|
|
66
|
+
updatedAt: Timestamp = field(default_factory=lambda: int(datetime.now().timestamp()))
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
"""String representation as JSON."""
|
|
70
|
+
# Use stored registry info if available
|
|
71
|
+
chain_id = getattr(self, '_chain_id', None)
|
|
72
|
+
registry_address = getattr(self, '_registry_address', None)
|
|
73
|
+
return json.dumps(self.to_dict(chain_id, registry_address), indent=2, default=str)
|
|
74
|
+
|
|
75
|
+
def __repr__(self) -> str:
|
|
76
|
+
"""Developer representation."""
|
|
77
|
+
return f"RegistrationFile(agentId={self.agentId}, agentURI={self.agentURI}, name={self.name})"
|
|
78
|
+
|
|
79
|
+
def to_dict(self, chain_id: Optional[int] = None, identity_registry_address: Optional[str] = None) -> Dict[str, Any]:
|
|
80
|
+
"""Convert to dictionary for JSON serialization."""
|
|
81
|
+
# Build endpoints array
|
|
82
|
+
endpoints = []
|
|
83
|
+
for endpoint in self.endpoints:
|
|
84
|
+
endpoint_dict = {
|
|
85
|
+
"name": endpoint.type.value,
|
|
86
|
+
"endpoint": endpoint.value,
|
|
87
|
+
**endpoint.meta
|
|
88
|
+
}
|
|
89
|
+
endpoints.append(endpoint_dict)
|
|
90
|
+
|
|
91
|
+
# Note: agentWallet is no longer included in endpoints array.
|
|
92
|
+
# It's now a reserved on-chain metadata key managed via Agent.setWallet().
|
|
93
|
+
|
|
94
|
+
# Build registrations array
|
|
95
|
+
registrations = []
|
|
96
|
+
if self.agentId:
|
|
97
|
+
agent_id_int = int(self.agentId.split(":")[-1]) if ":" in self.agentId else int(self.agentId)
|
|
98
|
+
agent_registry = f"eip155:{chain_id}:{identity_registry_address}" if chain_id and identity_registry_address else f"eip155:1:{{identityRegistry}}"
|
|
99
|
+
registrations.append({
|
|
100
|
+
"agentId": agent_id_int,
|
|
101
|
+
"agentRegistry": agent_registry
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
|
|
106
|
+
"name": self.name,
|
|
107
|
+
"description": self.description,
|
|
108
|
+
"image": self.image,
|
|
109
|
+
"services": endpoints,
|
|
110
|
+
"registrations": registrations,
|
|
111
|
+
"supportedTrust": [tm.value if isinstance(tm, TrustModel) else tm for tm in self.trustModels],
|
|
112
|
+
"active": self.active,
|
|
113
|
+
"x402Support": self.x402support, # Use camelCase in JSON output per spec
|
|
114
|
+
"updatedAt": self.updatedAt,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_dict(cls, data: Dict[str, Any]) -> RegistrationFile:
|
|
119
|
+
"""Create from dictionary."""
|
|
120
|
+
endpoints = []
|
|
121
|
+
raw_services = data.get("services", data.get("endpoints", []))
|
|
122
|
+
for ep_data in raw_services:
|
|
123
|
+
name = ep_data["name"]
|
|
124
|
+
# Special handling for agentWallet - it's not a standard endpoint type
|
|
125
|
+
if name == "agentWallet":
|
|
126
|
+
# Skip agentWallet endpoints as they're handled separately via walletAddress field
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
ep_type = EndpointType(name)
|
|
130
|
+
ep_value = ep_data["endpoint"]
|
|
131
|
+
ep_meta = {k: v for k, v in ep_data.items() if k not in ["name", "endpoint"]}
|
|
132
|
+
endpoints.append(Endpoint(type=ep_type, value=ep_value, meta=ep_meta))
|
|
133
|
+
|
|
134
|
+
trust_models = []
|
|
135
|
+
for tm in data.get("supportedTrust", []):
|
|
136
|
+
try:
|
|
137
|
+
trust_models.append(TrustModel(tm))
|
|
138
|
+
except ValueError:
|
|
139
|
+
trust_models.append(tm) # custom string
|
|
140
|
+
|
|
141
|
+
return cls(
|
|
142
|
+
agentId=data.get("agentId"),
|
|
143
|
+
agentURI=data.get("agentURI"),
|
|
144
|
+
name=data.get("name", ""),
|
|
145
|
+
description=data.get("description", ""),
|
|
146
|
+
image=data.get("image"),
|
|
147
|
+
walletAddress=data.get("walletAddress"),
|
|
148
|
+
walletChainId=data.get("walletChainId"),
|
|
149
|
+
endpoints=endpoints,
|
|
150
|
+
trustModels=trust_models,
|
|
151
|
+
active=data.get("active", False),
|
|
152
|
+
x402support=data.get("x402Support", data.get("x402support", False)), # Handle both camelCase and lowercase
|
|
153
|
+
metadata=data.get("metadata", {}),
|
|
154
|
+
updatedAt=data.get("updatedAt", int(datetime.now().timestamp())),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class AgentSummary:
|
|
160
|
+
"""Summary information for agent discovery and search."""
|
|
161
|
+
chainId: ChainId
|
|
162
|
+
agentId: AgentId
|
|
163
|
+
name: str
|
|
164
|
+
image: Optional[URI]
|
|
165
|
+
description: str
|
|
166
|
+
owners: List[Address]
|
|
167
|
+
operators: List[Address]
|
|
168
|
+
mcp: bool
|
|
169
|
+
a2a: bool
|
|
170
|
+
ens: Optional[str]
|
|
171
|
+
did: Optional[str]
|
|
172
|
+
walletAddress: Optional[Address]
|
|
173
|
+
supportedTrusts: List[str] # normalized string keys
|
|
174
|
+
a2aSkills: List[str]
|
|
175
|
+
mcpTools: List[str]
|
|
176
|
+
mcpPrompts: List[str]
|
|
177
|
+
mcpResources: List[str]
|
|
178
|
+
active: bool
|
|
179
|
+
x402support: bool = False
|
|
180
|
+
extras: Dict[str, Any] = field(default_factory=dict)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class Feedback:
|
|
185
|
+
"""Feedback data structure."""
|
|
186
|
+
id: tuple # (agentId, clientAddress, feedbackIndex) - tuple for efficiency
|
|
187
|
+
agentId: AgentId
|
|
188
|
+
reviewer: Address
|
|
189
|
+
# ReputationRegistry Jan 2026: decimal value computed as (value:int256 / 10^valueDecimals).
|
|
190
|
+
# SDK exposes ONLY the computed value.
|
|
191
|
+
value: Optional[float]
|
|
192
|
+
tags: List[str] = field(default_factory=list)
|
|
193
|
+
text: Optional[str] = None
|
|
194
|
+
context: Optional[Dict[str, Any]] = None
|
|
195
|
+
proofOfPayment: Optional[Dict[str, Any]] = None
|
|
196
|
+
fileURI: Optional[URI] = None
|
|
197
|
+
endpoint: Optional[str] = None # Endpoint URI associated with feedback
|
|
198
|
+
createdAt: Timestamp = field(default_factory=lambda: int(datetime.now().timestamp()))
|
|
199
|
+
answers: List[Dict[str, Any]] = field(default_factory=list)
|
|
200
|
+
isRevoked: bool = False
|
|
201
|
+
|
|
202
|
+
# Off-chain only fields (not stored on blockchain)
|
|
203
|
+
capability: Optional[str] = None # MCP capability: "prompts", "resources", "tools", "completions"
|
|
204
|
+
name: Optional[str] = None # MCP tool/resource name
|
|
205
|
+
skill: Optional[str] = None # A2A skill
|
|
206
|
+
task: Optional[str] = None # A2A task
|
|
207
|
+
|
|
208
|
+
def __post_init__(self):
|
|
209
|
+
"""Validate and set ID after initialization."""
|
|
210
|
+
if isinstance(self.id, str):
|
|
211
|
+
# Convert string ID to tuple
|
|
212
|
+
parsed_id = self.from_id_string(self.id)
|
|
213
|
+
self.id = parsed_id
|
|
214
|
+
elif not isinstance(self.id, tuple) or len(self.id) != 3:
|
|
215
|
+
raise ValueError(f"Feedback ID must be tuple of (agentId, clientAddress, feedbackIndex), got: {self.id}")
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def id_string(self) -> str:
|
|
219
|
+
"""Get string representation of ID for external APIs."""
|
|
220
|
+
return f"{self.id[0]}:{self.id[1]}:{self.id[2]}"
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def create_id(cls, agentId: AgentId, clientAddress: Address, feedbackIndex: int) -> tuple:
|
|
224
|
+
"""Create feedback ID tuple with normalized address."""
|
|
225
|
+
# Normalize address to lowercase for consistency
|
|
226
|
+
# Ethereum addresses are case-insensitive, but we store them in lowercase
|
|
227
|
+
if isinstance(clientAddress, str) and (clientAddress.startswith("0x") or clientAddress.startswith("0X")):
|
|
228
|
+
normalized_address = "0x" + clientAddress[2:].lower()
|
|
229
|
+
else:
|
|
230
|
+
normalized_address = clientAddress.lower() if isinstance(clientAddress, str) else str(clientAddress).lower()
|
|
231
|
+
return (agentId, normalized_address, feedbackIndex)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def from_id_string(cls, id_string: str) -> tuple:
|
|
235
|
+
"""Parse feedback ID from string.
|
|
236
|
+
|
|
237
|
+
Format: agentId:clientAddress:feedbackIndex
|
|
238
|
+
Note: agentId may contain colons (e.g., "11155111:123"), so we need to split from the right.
|
|
239
|
+
"""
|
|
240
|
+
parts = id_string.rsplit(":", 2) # Split from right, max 2 splits
|
|
241
|
+
if len(parts) != 3:
|
|
242
|
+
raise ValueError(f"Invalid feedback ID format: {id_string}")
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
feedback_index = int(parts[2])
|
|
246
|
+
except ValueError:
|
|
247
|
+
raise ValueError(f"Invalid feedback index: {parts[2]}")
|
|
248
|
+
|
|
249
|
+
# Normalize address to lowercase for consistency
|
|
250
|
+
client_address = parts[1]
|
|
251
|
+
if isinstance(client_address, str) and (client_address.startswith("0x") or client_address.startswith("0X")):
|
|
252
|
+
normalized_address = "0x" + client_address[2:].lower()
|
|
253
|
+
else:
|
|
254
|
+
normalized_address = client_address.lower() if isinstance(client_address, str) else str(client_address).lower()
|
|
255
|
+
|
|
256
|
+
return (parts[0], normalized_address, feedback_index)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass
|
|
260
|
+
class SearchParams:
|
|
261
|
+
"""Parameters for agent search."""
|
|
262
|
+
chains: Optional[Union[List[ChainId], Literal["all"]]] = None
|
|
263
|
+
name: Optional[str] = None # case-insensitive substring
|
|
264
|
+
description: Optional[str] = None # semantic; vector distance < threshold
|
|
265
|
+
owners: Optional[List[Address]] = None
|
|
266
|
+
operators: Optional[List[Address]] = None
|
|
267
|
+
mcp: Optional[bool] = None
|
|
268
|
+
a2a: Optional[bool] = None
|
|
269
|
+
ens: Optional[str] = None # exact, case-insensitive
|
|
270
|
+
did: Optional[str] = None # exact
|
|
271
|
+
walletAddress: Optional[Address] = None
|
|
272
|
+
supportedTrust: Optional[List[str]] = None
|
|
273
|
+
a2aSkills: Optional[List[str]] = None
|
|
274
|
+
mcpTools: Optional[List[str]] = None
|
|
275
|
+
mcpPrompts: Optional[List[str]] = None
|
|
276
|
+
mcpResources: Optional[List[str]] = None
|
|
277
|
+
active: Optional[bool] = True
|
|
278
|
+
x402support: Optional[bool] = None
|
|
279
|
+
deduplicate_cross_chain: bool = False # Deduplicate same agent across chains
|
|
280
|
+
|
|
281
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
282
|
+
"""Convert to dictionary, filtering out None values."""
|
|
283
|
+
return {k: v for k, v in self.__dict__.items() if v is not None}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class SearchFeedbackParams:
|
|
288
|
+
"""Parameters for feedback search."""
|
|
289
|
+
agents: Optional[List[AgentId]] = None
|
|
290
|
+
tags: Optional[List[str]] = None
|
|
291
|
+
reviewers: Optional[List[Address]] = None
|
|
292
|
+
capabilities: Optional[List[str]] = None
|
|
293
|
+
skills: Optional[List[str]] = None
|
|
294
|
+
tasks: Optional[List[str]] = None
|
|
295
|
+
names: Optional[List[str]] = None # MCP tool/resource/prompt names
|
|
296
|
+
endpoint: Optional[str] = None # Filter by endpoint URI
|
|
297
|
+
minValue: Optional[float] = None
|
|
298
|
+
maxValue: Optional[float] = None
|
|
299
|
+
includeRevoked: bool = False
|
|
300
|
+
|
|
301
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
302
|
+
"""Convert to dictionary, filtering out None values."""
|
|
303
|
+
return {k: v for k, v in self.__dict__.items() if v is not None}
|