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
agent0_sdk/core/sdk.py
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main SDK class for Agent0.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from .models import (
|
|
14
|
+
AgentId, ChainId, Address, URI, Timestamp, IdemKey,
|
|
15
|
+
EndpointType, TrustModel, Endpoint, RegistrationFile,
|
|
16
|
+
AgentSummary, Feedback, SearchParams
|
|
17
|
+
)
|
|
18
|
+
from .web3_client import Web3Client
|
|
19
|
+
from .contracts import (
|
|
20
|
+
IDENTITY_REGISTRY_ABI, REPUTATION_REGISTRY_ABI, VALIDATION_REGISTRY_ABI,
|
|
21
|
+
DEFAULT_REGISTRIES, DEFAULT_SUBGRAPH_URLS
|
|
22
|
+
)
|
|
23
|
+
from .agent import Agent
|
|
24
|
+
from .indexer import AgentIndexer
|
|
25
|
+
from .ipfs_client import IPFSClient
|
|
26
|
+
from .feedback_manager import FeedbackManager
|
|
27
|
+
from .subgraph_client import SubgraphClient
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SDK:
|
|
31
|
+
"""Main SDK class for Agent0."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
chainId: ChainId,
|
|
36
|
+
rpcUrl: str,
|
|
37
|
+
signer: Optional[Any] = None, # Optional for read-only operations
|
|
38
|
+
registryOverrides: Optional[Dict[ChainId, Dict[str, Address]]] = None,
|
|
39
|
+
indexingStore: Optional[Any] = None, # optional (e.g., sqlite/postgres/duckdb)
|
|
40
|
+
embeddings: Optional[Any] = None, # optional vector backend
|
|
41
|
+
# IPFS configuration
|
|
42
|
+
ipfs: Optional[str] = None, # "node", "filecoinPin", or "pinata"
|
|
43
|
+
# Direct IPFS node config
|
|
44
|
+
ipfsNodeUrl: Optional[str] = None,
|
|
45
|
+
# Filecoin Pin config
|
|
46
|
+
filecoinPrivateKey: Optional[str] = None,
|
|
47
|
+
# Pinata config
|
|
48
|
+
pinataJwt: Optional[str] = None,
|
|
49
|
+
# Subgraph configuration
|
|
50
|
+
subgraphOverrides: Optional[Dict[ChainId, str]] = None, # Override subgraph URLs per chain
|
|
51
|
+
):
|
|
52
|
+
"""Initialize the SDK."""
|
|
53
|
+
self.chainId = chainId
|
|
54
|
+
self.rpcUrl = rpcUrl
|
|
55
|
+
self.signer = signer
|
|
56
|
+
|
|
57
|
+
# Initialize Web3 client (with or without signer for read-only operations)
|
|
58
|
+
if signer:
|
|
59
|
+
if isinstance(signer, str):
|
|
60
|
+
self.web3_client = Web3Client(rpcUrl, private_key=signer)
|
|
61
|
+
else:
|
|
62
|
+
self.web3_client = Web3Client(rpcUrl, account=signer)
|
|
63
|
+
else:
|
|
64
|
+
# Read-only mode - no signer
|
|
65
|
+
self.web3_client = Web3Client(rpcUrl)
|
|
66
|
+
|
|
67
|
+
# Registry addresses
|
|
68
|
+
self.registry_overrides = registryOverrides or {}
|
|
69
|
+
self._registries = self._resolve_registries()
|
|
70
|
+
|
|
71
|
+
# Initialize contract instances
|
|
72
|
+
self._identity_registry = None
|
|
73
|
+
self._reputation_registry = None
|
|
74
|
+
self._validation_registry = None
|
|
75
|
+
|
|
76
|
+
# Resolve subgraph URL (with fallback chain)
|
|
77
|
+
self._subgraph_urls = {}
|
|
78
|
+
if subgraphOverrides:
|
|
79
|
+
self._subgraph_urls.update(subgraphOverrides)
|
|
80
|
+
|
|
81
|
+
# Get subgraph URL for current chain
|
|
82
|
+
resolved_subgraph_url = None
|
|
83
|
+
|
|
84
|
+
# Priority 1: Chain-specific override
|
|
85
|
+
if chainId in self._subgraph_urls:
|
|
86
|
+
resolved_subgraph_url = self._subgraph_urls[chainId]
|
|
87
|
+
# Priority 2: Default for chain
|
|
88
|
+
elif chainId in DEFAULT_SUBGRAPH_URLS:
|
|
89
|
+
resolved_subgraph_url = DEFAULT_SUBGRAPH_URLS[chainId]
|
|
90
|
+
else:
|
|
91
|
+
# No subgraph available - subgraph_client will be None
|
|
92
|
+
resolved_subgraph_url = None
|
|
93
|
+
|
|
94
|
+
# Initialize subgraph client if URL available
|
|
95
|
+
if resolved_subgraph_url:
|
|
96
|
+
self.subgraph_client = SubgraphClient(resolved_subgraph_url)
|
|
97
|
+
else:
|
|
98
|
+
self.subgraph_client = None
|
|
99
|
+
|
|
100
|
+
# Initialize services
|
|
101
|
+
self.indexer = AgentIndexer(
|
|
102
|
+
web3_client=self.web3_client,
|
|
103
|
+
store=indexingStore,
|
|
104
|
+
embeddings=embeddings,
|
|
105
|
+
subgraph_client=self.subgraph_client
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Initialize IPFS client based on configuration
|
|
109
|
+
self.ipfs_client = self._initialize_ipfs_client(
|
|
110
|
+
ipfs, ipfsNodeUrl, filecoinPrivateKey, pinataJwt
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Load registries before passing to FeedbackManager
|
|
114
|
+
identity_registry = self.identity_registry
|
|
115
|
+
reputation_registry = self.reputation_registry
|
|
116
|
+
|
|
117
|
+
self.feedback_manager = FeedbackManager(
|
|
118
|
+
subgraph_client=self.subgraph_client,
|
|
119
|
+
web3_client=self.web3_client,
|
|
120
|
+
ipfs_client=self.ipfs_client,
|
|
121
|
+
reputation_registry=reputation_registry,
|
|
122
|
+
identity_registry=identity_registry,
|
|
123
|
+
indexer=self.indexer # Pass indexer for unified search interface
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _resolve_registries(self) -> Dict[str, Address]:
|
|
127
|
+
"""Resolve registry addresses for current chain."""
|
|
128
|
+
# Start with defaults
|
|
129
|
+
registries = DEFAULT_REGISTRIES.get(self.chainId, {}).copy()
|
|
130
|
+
|
|
131
|
+
# Apply overrides
|
|
132
|
+
if self.chainId in self.registry_overrides:
|
|
133
|
+
registries.update(self.registry_overrides[self.chainId])
|
|
134
|
+
|
|
135
|
+
return registries
|
|
136
|
+
|
|
137
|
+
def _initialize_ipfs_client(
|
|
138
|
+
self,
|
|
139
|
+
ipfs: Optional[str],
|
|
140
|
+
ipfsNodeUrl: Optional[str],
|
|
141
|
+
filecoinPrivateKey: Optional[str],
|
|
142
|
+
pinataJwt: Optional[str]
|
|
143
|
+
) -> Optional[IPFSClient]:
|
|
144
|
+
"""Initialize IPFS client based on configuration."""
|
|
145
|
+
if not ipfs:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
if ipfs == "node":
|
|
149
|
+
if not ipfsNodeUrl:
|
|
150
|
+
raise ValueError("ipfsNodeUrl is required when ipfs='node'")
|
|
151
|
+
return IPFSClient(url=ipfsNodeUrl, filecoin_pin_enabled=False)
|
|
152
|
+
|
|
153
|
+
elif ipfs == "filecoinPin":
|
|
154
|
+
if not filecoinPrivateKey:
|
|
155
|
+
raise ValueError("filecoinPrivateKey is required when ipfs='filecoinPin'")
|
|
156
|
+
return IPFSClient(
|
|
157
|
+
url=None,
|
|
158
|
+
filecoin_pin_enabled=True,
|
|
159
|
+
filecoin_private_key=filecoinPrivateKey
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
elif ipfs == "pinata":
|
|
163
|
+
if not pinataJwt:
|
|
164
|
+
raise ValueError("pinataJwt is required when ipfs='pinata'")
|
|
165
|
+
return IPFSClient(
|
|
166
|
+
url=None,
|
|
167
|
+
filecoin_pin_enabled=False,
|
|
168
|
+
pinata_enabled=True,
|
|
169
|
+
pinata_jwt=pinataJwt
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError(f"Invalid ipfs value: {ipfs}. Must be 'node', 'filecoinPin', or 'pinata'")
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def isReadOnly(self) -> bool:
|
|
177
|
+
"""Check if SDK is in read-only mode (no signer)."""
|
|
178
|
+
return self.signer is None
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def identity_registry(self):
|
|
182
|
+
"""Get identity registry contract."""
|
|
183
|
+
if self._identity_registry is None:
|
|
184
|
+
address = self._registries.get("IDENTITY")
|
|
185
|
+
if not address:
|
|
186
|
+
raise ValueError(f"No identity registry address for chain {self.chainId}")
|
|
187
|
+
self._identity_registry = self.web3_client.get_contract(
|
|
188
|
+
address, IDENTITY_REGISTRY_ABI
|
|
189
|
+
)
|
|
190
|
+
return self._identity_registry
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def reputation_registry(self):
|
|
194
|
+
"""Get reputation registry contract."""
|
|
195
|
+
if self._reputation_registry is None:
|
|
196
|
+
address = self._registries.get("REPUTATION")
|
|
197
|
+
if not address:
|
|
198
|
+
raise ValueError(f"No reputation registry address for chain {self.chainId}")
|
|
199
|
+
self._reputation_registry = self.web3_client.get_contract(
|
|
200
|
+
address, REPUTATION_REGISTRY_ABI
|
|
201
|
+
)
|
|
202
|
+
return self._reputation_registry
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def validation_registry(self):
|
|
206
|
+
"""Get validation registry contract."""
|
|
207
|
+
if self._validation_registry is None:
|
|
208
|
+
address = self._registries.get("VALIDATION")
|
|
209
|
+
if not address:
|
|
210
|
+
raise ValueError(f"No validation registry address for chain {self.chainId}")
|
|
211
|
+
self._validation_registry = self.web3_client.get_contract(
|
|
212
|
+
address, VALIDATION_REGISTRY_ABI
|
|
213
|
+
)
|
|
214
|
+
return self._validation_registry
|
|
215
|
+
|
|
216
|
+
def chain_id(self) -> ChainId:
|
|
217
|
+
"""Get current chain ID."""
|
|
218
|
+
return self.chainId
|
|
219
|
+
|
|
220
|
+
def registries(self) -> Dict[str, Address]:
|
|
221
|
+
"""Get resolved addresses for current chain."""
|
|
222
|
+
return self._registries.copy()
|
|
223
|
+
|
|
224
|
+
def get_subgraph_client(self, chain_id: Optional[ChainId] = None) -> Optional[SubgraphClient]:
|
|
225
|
+
"""
|
|
226
|
+
Get subgraph client for a specific chain.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
chain_id: Chain ID (defaults to current chain)
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
SubgraphClient instance or None if no subgraph available
|
|
233
|
+
"""
|
|
234
|
+
target_chain = chain_id if chain_id is not None else self.chainId
|
|
235
|
+
|
|
236
|
+
# Check if we already have a client for this chain
|
|
237
|
+
if target_chain == self.chainId and self.subgraph_client:
|
|
238
|
+
return self.subgraph_client
|
|
239
|
+
|
|
240
|
+
# Resolve URL for target chain
|
|
241
|
+
url = None
|
|
242
|
+
if target_chain in self._subgraph_urls:
|
|
243
|
+
url = self._subgraph_urls[target_chain]
|
|
244
|
+
elif target_chain in DEFAULT_SUBGRAPH_URLS:
|
|
245
|
+
url = DEFAULT_SUBGRAPH_URLS[target_chain]
|
|
246
|
+
|
|
247
|
+
if url:
|
|
248
|
+
return SubgraphClient(url)
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def set_chain(self, chain_id: ChainId) -> None:
|
|
252
|
+
"""Switch chains (advanced)."""
|
|
253
|
+
self.chainId = chain_id
|
|
254
|
+
self._registries = self._resolve_registries()
|
|
255
|
+
# Reset contract instances
|
|
256
|
+
self._identity_registry = None
|
|
257
|
+
self._reputation_registry = None
|
|
258
|
+
self._validation_registry = None
|
|
259
|
+
|
|
260
|
+
# Agent lifecycle methods
|
|
261
|
+
def createAgent(
|
|
262
|
+
self,
|
|
263
|
+
name: str,
|
|
264
|
+
description: str,
|
|
265
|
+
image: Optional[URI] = None,
|
|
266
|
+
) -> Agent:
|
|
267
|
+
"""Create a new agent (off-chain object in memory)."""
|
|
268
|
+
registration_file = RegistrationFile(
|
|
269
|
+
name=name,
|
|
270
|
+
description=description,
|
|
271
|
+
image=image,
|
|
272
|
+
updatedAt=int(time.time())
|
|
273
|
+
)
|
|
274
|
+
return Agent(sdk=self, registration_file=registration_file)
|
|
275
|
+
|
|
276
|
+
def loadAgent(self, agentId: AgentId) -> Agent:
|
|
277
|
+
"""Load an existing agent (hydrates from registration file if registered)."""
|
|
278
|
+
# Convert agentId to string if it's an integer
|
|
279
|
+
agentId = str(agentId)
|
|
280
|
+
|
|
281
|
+
# Parse agent ID
|
|
282
|
+
if ":" in agentId:
|
|
283
|
+
chain_id, token_id = agentId.split(":", 1)
|
|
284
|
+
if int(chain_id) != self.chainId:
|
|
285
|
+
raise ValueError(f"Agent {agentId} is not on current chain {self.chainId}")
|
|
286
|
+
else:
|
|
287
|
+
token_id = agentId
|
|
288
|
+
|
|
289
|
+
# Get token URI from contract
|
|
290
|
+
try:
|
|
291
|
+
token_uri = self.web3_client.call_contract(
|
|
292
|
+
self.identity_registry, "tokenURI", int(token_id)
|
|
293
|
+
)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
raise ValueError(f"Failed to load agent {agentId}: {e}")
|
|
296
|
+
|
|
297
|
+
# Load registration file
|
|
298
|
+
registration_file = self._load_registration_file(token_uri)
|
|
299
|
+
registration_file.agentId = agentId
|
|
300
|
+
registration_file.agentURI = token_uri if token_uri else None
|
|
301
|
+
|
|
302
|
+
# Store registry address for proper JSON generation
|
|
303
|
+
registry_address = self._registries.get("IDENTITY")
|
|
304
|
+
if registry_address:
|
|
305
|
+
registration_file._registry_address = registry_address
|
|
306
|
+
registration_file._chain_id = self.chainId
|
|
307
|
+
|
|
308
|
+
# Hydrate on-chain data
|
|
309
|
+
self._hydrate_agent_data(registration_file, int(token_id))
|
|
310
|
+
|
|
311
|
+
return Agent(sdk=self, registration_file=registration_file)
|
|
312
|
+
|
|
313
|
+
def _load_registration_file(self, uri: str) -> RegistrationFile:
|
|
314
|
+
"""Load registration file from URI."""
|
|
315
|
+
if uri.startswith("ipfs://"):
|
|
316
|
+
if not self.ipfs_client:
|
|
317
|
+
raise ValueError("IPFS client not configured")
|
|
318
|
+
content = self.ipfs_client.get(uri)
|
|
319
|
+
elif uri.startswith("http"):
|
|
320
|
+
try:
|
|
321
|
+
import requests
|
|
322
|
+
response = requests.get(uri)
|
|
323
|
+
response.raise_for_status()
|
|
324
|
+
content = response.text
|
|
325
|
+
except ImportError:
|
|
326
|
+
raise ImportError("requests not installed. Install with: pip install requests")
|
|
327
|
+
else:
|
|
328
|
+
raise ValueError(f"Unsupported URI scheme: {uri}")
|
|
329
|
+
|
|
330
|
+
data = json.loads(content)
|
|
331
|
+
return RegistrationFile.from_dict(data)
|
|
332
|
+
|
|
333
|
+
def _hydrate_agent_data(self, registration_file: RegistrationFile, token_id: int):
|
|
334
|
+
"""Hydrate agent data from on-chain sources."""
|
|
335
|
+
# Get owner
|
|
336
|
+
owner = self.web3_client.call_contract(
|
|
337
|
+
self.identity_registry, "ownerOf", token_id
|
|
338
|
+
)
|
|
339
|
+
registration_file.owners = [owner]
|
|
340
|
+
|
|
341
|
+
# Get operators (this would require additional contract calls)
|
|
342
|
+
# For now, we'll leave it empty
|
|
343
|
+
registration_file.operators = []
|
|
344
|
+
|
|
345
|
+
# Hydrate metadata from on-chain (agentWallet, agentName, custom metadata)
|
|
346
|
+
agent_id = token_id
|
|
347
|
+
try:
|
|
348
|
+
# Try to get agentWallet from on-chain metadata
|
|
349
|
+
wallet_bytes = self.web3_client.call_contract(
|
|
350
|
+
self.identity_registry, "getMetadata", agent_id, "agentWallet"
|
|
351
|
+
)
|
|
352
|
+
if wallet_bytes and len(wallet_bytes) > 0:
|
|
353
|
+
wallet_address = "0x" + wallet_bytes.hex()
|
|
354
|
+
registration_file.walletAddress = wallet_address
|
|
355
|
+
# If wallet is read from on-chain, use current chain ID
|
|
356
|
+
# (the chain ID from the registration file might be outdated)
|
|
357
|
+
registration_file.walletChainId = self.chainId
|
|
358
|
+
except Exception as e:
|
|
359
|
+
# No on-chain wallet, will fall back to registration file
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
# Try to get agentName (ENS) from on-chain metadata
|
|
364
|
+
name_bytes = self.web3_client.call_contract(
|
|
365
|
+
self.identity_registry, "getMetadata", agent_id, "agentName"
|
|
366
|
+
)
|
|
367
|
+
if name_bytes and len(name_bytes) > 0:
|
|
368
|
+
ens_name = name_bytes.decode('utf-8')
|
|
369
|
+
# Add ENS endpoint to registration file
|
|
370
|
+
from .models import EndpointType, Endpoint
|
|
371
|
+
# Remove existing ENS endpoints
|
|
372
|
+
registration_file.endpoints = [
|
|
373
|
+
ep for ep in registration_file.endpoints
|
|
374
|
+
if ep.type != EndpointType.ENS
|
|
375
|
+
]
|
|
376
|
+
# Add new ENS endpoint
|
|
377
|
+
ens_endpoint = Endpoint(
|
|
378
|
+
type=EndpointType.ENS,
|
|
379
|
+
value=ens_name,
|
|
380
|
+
meta={"version": "1.0"}
|
|
381
|
+
)
|
|
382
|
+
registration_file.endpoints.append(ens_endpoint)
|
|
383
|
+
except Exception as e:
|
|
384
|
+
# No on-chain ENS name, will fall back to registration file
|
|
385
|
+
pass
|
|
386
|
+
|
|
387
|
+
# Try to get custom metadata keys from registration file and check on-chain
|
|
388
|
+
# Note: We can't enumerate on-chain metadata keys, so we check each key from the registration file
|
|
389
|
+
# Also check for common custom metadata keys that might exist on-chain
|
|
390
|
+
keys_to_check = list(registration_file.metadata.keys())
|
|
391
|
+
# Also check for known metadata keys that might have been set on-chain
|
|
392
|
+
known_keys = ["testKey", "version", "timestamp", "customField", "anotherField", "numericField"]
|
|
393
|
+
for key in known_keys:
|
|
394
|
+
if key not in keys_to_check:
|
|
395
|
+
keys_to_check.append(key)
|
|
396
|
+
|
|
397
|
+
for key in keys_to_check:
|
|
398
|
+
try:
|
|
399
|
+
value_bytes = self.web3_client.call_contract(
|
|
400
|
+
self.identity_registry, "getMetadata", agent_id, key
|
|
401
|
+
)
|
|
402
|
+
if value_bytes and len(value_bytes) > 0:
|
|
403
|
+
value_str = value_bytes.decode('utf-8')
|
|
404
|
+
# Try to convert back to original type if possible
|
|
405
|
+
try:
|
|
406
|
+
# Try integer
|
|
407
|
+
value_int = int(value_str)
|
|
408
|
+
# Check if it's actually stored as integer in metadata or if it was originally a string
|
|
409
|
+
registration_file.metadata[key] = value_str # Keep as string for now
|
|
410
|
+
except ValueError:
|
|
411
|
+
# Try float
|
|
412
|
+
try:
|
|
413
|
+
value_float = float(value_str)
|
|
414
|
+
registration_file.metadata[key] = value_str # Keep as string for now
|
|
415
|
+
except ValueError:
|
|
416
|
+
registration_file.metadata[key] = value_str
|
|
417
|
+
except Exception as e:
|
|
418
|
+
# Keep registration file value if on-chain not found
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
# Discovery and indexing
|
|
422
|
+
def refreshAgentIndex(self, agentId: AgentId, deep: bool = False) -> AgentSummary:
|
|
423
|
+
"""Refresh index for a single agent."""
|
|
424
|
+
return asyncio.run(self.indexer.refresh_agent(agentId, deep=deep))
|
|
425
|
+
|
|
426
|
+
def refreshIndex(
|
|
427
|
+
self,
|
|
428
|
+
agentIds: Optional[List[AgentId]] = None,
|
|
429
|
+
concurrency: int = 8,
|
|
430
|
+
) -> List[AgentSummary]:
|
|
431
|
+
"""Refresh index for multiple agents."""
|
|
432
|
+
return asyncio.run(self.indexer.refresh_agents(agentIds, concurrency))
|
|
433
|
+
|
|
434
|
+
def getAgent(self, agentId: AgentId) -> AgentSummary:
|
|
435
|
+
"""Get agent summary from index."""
|
|
436
|
+
return self.indexer.get_agent(agentId)
|
|
437
|
+
|
|
438
|
+
def searchAgents(
|
|
439
|
+
self,
|
|
440
|
+
params: Union[SearchParams, Dict[str, Any], None] = None,
|
|
441
|
+
sort: List[str] = None,
|
|
442
|
+
page_size: int = 50,
|
|
443
|
+
cursor: Optional[str] = None,
|
|
444
|
+
**kwargs # Accept search criteria as kwargs for better DX
|
|
445
|
+
) -> Dict[str, Any]:
|
|
446
|
+
"""Search for agents.
|
|
447
|
+
|
|
448
|
+
Examples:
|
|
449
|
+
# Simple kwargs for better developer experience
|
|
450
|
+
sdk.searchAgents(name="Test")
|
|
451
|
+
sdk.searchAgents(mcpTools=["code_generation"], active=True)
|
|
452
|
+
|
|
453
|
+
# Explicit SearchParams (for complex queries or IDE autocomplete)
|
|
454
|
+
sdk.searchAgents(SearchParams(name="Test", mcpTools=["code_generation"]))
|
|
455
|
+
|
|
456
|
+
# With pagination
|
|
457
|
+
sdk.searchAgents(name="Test", page_size=10)
|
|
458
|
+
"""
|
|
459
|
+
# If kwargs provided, use them instead of params
|
|
460
|
+
if kwargs and params is None:
|
|
461
|
+
params = SearchParams(**kwargs)
|
|
462
|
+
elif params is None:
|
|
463
|
+
params = SearchParams()
|
|
464
|
+
elif isinstance(params, dict):
|
|
465
|
+
params = SearchParams(**params)
|
|
466
|
+
|
|
467
|
+
if sort is None:
|
|
468
|
+
sort = ["updatedAt:desc"]
|
|
469
|
+
|
|
470
|
+
return self.indexer.search_agents(params, sort, page_size, cursor)
|
|
471
|
+
|
|
472
|
+
# Feedback methods
|
|
473
|
+
def prepareFeedback(
|
|
474
|
+
self,
|
|
475
|
+
agentId: AgentId,
|
|
476
|
+
score: Optional[int] = None, # 0-100
|
|
477
|
+
tags: List[str] = None,
|
|
478
|
+
text: Optional[str] = None,
|
|
479
|
+
capability: Optional[str] = None,
|
|
480
|
+
name: Optional[str] = None,
|
|
481
|
+
skill: Optional[str] = None,
|
|
482
|
+
task: Optional[str] = None,
|
|
483
|
+
context: Optional[Dict[str, Any]] = None,
|
|
484
|
+
proof_of_payment: Optional[Dict[str, Any]] = None,
|
|
485
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
486
|
+
) -> Dict[str, Any]:
|
|
487
|
+
"""Prepare feedback file (local file/object)."""
|
|
488
|
+
return self.feedback_manager.prepareFeedback(
|
|
489
|
+
agentId=agentId,
|
|
490
|
+
score=score,
|
|
491
|
+
tags=tags,
|
|
492
|
+
text=text,
|
|
493
|
+
capability=capability,
|
|
494
|
+
name=name,
|
|
495
|
+
skill=skill,
|
|
496
|
+
task=task,
|
|
497
|
+
context=context,
|
|
498
|
+
proof_of_payment=proof_of_payment,
|
|
499
|
+
extra=extra
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def giveFeedback(
|
|
503
|
+
self,
|
|
504
|
+
agentId: AgentId,
|
|
505
|
+
feedbackFile: Dict[str, Any],
|
|
506
|
+
idem: Optional[IdemKey] = None,
|
|
507
|
+
feedback_auth: Optional[bytes] = None,
|
|
508
|
+
) -> Feedback:
|
|
509
|
+
"""Give feedback (maps 8004 endpoint)."""
|
|
510
|
+
return self.feedback_manager.giveFeedback(
|
|
511
|
+
agentId=agentId,
|
|
512
|
+
feedbackFile=feedbackFile,
|
|
513
|
+
idem=idem,
|
|
514
|
+
feedback_auth=feedback_auth
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def getFeedback(self, feedbackId: str) -> Feedback:
|
|
518
|
+
"""Get single feedback by ID string."""
|
|
519
|
+
# Parse feedback ID
|
|
520
|
+
agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
|
|
521
|
+
return self.feedback_manager.getFeedback(agentId, clientAddress, feedbackIndex)
|
|
522
|
+
|
|
523
|
+
def searchFeedback(
|
|
524
|
+
self,
|
|
525
|
+
agentId: AgentId,
|
|
526
|
+
reviewers: Optional[List[Address]] = None,
|
|
527
|
+
tags: Optional[List[str]] = None,
|
|
528
|
+
capabilities: Optional[List[str]] = None,
|
|
529
|
+
skills: Optional[List[str]] = None,
|
|
530
|
+
tasks: Optional[List[str]] = None,
|
|
531
|
+
names: Optional[List[str]] = None,
|
|
532
|
+
minScore: Optional[int] = None,
|
|
533
|
+
maxScore: Optional[int] = None,
|
|
534
|
+
include_revoked: bool = False,
|
|
535
|
+
first: int = 100,
|
|
536
|
+
skip: int = 0,
|
|
537
|
+
) -> List[Feedback]:
|
|
538
|
+
"""Search feedback for an agent."""
|
|
539
|
+
return self.feedback_manager.searchFeedback(
|
|
540
|
+
agentId=agentId,
|
|
541
|
+
clientAddresses=reviewers,
|
|
542
|
+
tags=tags,
|
|
543
|
+
capabilities=capabilities,
|
|
544
|
+
skills=skills,
|
|
545
|
+
tasks=tasks,
|
|
546
|
+
names=names,
|
|
547
|
+
minScore=minScore,
|
|
548
|
+
maxScore=maxScore,
|
|
549
|
+
include_revoked=include_revoked,
|
|
550
|
+
first=first,
|
|
551
|
+
skip=skip
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def revokeFeedback(
|
|
555
|
+
self,
|
|
556
|
+
feedbackId: str,
|
|
557
|
+
reason: Optional[str] = None,
|
|
558
|
+
idem: Optional[IdemKey] = None,
|
|
559
|
+
) -> Dict[str, Any]:
|
|
560
|
+
"""Revoke feedback."""
|
|
561
|
+
# Parse feedback ID
|
|
562
|
+
agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
|
|
563
|
+
return self.feedback_manager.revokeFeedback(agentId, feedbackIndex)
|
|
564
|
+
|
|
565
|
+
def appendResponse(
|
|
566
|
+
self,
|
|
567
|
+
feedbackId: str,
|
|
568
|
+
response: Dict[str, Any],
|
|
569
|
+
idem: Optional[IdemKey] = None,
|
|
570
|
+
) -> Feedback:
|
|
571
|
+
"""Append a response/follow-up to existing feedback."""
|
|
572
|
+
# Parse feedback ID
|
|
573
|
+
agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
|
|
574
|
+
return self.feedback_manager.appendResponse(agentId, clientAddress, feedbackIndex, response)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def searchAgentsByReputation(
|
|
578
|
+
self,
|
|
579
|
+
agents: Optional[List[AgentId]] = None,
|
|
580
|
+
tags: Optional[List[str]] = None,
|
|
581
|
+
reviewers: Optional[List[Address]] = None,
|
|
582
|
+
capabilities: Optional[List[str]] = None,
|
|
583
|
+
skills: Optional[List[str]] = None,
|
|
584
|
+
tasks: Optional[List[str]] = None,
|
|
585
|
+
names: Optional[List[str]] = None,
|
|
586
|
+
minAverageScore: Optional[int] = None, # 0-100
|
|
587
|
+
includeRevoked: bool = False,
|
|
588
|
+
page_size: int = 50,
|
|
589
|
+
cursor: Optional[str] = None,
|
|
590
|
+
sort: Optional[List[str]] = None,
|
|
591
|
+
) -> Dict[str, Any]:
|
|
592
|
+
"""Search agents filtered by reputation criteria."""
|
|
593
|
+
if not self.subgraph_client:
|
|
594
|
+
raise ValueError("Subgraph client required for searchAgentsByReputation")
|
|
595
|
+
|
|
596
|
+
if sort is None:
|
|
597
|
+
sort = ["createdAt:desc"]
|
|
598
|
+
|
|
599
|
+
skip = 0
|
|
600
|
+
if cursor:
|
|
601
|
+
try:
|
|
602
|
+
skip = int(cursor)
|
|
603
|
+
except ValueError:
|
|
604
|
+
skip = 0
|
|
605
|
+
|
|
606
|
+
order_by = "createdAt"
|
|
607
|
+
order_direction = "desc"
|
|
608
|
+
if sort and len(sort) > 0:
|
|
609
|
+
sort_field = sort[0].split(":")
|
|
610
|
+
order_by = sort_field[0] if len(sort_field) >= 1 else order_by
|
|
611
|
+
order_direction = sort_field[1] if len(sort_field) >= 2 else order_direction
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
agents_data = self.subgraph_client.search_agents_by_reputation(
|
|
615
|
+
agents=agents,
|
|
616
|
+
tags=tags,
|
|
617
|
+
reviewers=reviewers,
|
|
618
|
+
capabilities=capabilities,
|
|
619
|
+
skills=skills,
|
|
620
|
+
tasks=tasks,
|
|
621
|
+
names=names,
|
|
622
|
+
minAverageScore=minAverageScore,
|
|
623
|
+
includeRevoked=includeRevoked,
|
|
624
|
+
first=page_size,
|
|
625
|
+
skip=skip,
|
|
626
|
+
order_by=order_by,
|
|
627
|
+
order_direction=order_direction
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
from .models import AgentSummary
|
|
631
|
+
results = []
|
|
632
|
+
for agent_data in agents_data:
|
|
633
|
+
reg_file = agent_data.get('registrationFile') or {}
|
|
634
|
+
if not isinstance(reg_file, dict):
|
|
635
|
+
reg_file = {}
|
|
636
|
+
|
|
637
|
+
agent_summary = AgentSummary(
|
|
638
|
+
chainId=int(agent_data.get('chainId', 0)),
|
|
639
|
+
agentId=agent_data.get('id'),
|
|
640
|
+
name=reg_file.get('name', f"Agent {agent_data.get('id')}"),
|
|
641
|
+
image=reg_file.get('image'),
|
|
642
|
+
description=reg_file.get('description', ''),
|
|
643
|
+
owners=[agent_data.get('owner', '')],
|
|
644
|
+
operators=agent_data.get('operators', []),
|
|
645
|
+
mcp=reg_file.get('mcpEndpoint') is not None,
|
|
646
|
+
a2a=reg_file.get('a2aEndpoint') is not None,
|
|
647
|
+
ens=reg_file.get('ens'),
|
|
648
|
+
did=reg_file.get('did'),
|
|
649
|
+
walletAddress=reg_file.get('agentWallet'),
|
|
650
|
+
supportedTrusts=reg_file.get('supportedTrusts', []),
|
|
651
|
+
a2aSkills=reg_file.get('a2aSkills', []),
|
|
652
|
+
mcpTools=reg_file.get('mcpTools', []),
|
|
653
|
+
mcpPrompts=reg_file.get('mcpPrompts', []),
|
|
654
|
+
mcpResources=reg_file.get('mcpResources', []),
|
|
655
|
+
active=reg_file.get('active', True),
|
|
656
|
+
x402support=reg_file.get('x402support', False),
|
|
657
|
+
extras={'averageScore': agent_data.get('averageScore')}
|
|
658
|
+
)
|
|
659
|
+
results.append(agent_summary)
|
|
660
|
+
|
|
661
|
+
next_cursor = str(skip + len(results)) if len(results) == page_size else None
|
|
662
|
+
return {"items": results, "nextCursor": next_cursor}
|
|
663
|
+
|
|
664
|
+
except Exception as e:
|
|
665
|
+
raise ValueError(f"Failed to search agents by reputation: {e}")
|
|
666
|
+
|
|
667
|
+
# Feedback methods - delegate to feedback_manager
|
|
668
|
+
def signFeedbackAuth(
|
|
669
|
+
self,
|
|
670
|
+
agentId: "AgentId",
|
|
671
|
+
clientAddress: "Address",
|
|
672
|
+
indexLimit: Optional[int] = None,
|
|
673
|
+
expiryHours: int = 24,
|
|
674
|
+
) -> bytes:
|
|
675
|
+
"""Sign feedback authorization for a client."""
|
|
676
|
+
return self.feedback_manager.signFeedbackAuth(
|
|
677
|
+
agentId, clientAddress, indexLimit, expiryHours
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
def prepareFeedback(
|
|
681
|
+
self,
|
|
682
|
+
agentId: "AgentId",
|
|
683
|
+
score: Optional[int] = None, # 0-100
|
|
684
|
+
tags: List[str] = None,
|
|
685
|
+
text: Optional[str] = None,
|
|
686
|
+
capability: Optional[str] = None,
|
|
687
|
+
name: Optional[str] = None,
|
|
688
|
+
skill: Optional[str] = None,
|
|
689
|
+
task: Optional[str] = None,
|
|
690
|
+
context: Optional[Dict[str, Any]] = None,
|
|
691
|
+
proof_of_payment: Optional[Dict[str, Any]] = None,
|
|
692
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
693
|
+
) -> Dict[str, Any]:
|
|
694
|
+
"""Prepare feedback file (local file/object) according to spec."""
|
|
695
|
+
return self.feedback_manager.prepareFeedback(
|
|
696
|
+
agentId, score, tags, text, capability, name, skill, task, context, proof_of_payment, extra
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
def giveFeedback(
|
|
700
|
+
self,
|
|
701
|
+
agentId: "AgentId",
|
|
702
|
+
feedbackFile: Dict[str, Any],
|
|
703
|
+
idem: Optional["IdemKey"] = None,
|
|
704
|
+
feedbackAuth: Optional[bytes] = None,
|
|
705
|
+
) -> "Feedback":
|
|
706
|
+
"""Give feedback (maps 8004 endpoint)."""
|
|
707
|
+
return self.feedback_manager.giveFeedback(
|
|
708
|
+
agentId, feedbackFile, idem, feedbackAuth
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
def getFeedback(
|
|
712
|
+
self,
|
|
713
|
+
agentId: "AgentId",
|
|
714
|
+
clientAddress: "Address",
|
|
715
|
+
feedbackIndex: int,
|
|
716
|
+
) -> "Feedback":
|
|
717
|
+
"""Get feedback (maps 8004 endpoint)."""
|
|
718
|
+
return self.feedback_manager.getFeedback(
|
|
719
|
+
agentId, clientAddress, feedbackIndex
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
def revokeFeedback(
|
|
723
|
+
self,
|
|
724
|
+
agentId: "AgentId",
|
|
725
|
+
clientAddress: "Address",
|
|
726
|
+
feedbackIndex: int,
|
|
727
|
+
) -> "Feedback":
|
|
728
|
+
"""Revoke feedback."""
|
|
729
|
+
return self.feedback_manager.revokeFeedback(
|
|
730
|
+
agentId, clientAddress, feedbackIndex
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def appendResponse(
|
|
734
|
+
self,
|
|
735
|
+
agentId: "AgentId",
|
|
736
|
+
clientAddress: "Address",
|
|
737
|
+
feedbackIndex: int,
|
|
738
|
+
response: Dict[str, Any],
|
|
739
|
+
) -> "Feedback":
|
|
740
|
+
"""Append a response/follow-up to existing feedback."""
|
|
741
|
+
return self.feedback_manager.appendResponse(
|
|
742
|
+
agentId, clientAddress, feedbackIndex, response
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
def getReputationSummary(
|
|
746
|
+
self,
|
|
747
|
+
agentId: "AgentId",
|
|
748
|
+
) -> Dict[str, Any]:
|
|
749
|
+
"""Get reputation summary for an agent."""
|
|
750
|
+
return self.feedback_manager.getReputationSummary(
|
|
751
|
+
agentId
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
def transferAgent(
|
|
755
|
+
self,
|
|
756
|
+
agentId: "AgentId",
|
|
757
|
+
newOwnerAddress: str,
|
|
758
|
+
) -> Dict[str, Any]:
|
|
759
|
+
"""Transfer agent ownership to a new address.
|
|
760
|
+
|
|
761
|
+
Convenience method that loads the agent and calls transfer().
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
agentId: The agent ID to transfer
|
|
765
|
+
newOwnerAddress: Ethereum address of the new owner
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
Transaction receipt
|
|
769
|
+
|
|
770
|
+
Raises:
|
|
771
|
+
ValueError: If agent not found or transfer not allowed
|
|
772
|
+
"""
|
|
773
|
+
# Load the agent
|
|
774
|
+
agent = self.loadAgent(agentId)
|
|
775
|
+
|
|
776
|
+
# Call the transfer method
|
|
777
|
+
return agent.transfer(newOwnerAddress)
|
|
778
|
+
|
|
779
|
+
# Utility methods for owner operations
|
|
780
|
+
def getAgentOwner(self, agentId: AgentId) -> str:
|
|
781
|
+
"""Get the current owner of an agent.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
agentId: The agent ID to check (can be "chainId:tokenId" or just tokenId)
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
The current owner's Ethereum address
|
|
788
|
+
|
|
789
|
+
Raises:
|
|
790
|
+
ValueError: If agent ID is invalid or agent doesn't exist
|
|
791
|
+
"""
|
|
792
|
+
try:
|
|
793
|
+
# Parse agentId to extract tokenId
|
|
794
|
+
if ":" in str(agentId):
|
|
795
|
+
tokenId = int(str(agentId).split(":")[-1])
|
|
796
|
+
else:
|
|
797
|
+
tokenId = int(agentId)
|
|
798
|
+
|
|
799
|
+
owner = self.web3_client.call_contract(
|
|
800
|
+
self.identity_registry,
|
|
801
|
+
"ownerOf",
|
|
802
|
+
tokenId
|
|
803
|
+
)
|
|
804
|
+
return owner
|
|
805
|
+
except Exception as e:
|
|
806
|
+
raise ValueError(f"Failed to get owner for agent {agentId}: {e}")
|
|
807
|
+
|
|
808
|
+
def isAgentOwner(self, agentId: AgentId, address: Optional[str] = None) -> bool:
|
|
809
|
+
"""Check if an address is the owner of an agent.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
agentId: The agent ID to check
|
|
813
|
+
address: Address to check (defaults to SDK's signer address)
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
True if the address is the owner, False otherwise
|
|
817
|
+
|
|
818
|
+
Raises:
|
|
819
|
+
ValueError: If agent ID is invalid or agent doesn't exist
|
|
820
|
+
"""
|
|
821
|
+
if address is None:
|
|
822
|
+
if not self.signer:
|
|
823
|
+
raise ValueError("No signer available and no address provided")
|
|
824
|
+
address = self.web3_client.account.address
|
|
825
|
+
|
|
826
|
+
try:
|
|
827
|
+
owner = self.getAgentOwner(agentId)
|
|
828
|
+
return owner.lower() == address.lower()
|
|
829
|
+
except ValueError:
|
|
830
|
+
return False
|
|
831
|
+
|
|
832
|
+
def canTransferAgent(self, agentId: AgentId, address: Optional[str] = None) -> bool:
|
|
833
|
+
"""Check if an address can transfer an agent (i.e., is the owner).
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
agentId: The agent ID to check
|
|
837
|
+
address: Address to check (defaults to SDK's signer address)
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
True if the address can transfer the agent, False otherwise
|
|
841
|
+
"""
|
|
842
|
+
return self.isAgentOwner(agentId, address)
|