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/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)