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
agent0_sdk/core/sdk.py
ADDED
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main SDK class for Agent0.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union, Literal
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
from .models import (
|
|
17
|
+
AgentId, ChainId, Address, URI, Timestamp, IdemKey,
|
|
18
|
+
EndpointType, TrustModel, Endpoint, RegistrationFile,
|
|
19
|
+
AgentSummary, Feedback, SearchParams
|
|
20
|
+
)
|
|
21
|
+
from .web3_client import Web3Client
|
|
22
|
+
from .contracts import (
|
|
23
|
+
IDENTITY_REGISTRY_ABI, REPUTATION_REGISTRY_ABI, VALIDATION_REGISTRY_ABI,
|
|
24
|
+
DEFAULT_REGISTRIES, DEFAULT_SUBGRAPH_URLS
|
|
25
|
+
)
|
|
26
|
+
from .agent import Agent
|
|
27
|
+
from .indexer import AgentIndexer
|
|
28
|
+
from .ipfs_client import IPFSClient
|
|
29
|
+
from .feedback_manager import FeedbackManager
|
|
30
|
+
from .transaction_handle import TransactionHandle
|
|
31
|
+
from .subgraph_client import SubgraphClient
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SDK:
|
|
35
|
+
"""Main SDK class for Agent0."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
chainId: ChainId,
|
|
40
|
+
rpcUrl: str,
|
|
41
|
+
signer: Optional[Any] = None, # Optional for read-only operations
|
|
42
|
+
registryOverrides: Optional[Dict[ChainId, Dict[str, Address]]] = None,
|
|
43
|
+
indexingStore: Optional[Any] = None, # optional (e.g., sqlite/postgres/duckdb)
|
|
44
|
+
embeddings: Optional[Any] = None, # optional vector backend
|
|
45
|
+
# IPFS configuration
|
|
46
|
+
ipfs: Optional[str] = None, # "node", "filecoinPin", or "pinata"
|
|
47
|
+
# Direct IPFS node config
|
|
48
|
+
ipfsNodeUrl: Optional[str] = None,
|
|
49
|
+
# Filecoin Pin config
|
|
50
|
+
filecoinPrivateKey: Optional[str] = None,
|
|
51
|
+
# Pinata config
|
|
52
|
+
pinataJwt: Optional[str] = None,
|
|
53
|
+
# Subgraph configuration
|
|
54
|
+
subgraphOverrides: Optional[Dict[ChainId, str]] = None, # Override subgraph URLs per chain
|
|
55
|
+
):
|
|
56
|
+
"""Initialize the SDK."""
|
|
57
|
+
self.chainId = chainId
|
|
58
|
+
self.rpcUrl = rpcUrl
|
|
59
|
+
self.signer = signer
|
|
60
|
+
|
|
61
|
+
# Initialize Web3 client (with or without signer for read-only operations)
|
|
62
|
+
if signer:
|
|
63
|
+
if isinstance(signer, str):
|
|
64
|
+
self.web3_client = Web3Client(rpcUrl, private_key=signer)
|
|
65
|
+
else:
|
|
66
|
+
self.web3_client = Web3Client(rpcUrl, account=signer)
|
|
67
|
+
else:
|
|
68
|
+
# Read-only mode - no signer
|
|
69
|
+
self.web3_client = Web3Client(rpcUrl)
|
|
70
|
+
|
|
71
|
+
# Registry addresses
|
|
72
|
+
self.registry_overrides = registryOverrides or {}
|
|
73
|
+
self._registries = self._resolve_registries()
|
|
74
|
+
|
|
75
|
+
# Initialize contract instances
|
|
76
|
+
self._identity_registry = None
|
|
77
|
+
self._reputation_registry = None
|
|
78
|
+
self._validation_registry = None
|
|
79
|
+
|
|
80
|
+
# Resolve subgraph URL (with fallback chain)
|
|
81
|
+
self._subgraph_urls = {}
|
|
82
|
+
if subgraphOverrides:
|
|
83
|
+
self._subgraph_urls.update(subgraphOverrides)
|
|
84
|
+
|
|
85
|
+
# Get subgraph URL for current chain
|
|
86
|
+
resolved_subgraph_url = None
|
|
87
|
+
|
|
88
|
+
# Priority 1: Chain-specific override
|
|
89
|
+
if chainId in self._subgraph_urls:
|
|
90
|
+
resolved_subgraph_url = self._subgraph_urls[chainId]
|
|
91
|
+
# Priority 2: Default for chain
|
|
92
|
+
elif chainId in DEFAULT_SUBGRAPH_URLS:
|
|
93
|
+
resolved_subgraph_url = DEFAULT_SUBGRAPH_URLS[chainId]
|
|
94
|
+
else:
|
|
95
|
+
# No subgraph available - subgraph_client will be None
|
|
96
|
+
resolved_subgraph_url = None
|
|
97
|
+
|
|
98
|
+
# Initialize subgraph client if URL available
|
|
99
|
+
if resolved_subgraph_url:
|
|
100
|
+
self.subgraph_client = SubgraphClient(resolved_subgraph_url)
|
|
101
|
+
else:
|
|
102
|
+
self.subgraph_client = None
|
|
103
|
+
|
|
104
|
+
# Initialize services
|
|
105
|
+
self.indexer = AgentIndexer(
|
|
106
|
+
web3_client=self.web3_client,
|
|
107
|
+
store=indexingStore,
|
|
108
|
+
embeddings=embeddings,
|
|
109
|
+
subgraph_client=self.subgraph_client,
|
|
110
|
+
subgraph_url_overrides=self._subgraph_urls
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Initialize IPFS client based on configuration
|
|
114
|
+
self.ipfs_client = self._initialize_ipfs_client(
|
|
115
|
+
ipfs, ipfsNodeUrl, filecoinPrivateKey, pinataJwt
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Load registries before passing to FeedbackManager
|
|
119
|
+
identity_registry = self.identity_registry
|
|
120
|
+
reputation_registry = self.reputation_registry
|
|
121
|
+
|
|
122
|
+
self.feedback_manager = FeedbackManager(
|
|
123
|
+
subgraph_client=self.subgraph_client,
|
|
124
|
+
web3_client=self.web3_client,
|
|
125
|
+
ipfs_client=self.ipfs_client,
|
|
126
|
+
reputation_registry=reputation_registry,
|
|
127
|
+
identity_registry=identity_registry,
|
|
128
|
+
indexer=self.indexer # Pass indexer for unified search interface
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _resolve_registries(self) -> Dict[str, Address]:
|
|
132
|
+
"""Resolve registry addresses for current chain."""
|
|
133
|
+
# Start with defaults
|
|
134
|
+
registries = DEFAULT_REGISTRIES.get(self.chainId, {}).copy()
|
|
135
|
+
|
|
136
|
+
# Apply overrides
|
|
137
|
+
if self.chainId in self.registry_overrides:
|
|
138
|
+
registries.update(self.registry_overrides[self.chainId])
|
|
139
|
+
|
|
140
|
+
return registries
|
|
141
|
+
|
|
142
|
+
def _initialize_ipfs_client(
|
|
143
|
+
self,
|
|
144
|
+
ipfs: Optional[str],
|
|
145
|
+
ipfsNodeUrl: Optional[str],
|
|
146
|
+
filecoinPrivateKey: Optional[str],
|
|
147
|
+
pinataJwt: Optional[str]
|
|
148
|
+
) -> Optional[IPFSClient]:
|
|
149
|
+
"""Initialize IPFS client based on configuration."""
|
|
150
|
+
if not ipfs:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
if ipfs == "node":
|
|
154
|
+
if not ipfsNodeUrl:
|
|
155
|
+
raise ValueError("ipfsNodeUrl is required when ipfs='node'")
|
|
156
|
+
return IPFSClient(url=ipfsNodeUrl, filecoin_pin_enabled=False)
|
|
157
|
+
|
|
158
|
+
elif ipfs == "filecoinPin":
|
|
159
|
+
if not filecoinPrivateKey:
|
|
160
|
+
raise ValueError("filecoinPrivateKey is required when ipfs='filecoinPin'")
|
|
161
|
+
return IPFSClient(
|
|
162
|
+
url=None,
|
|
163
|
+
filecoin_pin_enabled=True,
|
|
164
|
+
filecoin_private_key=filecoinPrivateKey
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
elif ipfs == "pinata":
|
|
168
|
+
if not pinataJwt:
|
|
169
|
+
raise ValueError("pinataJwt is required when ipfs='pinata'")
|
|
170
|
+
return IPFSClient(
|
|
171
|
+
url=None,
|
|
172
|
+
filecoin_pin_enabled=False,
|
|
173
|
+
pinata_enabled=True,
|
|
174
|
+
pinata_jwt=pinataJwt
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
raise ValueError(f"Invalid ipfs value: {ipfs}. Must be 'node', 'filecoinPin', or 'pinata'")
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def isReadOnly(self) -> bool:
|
|
182
|
+
"""Check if SDK is in read-only mode (no signer)."""
|
|
183
|
+
return self.signer is None
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def identity_registry(self):
|
|
187
|
+
"""Get identity registry contract."""
|
|
188
|
+
if self._identity_registry is None:
|
|
189
|
+
address = self._registries.get("IDENTITY")
|
|
190
|
+
if not address:
|
|
191
|
+
raise ValueError(f"No identity registry address for chain {self.chainId}")
|
|
192
|
+
self._identity_registry = self.web3_client.get_contract(
|
|
193
|
+
address, IDENTITY_REGISTRY_ABI
|
|
194
|
+
)
|
|
195
|
+
return self._identity_registry
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def reputation_registry(self):
|
|
199
|
+
"""Get reputation registry contract."""
|
|
200
|
+
if self._reputation_registry is None:
|
|
201
|
+
address = self._registries.get("REPUTATION")
|
|
202
|
+
if not address:
|
|
203
|
+
raise ValueError(f"No reputation registry address for chain {self.chainId}")
|
|
204
|
+
self._reputation_registry = self.web3_client.get_contract(
|
|
205
|
+
address, REPUTATION_REGISTRY_ABI
|
|
206
|
+
)
|
|
207
|
+
return self._reputation_registry
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def validation_registry(self):
|
|
211
|
+
"""Get validation registry contract."""
|
|
212
|
+
if self._validation_registry is None:
|
|
213
|
+
address = self._registries.get("VALIDATION")
|
|
214
|
+
if not address:
|
|
215
|
+
raise ValueError(f"No validation registry address for chain {self.chainId}")
|
|
216
|
+
self._validation_registry = self.web3_client.get_contract(
|
|
217
|
+
address, VALIDATION_REGISTRY_ABI
|
|
218
|
+
)
|
|
219
|
+
return self._validation_registry
|
|
220
|
+
|
|
221
|
+
def chain_id(self) -> ChainId:
|
|
222
|
+
"""Get current chain ID."""
|
|
223
|
+
return self.chainId
|
|
224
|
+
|
|
225
|
+
def registries(self) -> Dict[str, Address]:
|
|
226
|
+
"""Get resolved addresses for current chain."""
|
|
227
|
+
return self._registries.copy()
|
|
228
|
+
|
|
229
|
+
def get_subgraph_client(self, chain_id: Optional[ChainId] = None) -> Optional[SubgraphClient]:
|
|
230
|
+
"""
|
|
231
|
+
Get subgraph client for a specific chain.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
chain_id: Chain ID (defaults to current chain)
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
SubgraphClient instance or None if no subgraph available
|
|
238
|
+
"""
|
|
239
|
+
target_chain = chain_id if chain_id is not None else self.chainId
|
|
240
|
+
|
|
241
|
+
# Check if we already have a client for this chain
|
|
242
|
+
if target_chain == self.chainId and self.subgraph_client:
|
|
243
|
+
return self.subgraph_client
|
|
244
|
+
|
|
245
|
+
# Resolve URL for target chain
|
|
246
|
+
url = None
|
|
247
|
+
if target_chain in self._subgraph_urls:
|
|
248
|
+
url = self._subgraph_urls[target_chain]
|
|
249
|
+
elif target_chain in DEFAULT_SUBGRAPH_URLS:
|
|
250
|
+
url = DEFAULT_SUBGRAPH_URLS[target_chain]
|
|
251
|
+
|
|
252
|
+
if url:
|
|
253
|
+
return SubgraphClient(url)
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
def set_chain(self, chain_id: ChainId) -> None:
|
|
257
|
+
"""Switch chains (advanced)."""
|
|
258
|
+
self.chainId = chain_id
|
|
259
|
+
self._registries = self._resolve_registries()
|
|
260
|
+
# Reset contract instances
|
|
261
|
+
self._identity_registry = None
|
|
262
|
+
self._reputation_registry = None
|
|
263
|
+
self._validation_registry = None
|
|
264
|
+
|
|
265
|
+
# Agent lifecycle methods
|
|
266
|
+
def createAgent(
|
|
267
|
+
self,
|
|
268
|
+
name: str,
|
|
269
|
+
description: str,
|
|
270
|
+
image: Optional[URI] = None,
|
|
271
|
+
) -> Agent:
|
|
272
|
+
"""Create a new agent (off-chain object in memory)."""
|
|
273
|
+
registration_file = RegistrationFile(
|
|
274
|
+
name=name,
|
|
275
|
+
description=description,
|
|
276
|
+
image=image,
|
|
277
|
+
# Default trust model: reputation (if caller doesn't set one explicitly).
|
|
278
|
+
trustModels=[TrustModel.REPUTATION],
|
|
279
|
+
updatedAt=int(time.time())
|
|
280
|
+
)
|
|
281
|
+
return Agent(sdk=self, registration_file=registration_file)
|
|
282
|
+
|
|
283
|
+
def loadAgent(self, agentId: AgentId) -> Agent:
|
|
284
|
+
"""Load an existing agent (hydrates from registration file if registered).
|
|
285
|
+
|
|
286
|
+
Note: Agents can be minted with an empty token URI (e.g. IPFS flow where publish fails).
|
|
287
|
+
In that case we return a partially-hydrated Agent with an empty registration file so the
|
|
288
|
+
caller can resume publishing and set the URI later.
|
|
289
|
+
"""
|
|
290
|
+
# Convert agentId to string if it's an integer
|
|
291
|
+
agentId = str(agentId)
|
|
292
|
+
|
|
293
|
+
# Parse agent ID
|
|
294
|
+
if ":" in agentId:
|
|
295
|
+
chain_id, token_id = agentId.split(":", 1)
|
|
296
|
+
if int(chain_id) != self.chainId:
|
|
297
|
+
raise ValueError(f"Agent {agentId} is not on current chain {self.chainId}")
|
|
298
|
+
else:
|
|
299
|
+
token_id = agentId
|
|
300
|
+
|
|
301
|
+
# Get token URI from contract
|
|
302
|
+
try:
|
|
303
|
+
agent_uri = self.web3_client.call_contract(
|
|
304
|
+
self.identity_registry, "tokenURI", int(token_id) # tokenURI is ERC-721 standard, but represents agentURI
|
|
305
|
+
)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
raise ValueError(f"Failed to load agent {agentId}: {e}")
|
|
308
|
+
|
|
309
|
+
# Load registration file (or fall back to a minimal file if agent URI is missing)
|
|
310
|
+
registration_file = self._load_registration_file(agent_uri)
|
|
311
|
+
registration_file.agentId = agentId
|
|
312
|
+
registration_file.agentURI = agent_uri if agent_uri else None
|
|
313
|
+
|
|
314
|
+
if not agent_uri or not str(agent_uri).strip():
|
|
315
|
+
logger.warning(
|
|
316
|
+
f"Agent {agentId} has no agentURI set on-chain yet. "
|
|
317
|
+
"Returning a partial agent; update info and call registerIPFS() to publish and set URI."
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Store registry address for proper JSON generation
|
|
321
|
+
registry_address = self._registries.get("IDENTITY")
|
|
322
|
+
if registry_address:
|
|
323
|
+
registration_file._registry_address = registry_address
|
|
324
|
+
registration_file._chain_id = self.chainId
|
|
325
|
+
|
|
326
|
+
# Hydrate on-chain data
|
|
327
|
+
self._hydrate_agent_data(registration_file, int(token_id))
|
|
328
|
+
|
|
329
|
+
return Agent(sdk=self, registration_file=registration_file)
|
|
330
|
+
|
|
331
|
+
def _load_registration_file(self, uri: str) -> RegistrationFile:
|
|
332
|
+
"""Load registration file from URI.
|
|
333
|
+
|
|
334
|
+
If uri is empty/None/whitespace, returns an empty RegistrationFile to allow resume flows.
|
|
335
|
+
"""
|
|
336
|
+
if not uri or not str(uri).strip():
|
|
337
|
+
return RegistrationFile()
|
|
338
|
+
|
|
339
|
+
if uri.startswith("ipfs://"):
|
|
340
|
+
if not self.ipfs_client:
|
|
341
|
+
raise ValueError("IPFS client not configured")
|
|
342
|
+
content = self.ipfs_client.get(uri)
|
|
343
|
+
elif uri.startswith("http"):
|
|
344
|
+
try:
|
|
345
|
+
import requests
|
|
346
|
+
response = requests.get(uri)
|
|
347
|
+
response.raise_for_status()
|
|
348
|
+
content = response.text
|
|
349
|
+
except ImportError:
|
|
350
|
+
raise ImportError("requests not installed. Install with: pip install requests")
|
|
351
|
+
else:
|
|
352
|
+
raise ValueError(f"Unsupported URI scheme: {uri}")
|
|
353
|
+
|
|
354
|
+
data = json.loads(content)
|
|
355
|
+
return RegistrationFile.from_dict(data)
|
|
356
|
+
|
|
357
|
+
def _hydrate_agent_data(self, registration_file: RegistrationFile, token_id: int):
|
|
358
|
+
"""Hydrate agent data from on-chain sources."""
|
|
359
|
+
# Get owner
|
|
360
|
+
owner = self.web3_client.call_contract(
|
|
361
|
+
self.identity_registry, "ownerOf", token_id
|
|
362
|
+
)
|
|
363
|
+
registration_file.owners = [owner]
|
|
364
|
+
|
|
365
|
+
# Get operators (this would require additional contract calls)
|
|
366
|
+
# For now, we'll leave it empty
|
|
367
|
+
registration_file.operators = []
|
|
368
|
+
|
|
369
|
+
# Hydrate agentWallet from on-chain (now uses getAgentWallet() instead of metadata)
|
|
370
|
+
agent_id = token_id
|
|
371
|
+
try:
|
|
372
|
+
# Get agentWallet using the new dedicated function
|
|
373
|
+
wallet_address = self.web3_client.call_contract(
|
|
374
|
+
self.identity_registry, "getAgentWallet", agent_id
|
|
375
|
+
)
|
|
376
|
+
if wallet_address and wallet_address != "0x0000000000000000000000000000000000000000":
|
|
377
|
+
registration_file.walletAddress = wallet_address
|
|
378
|
+
# If wallet is read from on-chain, use current chain ID
|
|
379
|
+
# (the chain ID from the registration file might be outdated)
|
|
380
|
+
registration_file.walletChainId = self.chainId
|
|
381
|
+
except Exception as e:
|
|
382
|
+
# No on-chain wallet set, will fall back to registration file
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
# Try to get agentName (ENS) from on-chain metadata
|
|
387
|
+
name_bytes = self.web3_client.call_contract(
|
|
388
|
+
self.identity_registry, "getMetadata", agent_id, "agentName"
|
|
389
|
+
)
|
|
390
|
+
if name_bytes and len(name_bytes) > 0:
|
|
391
|
+
ens_name = name_bytes.decode('utf-8')
|
|
392
|
+
# Add ENS endpoint to registration file
|
|
393
|
+
from .models import EndpointType, Endpoint
|
|
394
|
+
# Remove existing ENS endpoints
|
|
395
|
+
registration_file.endpoints = [
|
|
396
|
+
ep for ep in registration_file.endpoints
|
|
397
|
+
if ep.type != EndpointType.ENS
|
|
398
|
+
]
|
|
399
|
+
# Add new ENS endpoint
|
|
400
|
+
ens_endpoint = Endpoint(
|
|
401
|
+
type=EndpointType.ENS,
|
|
402
|
+
value=ens_name,
|
|
403
|
+
meta={"version": "1.0"}
|
|
404
|
+
)
|
|
405
|
+
registration_file.endpoints.append(ens_endpoint)
|
|
406
|
+
except Exception as e:
|
|
407
|
+
# No on-chain ENS name, will fall back to registration file
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
# Try to get custom metadata keys from registration file and check on-chain
|
|
411
|
+
# Note: We can't enumerate on-chain metadata keys, so we check each key from the registration file
|
|
412
|
+
# Also check for common custom metadata keys that might exist on-chain
|
|
413
|
+
keys_to_check = list(registration_file.metadata.keys())
|
|
414
|
+
# Also check for known metadata keys that might have been set on-chain
|
|
415
|
+
known_keys = ["testKey", "version", "timestamp", "customField", "anotherField", "numericField"]
|
|
416
|
+
for key in known_keys:
|
|
417
|
+
if key not in keys_to_check:
|
|
418
|
+
keys_to_check.append(key)
|
|
419
|
+
|
|
420
|
+
for key in keys_to_check:
|
|
421
|
+
try:
|
|
422
|
+
value_bytes = self.web3_client.call_contract(
|
|
423
|
+
self.identity_registry, "getMetadata", agent_id, key
|
|
424
|
+
)
|
|
425
|
+
if value_bytes and len(value_bytes) > 0:
|
|
426
|
+
value_str = value_bytes.decode('utf-8')
|
|
427
|
+
# Try to convert back to original type if possible
|
|
428
|
+
try:
|
|
429
|
+
# Try integer
|
|
430
|
+
value_int = int(value_str)
|
|
431
|
+
# Check if it's actually stored as integer in metadata or if it was originally a string
|
|
432
|
+
registration_file.metadata[key] = value_str # Keep as string for now
|
|
433
|
+
except ValueError:
|
|
434
|
+
# Try float
|
|
435
|
+
try:
|
|
436
|
+
value_float = float(value_str)
|
|
437
|
+
registration_file.metadata[key] = value_str # Keep as string for now
|
|
438
|
+
except ValueError:
|
|
439
|
+
registration_file.metadata[key] = value_str
|
|
440
|
+
except Exception as e:
|
|
441
|
+
# Keep registration file value if on-chain not found
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
# Discovery and indexing
|
|
445
|
+
def refreshAgentIndex(self, agentId: AgentId, deep: bool = False) -> AgentSummary:
|
|
446
|
+
"""Refresh index for a single agent."""
|
|
447
|
+
return asyncio.run(self.indexer.refresh_agent(agentId, deep=deep))
|
|
448
|
+
|
|
449
|
+
def refreshIndex(
|
|
450
|
+
self,
|
|
451
|
+
agentIds: Optional[List[AgentId]] = None,
|
|
452
|
+
concurrency: int = 8,
|
|
453
|
+
) -> List[AgentSummary]:
|
|
454
|
+
"""Refresh index for multiple agents."""
|
|
455
|
+
return asyncio.run(self.indexer.refresh_agents(agentIds, concurrency))
|
|
456
|
+
|
|
457
|
+
def getAgent(self, agentId: AgentId) -> AgentSummary:
|
|
458
|
+
"""Get agent summary from index."""
|
|
459
|
+
return self.indexer.get_agent(agentId)
|
|
460
|
+
|
|
461
|
+
def searchAgents(
|
|
462
|
+
self,
|
|
463
|
+
params: Union[SearchParams, Dict[str, Any], None] = None,
|
|
464
|
+
sort: Union[str, List[str], None] = None,
|
|
465
|
+
page_size: int = 50,
|
|
466
|
+
cursor: Optional[str] = None,
|
|
467
|
+
**kwargs # Accept search criteria as kwargs for better DX
|
|
468
|
+
) -> Dict[str, Any]:
|
|
469
|
+
"""Search for agents.
|
|
470
|
+
|
|
471
|
+
Examples:
|
|
472
|
+
# Simple kwargs for better developer experience
|
|
473
|
+
sdk.searchAgents(name="Test")
|
|
474
|
+
sdk.searchAgents(mcpTools=["code_generation"], active=True)
|
|
475
|
+
|
|
476
|
+
# Explicit SearchParams (for complex queries or IDE autocomplete)
|
|
477
|
+
sdk.searchAgents(SearchParams(name="Test", mcpTools=["code_generation"]))
|
|
478
|
+
|
|
479
|
+
# With pagination
|
|
480
|
+
sdk.searchAgents(name="Test", page_size=10)
|
|
481
|
+
"""
|
|
482
|
+
# If kwargs provided, use them instead of params
|
|
483
|
+
if kwargs and params is None:
|
|
484
|
+
params = SearchParams(**kwargs)
|
|
485
|
+
elif params is None:
|
|
486
|
+
params = SearchParams()
|
|
487
|
+
elif isinstance(params, dict):
|
|
488
|
+
params = SearchParams(**params)
|
|
489
|
+
|
|
490
|
+
if sort is None:
|
|
491
|
+
sort = ["updatedAt:desc"]
|
|
492
|
+
elif isinstance(sort, str):
|
|
493
|
+
sort = [sort]
|
|
494
|
+
|
|
495
|
+
return self.indexer.search_agents(params, sort, page_size, cursor)
|
|
496
|
+
|
|
497
|
+
# Feedback methods are defined later in this class (single authoritative API).
|
|
498
|
+
|
|
499
|
+
def searchAgentsByReputation(
|
|
500
|
+
self,
|
|
501
|
+
agents: Optional[List[AgentId]] = None,
|
|
502
|
+
tags: Optional[List[str]] = None,
|
|
503
|
+
reviewers: Optional[List[Address]] = None,
|
|
504
|
+
capabilities: Optional[List[str]] = None,
|
|
505
|
+
skills: Optional[List[str]] = None,
|
|
506
|
+
tasks: Optional[List[str]] = None,
|
|
507
|
+
names: Optional[List[str]] = None,
|
|
508
|
+
minAverageValue: Optional[float] = None,
|
|
509
|
+
includeRevoked: bool = False,
|
|
510
|
+
page_size: int = 50,
|
|
511
|
+
cursor: Optional[str] = None,
|
|
512
|
+
sort: Optional[List[str]] = None,
|
|
513
|
+
chains: Optional[Union[List[ChainId], Literal["all"]]] = None,
|
|
514
|
+
) -> Dict[str, Any]:
|
|
515
|
+
"""Search agents filtered by reputation criteria."""
|
|
516
|
+
# Handle multi-chain search
|
|
517
|
+
if chains:
|
|
518
|
+
# Expand "all" if needed
|
|
519
|
+
if chains == "all":
|
|
520
|
+
chains = self.indexer._get_all_configured_chains()
|
|
521
|
+
|
|
522
|
+
# If multiple chains or single chain different from default
|
|
523
|
+
if isinstance(chains, list) and len(chains) > 0:
|
|
524
|
+
if len(chains) > 1 or (len(chains) == 1 and chains[0] != self.chainId):
|
|
525
|
+
return asyncio.run(
|
|
526
|
+
self._search_agents_by_reputation_across_chains(
|
|
527
|
+
agents, tags, reviewers, capabilities, skills, tasks, names,
|
|
528
|
+
minAverageValue, includeRevoked, page_size, cursor, sort, chains
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Single chain search (existing behavior)
|
|
533
|
+
if not self.subgraph_client:
|
|
534
|
+
raise ValueError("Subgraph client required for searchAgentsByReputation")
|
|
535
|
+
|
|
536
|
+
if sort is None:
|
|
537
|
+
sort = ["createdAt:desc"]
|
|
538
|
+
|
|
539
|
+
skip = 0
|
|
540
|
+
if cursor:
|
|
541
|
+
try:
|
|
542
|
+
skip = int(cursor)
|
|
543
|
+
except ValueError:
|
|
544
|
+
skip = 0
|
|
545
|
+
|
|
546
|
+
order_by = "createdAt"
|
|
547
|
+
order_direction = "desc"
|
|
548
|
+
if sort and len(sort) > 0:
|
|
549
|
+
sort_field = sort[0].split(":")
|
|
550
|
+
order_by = sort_field[0] if len(sort_field) >= 1 else order_by
|
|
551
|
+
order_direction = sort_field[1] if len(sort_field) >= 2 else order_direction
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
agents_data = self.subgraph_client.search_agents_by_reputation(
|
|
555
|
+
agents=agents,
|
|
556
|
+
tags=tags,
|
|
557
|
+
reviewers=reviewers,
|
|
558
|
+
capabilities=capabilities,
|
|
559
|
+
skills=skills,
|
|
560
|
+
tasks=tasks,
|
|
561
|
+
names=names,
|
|
562
|
+
minAverageValue=minAverageValue,
|
|
563
|
+
includeRevoked=includeRevoked,
|
|
564
|
+
first=page_size,
|
|
565
|
+
skip=skip,
|
|
566
|
+
order_by=order_by,
|
|
567
|
+
order_direction=order_direction
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
from .models import AgentSummary
|
|
571
|
+
results = []
|
|
572
|
+
for agent_data in agents_data:
|
|
573
|
+
reg_file = agent_data.get('registrationFile') or {}
|
|
574
|
+
if not isinstance(reg_file, dict):
|
|
575
|
+
reg_file = {}
|
|
576
|
+
|
|
577
|
+
agent_summary = AgentSummary(
|
|
578
|
+
chainId=int(agent_data.get('chainId', 0)),
|
|
579
|
+
agentId=agent_data.get('id'),
|
|
580
|
+
name=reg_file.get('name', f"Agent {agent_data.get('id')}"),
|
|
581
|
+
image=reg_file.get('image'),
|
|
582
|
+
description=reg_file.get('description', ''),
|
|
583
|
+
owners=[agent_data.get('owner', '')],
|
|
584
|
+
operators=agent_data.get('operators', []),
|
|
585
|
+
mcp=reg_file.get('mcpEndpoint') is not None,
|
|
586
|
+
a2a=reg_file.get('a2aEndpoint') is not None,
|
|
587
|
+
ens=reg_file.get('ens'),
|
|
588
|
+
did=reg_file.get('did'),
|
|
589
|
+
walletAddress=reg_file.get('agentWallet'),
|
|
590
|
+
supportedTrusts=reg_file.get('supportedTrusts', []),
|
|
591
|
+
a2aSkills=reg_file.get('a2aSkills', []),
|
|
592
|
+
mcpTools=reg_file.get('mcpTools', []),
|
|
593
|
+
mcpPrompts=reg_file.get('mcpPrompts', []),
|
|
594
|
+
mcpResources=reg_file.get('mcpResources', []),
|
|
595
|
+
active=reg_file.get('active', True),
|
|
596
|
+
x402support=reg_file.get('x402Support', reg_file.get('x402support', False)),
|
|
597
|
+
extras={'averageValue': agent_data.get('averageValue')}
|
|
598
|
+
)
|
|
599
|
+
results.append(agent_summary)
|
|
600
|
+
|
|
601
|
+
next_cursor = str(skip + len(results)) if len(results) == page_size else None
|
|
602
|
+
return {"items": results, "nextCursor": next_cursor}
|
|
603
|
+
|
|
604
|
+
except Exception as e:
|
|
605
|
+
raise ValueError(f"Failed to search agents by reputation: {e}")
|
|
606
|
+
|
|
607
|
+
async def _search_agents_by_reputation_across_chains(
|
|
608
|
+
self,
|
|
609
|
+
agents: Optional[List[AgentId]],
|
|
610
|
+
tags: Optional[List[str]],
|
|
611
|
+
reviewers: Optional[List[Address]],
|
|
612
|
+
capabilities: Optional[List[str]],
|
|
613
|
+
skills: Optional[List[str]],
|
|
614
|
+
tasks: Optional[List[str]],
|
|
615
|
+
names: Optional[List[str]],
|
|
616
|
+
minAverageValue: Optional[float],
|
|
617
|
+
includeRevoked: bool,
|
|
618
|
+
page_size: int,
|
|
619
|
+
cursor: Optional[str],
|
|
620
|
+
sort: Optional[List[str]],
|
|
621
|
+
chains: List[ChainId],
|
|
622
|
+
) -> Dict[str, Any]:
|
|
623
|
+
"""
|
|
624
|
+
Search agents by reputation across multiple chains in parallel.
|
|
625
|
+
|
|
626
|
+
Similar to indexer._search_agents_across_chains() but for reputation-based search.
|
|
627
|
+
"""
|
|
628
|
+
import time
|
|
629
|
+
start_time = time.time()
|
|
630
|
+
|
|
631
|
+
if sort is None:
|
|
632
|
+
sort = ["createdAt:desc"]
|
|
633
|
+
|
|
634
|
+
order_by = "createdAt"
|
|
635
|
+
order_direction = "desc"
|
|
636
|
+
if sort and len(sort) > 0:
|
|
637
|
+
sort_field = sort[0].split(":")
|
|
638
|
+
order_by = sort_field[0] if len(sort_field) >= 1 else order_by
|
|
639
|
+
order_direction = sort_field[1] if len(sort_field) >= 2 else order_direction
|
|
640
|
+
|
|
641
|
+
skip = 0
|
|
642
|
+
if cursor:
|
|
643
|
+
try:
|
|
644
|
+
skip = int(cursor)
|
|
645
|
+
except ValueError:
|
|
646
|
+
skip = 0
|
|
647
|
+
|
|
648
|
+
# Define async function for querying a single chain
|
|
649
|
+
async def query_single_chain(chain_id: int) -> Dict[str, Any]:
|
|
650
|
+
"""Query one chain and return its results with metadata."""
|
|
651
|
+
try:
|
|
652
|
+
# Get subgraph client for this chain
|
|
653
|
+
subgraph_client = self.indexer._get_subgraph_client_for_chain(chain_id)
|
|
654
|
+
|
|
655
|
+
if subgraph_client is None:
|
|
656
|
+
logger.warning(f"No subgraph client available for chain {chain_id}")
|
|
657
|
+
return {
|
|
658
|
+
"chainId": chain_id,
|
|
659
|
+
"status": "unavailable",
|
|
660
|
+
"agents": [],
|
|
661
|
+
"error": f"No subgraph configured for chain {chain_id}"
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# Execute reputation search query
|
|
665
|
+
try:
|
|
666
|
+
agents_data = subgraph_client.search_agents_by_reputation(
|
|
667
|
+
agents=agents,
|
|
668
|
+
tags=tags,
|
|
669
|
+
reviewers=reviewers,
|
|
670
|
+
capabilities=capabilities,
|
|
671
|
+
skills=skills,
|
|
672
|
+
tasks=tasks,
|
|
673
|
+
names=names,
|
|
674
|
+
minAverageValue=minAverageValue,
|
|
675
|
+
includeRevoked=includeRevoked,
|
|
676
|
+
first=page_size * 3, # Fetch extra to allow for filtering/sorting
|
|
677
|
+
skip=0, # We'll handle pagination after aggregation
|
|
678
|
+
order_by=order_by,
|
|
679
|
+
order_direction=order_direction
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
logger.info(f"Chain {chain_id}: fetched {len(agents_data)} agents by reputation")
|
|
683
|
+
except Exception as e:
|
|
684
|
+
logger.error(f"Error in search_agents_by_reputation for chain {chain_id}: {e}", exc_info=True)
|
|
685
|
+
agents_data = []
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
"chainId": chain_id,
|
|
689
|
+
"status": "success",
|
|
690
|
+
"agents": agents_data,
|
|
691
|
+
"count": len(agents_data),
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
except Exception as e:
|
|
695
|
+
logger.error(f"Error querying chain {chain_id} for reputation search: {e}", exc_info=True)
|
|
696
|
+
return {
|
|
697
|
+
"chainId": chain_id,
|
|
698
|
+
"status": "error",
|
|
699
|
+
"agents": [],
|
|
700
|
+
"error": str(e),
|
|
701
|
+
"count": 0
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
# Execute queries in parallel
|
|
705
|
+
chain_tasks = [query_single_chain(chain_id) for chain_id in chains]
|
|
706
|
+
chain_results = await asyncio.gather(*chain_tasks)
|
|
707
|
+
|
|
708
|
+
# Aggregate results from all chains
|
|
709
|
+
all_agents = []
|
|
710
|
+
successful_chains = []
|
|
711
|
+
failed_chains = []
|
|
712
|
+
|
|
713
|
+
for result in chain_results:
|
|
714
|
+
chain_id = result["chainId"]
|
|
715
|
+
if result["status"] == "success":
|
|
716
|
+
successful_chains.append(chain_id)
|
|
717
|
+
agents_count = len(result.get("agents", []))
|
|
718
|
+
logger.debug(f"Chain {chain_id}: aggregating {agents_count} agents")
|
|
719
|
+
all_agents.extend(result["agents"])
|
|
720
|
+
else:
|
|
721
|
+
failed_chains.append(chain_id)
|
|
722
|
+
logger.warning(f"Chain {chain_id}: status={result.get('status')}, error={result.get('error', 'N/A')}")
|
|
723
|
+
|
|
724
|
+
logger.debug(f"Total agents aggregated: {len(all_agents)} from {len(successful_chains)} chains")
|
|
725
|
+
|
|
726
|
+
# Transform to AgentSummary objects
|
|
727
|
+
from .models import AgentSummary
|
|
728
|
+
results = []
|
|
729
|
+
for agent_data in all_agents:
|
|
730
|
+
reg_file = agent_data.get('registrationFile') or {}
|
|
731
|
+
if not isinstance(reg_file, dict):
|
|
732
|
+
reg_file = {}
|
|
733
|
+
|
|
734
|
+
agent_summary = AgentSummary(
|
|
735
|
+
chainId=int(agent_data.get('chainId', 0)),
|
|
736
|
+
agentId=agent_data.get('id'),
|
|
737
|
+
name=reg_file.get('name', f"Agent {agent_data.get('id')}"),
|
|
738
|
+
image=reg_file.get('image'),
|
|
739
|
+
description=reg_file.get('description', ''),
|
|
740
|
+
owners=[agent_data.get('owner', '')],
|
|
741
|
+
operators=agent_data.get('operators', []),
|
|
742
|
+
mcp=reg_file.get('mcpEndpoint') is not None,
|
|
743
|
+
a2a=reg_file.get('a2aEndpoint') is not None,
|
|
744
|
+
ens=reg_file.get('ens'),
|
|
745
|
+
did=reg_file.get('did'),
|
|
746
|
+
walletAddress=reg_file.get('agentWallet'),
|
|
747
|
+
supportedTrusts=reg_file.get('supportedTrusts', []),
|
|
748
|
+
a2aSkills=reg_file.get('a2aSkills', []),
|
|
749
|
+
mcpTools=reg_file.get('mcpTools', []),
|
|
750
|
+
mcpPrompts=reg_file.get('mcpPrompts', []),
|
|
751
|
+
mcpResources=reg_file.get('mcpResources', []),
|
|
752
|
+
active=reg_file.get('active', True),
|
|
753
|
+
x402support=reg_file.get('x402Support', reg_file.get('x402support', False)),
|
|
754
|
+
extras={'averageValue': agent_data.get('averageValue')}
|
|
755
|
+
)
|
|
756
|
+
results.append(agent_summary)
|
|
757
|
+
|
|
758
|
+
# Sort by averageValue (descending) if available, otherwise by createdAt
|
|
759
|
+
results.sort(
|
|
760
|
+
key=lambda x: (
|
|
761
|
+
x.extras.get('averageValue') if x.extras.get('averageValue') is not None else 0,
|
|
762
|
+
x.chainId,
|
|
763
|
+
x.agentId
|
|
764
|
+
),
|
|
765
|
+
reverse=True
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# Apply pagination
|
|
769
|
+
paginated_results = results[skip:skip + page_size]
|
|
770
|
+
next_cursor = str(skip + len(paginated_results)) if len(paginated_results) == page_size and skip + len(paginated_results) < len(results) else None
|
|
771
|
+
|
|
772
|
+
elapsed_ms = int((time.time() - start_time) * 1000)
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
"items": paginated_results,
|
|
776
|
+
"nextCursor": next_cursor,
|
|
777
|
+
"meta": {
|
|
778
|
+
"chains": chains,
|
|
779
|
+
"successfulChains": successful_chains,
|
|
780
|
+
"failedChains": failed_chains,
|
|
781
|
+
"totalResults": len(results),
|
|
782
|
+
"timing": {"totalMs": elapsed_ms}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
# Feedback methods - delegate to feedback_manager
|
|
787
|
+
def prepareFeedbackFile(self, input: Dict[str, Any]) -> Dict[str, Any]:
|
|
788
|
+
"""Prepare an off-chain feedback file payload.
|
|
789
|
+
|
|
790
|
+
This is intentionally off-chain-only; it does not attempt to represent
|
|
791
|
+
the on-chain fields (value/tag1/tag2/endpoint-on-chain).
|
|
792
|
+
"""
|
|
793
|
+
return self.feedback_manager.prepareFeedbackFile(input)
|
|
794
|
+
|
|
795
|
+
def giveFeedback(
|
|
796
|
+
self,
|
|
797
|
+
agentId: "AgentId",
|
|
798
|
+
value: Union[int, float, str],
|
|
799
|
+
tag1: Optional[str] = None,
|
|
800
|
+
tag2: Optional[str] = None,
|
|
801
|
+
endpoint: Optional[str] = None,
|
|
802
|
+
feedbackFile: Optional[Dict[str, Any]] = None,
|
|
803
|
+
) -> "TransactionHandle[Feedback]":
|
|
804
|
+
"""Give feedback (on-chain first; optional off-chain file upload).
|
|
805
|
+
|
|
806
|
+
- If feedbackFile is None: submit on-chain only (no upload even if IPFS is configured).
|
|
807
|
+
- If feedbackFile is provided: requires IPFS configured; uploads and commits URI/hash on-chain.
|
|
808
|
+
"""
|
|
809
|
+
return self.feedback_manager.giveFeedback(
|
|
810
|
+
agentId=agentId,
|
|
811
|
+
value=value,
|
|
812
|
+
tag1=tag1,
|
|
813
|
+
tag2=tag2,
|
|
814
|
+
endpoint=endpoint,
|
|
815
|
+
feedbackFile=feedbackFile,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
def getFeedback(
|
|
819
|
+
self,
|
|
820
|
+
agentId: "AgentId",
|
|
821
|
+
clientAddress: "Address",
|
|
822
|
+
feedbackIndex: int,
|
|
823
|
+
) -> "Feedback":
|
|
824
|
+
"""Get feedback (maps 8004 endpoint)."""
|
|
825
|
+
return self.feedback_manager.getFeedback(
|
|
826
|
+
agentId, clientAddress, feedbackIndex
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
def searchFeedback(
|
|
830
|
+
self,
|
|
831
|
+
agentId: Optional["AgentId"] = None,
|
|
832
|
+
reviewers: Optional[List["Address"]] = None,
|
|
833
|
+
tags: Optional[List[str]] = None,
|
|
834
|
+
capabilities: Optional[List[str]] = None,
|
|
835
|
+
skills: Optional[List[str]] = None,
|
|
836
|
+
tasks: Optional[List[str]] = None,
|
|
837
|
+
names: Optional[List[str]] = None,
|
|
838
|
+
minValue: Optional[float] = None,
|
|
839
|
+
maxValue: Optional[float] = None,
|
|
840
|
+
include_revoked: bool = False,
|
|
841
|
+
first: int = 100,
|
|
842
|
+
skip: int = 0,
|
|
843
|
+
agents: Optional[List["AgentId"]] = None,
|
|
844
|
+
) -> List["Feedback"]:
|
|
845
|
+
"""Search feedback.
|
|
846
|
+
|
|
847
|
+
Backwards compatible:
|
|
848
|
+
- Previously required `agentId`; it is now optional.
|
|
849
|
+
|
|
850
|
+
New:
|
|
851
|
+
- `agents` can be used to search feedback across multiple agents in one call.
|
|
852
|
+
- `reviewers` can now be used without specifying any agent, enabling "all feedback given by a wallet".
|
|
853
|
+
"""
|
|
854
|
+
has_any_filter = any([
|
|
855
|
+
bool(agentId),
|
|
856
|
+
bool(agents),
|
|
857
|
+
bool(reviewers),
|
|
858
|
+
bool(tags),
|
|
859
|
+
bool(capabilities),
|
|
860
|
+
bool(skills),
|
|
861
|
+
bool(tasks),
|
|
862
|
+
bool(names),
|
|
863
|
+
minValue is not None,
|
|
864
|
+
maxValue is not None,
|
|
865
|
+
])
|
|
866
|
+
if not has_any_filter:
|
|
867
|
+
raise ValueError(
|
|
868
|
+
"searchFeedback requires at least one filter "
|
|
869
|
+
"(agentId/agents/reviewers/tags/capabilities/skills/tasks/names/minValue/maxValue)."
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
return self.feedback_manager.searchFeedback(
|
|
873
|
+
agentId=agentId,
|
|
874
|
+
agents=agents,
|
|
875
|
+
clientAddresses=reviewers,
|
|
876
|
+
tags=tags,
|
|
877
|
+
capabilities=capabilities,
|
|
878
|
+
skills=skills,
|
|
879
|
+
tasks=tasks,
|
|
880
|
+
names=names,
|
|
881
|
+
minValue=minValue,
|
|
882
|
+
maxValue=maxValue,
|
|
883
|
+
include_revoked=include_revoked,
|
|
884
|
+
first=first,
|
|
885
|
+
skip=skip,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
def revokeFeedback(
|
|
889
|
+
self,
|
|
890
|
+
agentId: "AgentId",
|
|
891
|
+
feedbackIndex: int,
|
|
892
|
+
) -> "TransactionHandle[Feedback]":
|
|
893
|
+
"""Revoke feedback (submitted-by-default)."""
|
|
894
|
+
return self.feedback_manager.revokeFeedback(agentId, feedbackIndex)
|
|
895
|
+
|
|
896
|
+
def appendResponse(
|
|
897
|
+
self,
|
|
898
|
+
agentId: "AgentId",
|
|
899
|
+
clientAddress: "Address",
|
|
900
|
+
feedbackIndex: int,
|
|
901
|
+
response: Dict[str, Any],
|
|
902
|
+
) -> "TransactionHandle[Feedback]":
|
|
903
|
+
"""Append a response/follow-up to existing feedback (submitted-by-default)."""
|
|
904
|
+
return self.feedback_manager.appendResponse(
|
|
905
|
+
agentId, clientAddress, feedbackIndex, response
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
def getReputationSummary(
|
|
909
|
+
self,
|
|
910
|
+
agentId: "AgentId",
|
|
911
|
+
) -> Dict[str, Any]:
|
|
912
|
+
"""Get reputation summary for an agent."""
|
|
913
|
+
return self.feedback_manager.getReputationSummary(
|
|
914
|
+
agentId
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
def transferAgent(
|
|
918
|
+
self,
|
|
919
|
+
agentId: "AgentId",
|
|
920
|
+
newOwnerAddress: str,
|
|
921
|
+
) -> "TransactionHandle[Dict[str, Any]]":
|
|
922
|
+
"""Transfer agent ownership to a new address.
|
|
923
|
+
|
|
924
|
+
Convenience method that loads the agent and calls transfer().
|
|
925
|
+
|
|
926
|
+
Args:
|
|
927
|
+
agentId: The agent ID to transfer
|
|
928
|
+
newOwnerAddress: Ethereum address of the new owner
|
|
929
|
+
|
|
930
|
+
Returns:
|
|
931
|
+
Transaction receipt
|
|
932
|
+
|
|
933
|
+
Raises:
|
|
934
|
+
ValueError: If agent not found or transfer not allowed
|
|
935
|
+
"""
|
|
936
|
+
# Load the agent
|
|
937
|
+
agent = self.loadAgent(agentId)
|
|
938
|
+
|
|
939
|
+
# Call the transfer method
|
|
940
|
+
return agent.transfer(newOwnerAddress)
|
|
941
|
+
|
|
942
|
+
# Utility methods for owner operations
|
|
943
|
+
def getAgentOwner(self, agentId: AgentId) -> str:
|
|
944
|
+
"""Get the current owner of an agent.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
agentId: The agent ID to check (can be "chainId:tokenId" or just tokenId)
|
|
948
|
+
|
|
949
|
+
Returns:
|
|
950
|
+
The current owner's Ethereum address
|
|
951
|
+
|
|
952
|
+
Raises:
|
|
953
|
+
ValueError: If agent ID is invalid or agent doesn't exist
|
|
954
|
+
"""
|
|
955
|
+
try:
|
|
956
|
+
# Parse agentId to extract tokenId
|
|
957
|
+
if ":" in str(agentId):
|
|
958
|
+
tokenId = int(str(agentId).split(":")[-1])
|
|
959
|
+
else:
|
|
960
|
+
tokenId = int(agentId)
|
|
961
|
+
|
|
962
|
+
owner = self.web3_client.call_contract(
|
|
963
|
+
self.identity_registry,
|
|
964
|
+
"ownerOf",
|
|
965
|
+
tokenId
|
|
966
|
+
)
|
|
967
|
+
return owner
|
|
968
|
+
except Exception as e:
|
|
969
|
+
raise ValueError(f"Failed to get owner for agent {agentId}: {e}")
|
|
970
|
+
|
|
971
|
+
def isAgentOwner(self, agentId: AgentId, address: Optional[str] = None) -> bool:
|
|
972
|
+
"""Check if an address is the owner of an agent.
|
|
973
|
+
|
|
974
|
+
Args:
|
|
975
|
+
agentId: The agent ID to check
|
|
976
|
+
address: Address to check (defaults to SDK's signer address)
|
|
977
|
+
|
|
978
|
+
Returns:
|
|
979
|
+
True if the address is the owner, False otherwise
|
|
980
|
+
|
|
981
|
+
Raises:
|
|
982
|
+
ValueError: If agent ID is invalid or agent doesn't exist
|
|
983
|
+
"""
|
|
984
|
+
if address is None:
|
|
985
|
+
if not self.signer:
|
|
986
|
+
raise ValueError("No signer available and no address provided")
|
|
987
|
+
address = self.web3_client.account.address
|
|
988
|
+
|
|
989
|
+
try:
|
|
990
|
+
owner = self.getAgentOwner(agentId)
|
|
991
|
+
return owner.lower() == address.lower()
|
|
992
|
+
except ValueError:
|
|
993
|
+
return False
|
|
994
|
+
|
|
995
|
+
def canTransferAgent(self, agentId: AgentId, address: Optional[str] = None) -> bool:
|
|
996
|
+
"""Check if an address can transfer an agent (i.e., is the owner).
|
|
997
|
+
|
|
998
|
+
Args:
|
|
999
|
+
agentId: The agent ID to check
|
|
1000
|
+
address: Address to check (defaults to SDK's signer address)
|
|
1001
|
+
|
|
1002
|
+
Returns:
|
|
1003
|
+
True if the address can transfer the agent, False otherwise
|
|
1004
|
+
"""
|
|
1005
|
+
return self.isAgentOwner(agentId, address)
|