agent0-sdk 1.4.2__py3-none-any.whl → 1.5.0b1__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/models.py CHANGED
@@ -165,8 +165,11 @@ class AgentSummary:
165
165
  description: str
166
166
  owners: List[Address]
167
167
  operators: List[Address]
168
- mcp: bool
169
- a2a: bool
168
+ # Endpoint strings (new unified search + Jan 2026 schema)
169
+ mcp: Optional[str] = None
170
+ a2a: Optional[str] = None
171
+ web: Optional[str] = None
172
+ email: Optional[str] = None
170
173
  ens: Optional[str]
171
174
  did: Optional[str]
172
175
  walletAddress: Optional[Address]
@@ -175,8 +178,18 @@ class AgentSummary:
175
178
  mcpTools: List[str]
176
179
  mcpPrompts: List[str]
177
180
  mcpResources: List[str]
181
+ oasfSkills: List[str] = field(default_factory=list)
182
+ oasfDomains: List[str] = field(default_factory=list)
178
183
  active: bool
179
184
  x402support: bool = False
185
+ createdAt: Optional[int] = None
186
+ updatedAt: Optional[int] = None
187
+ lastActivity: Optional[int] = None
188
+ agentURI: Optional[str] = None
189
+ agentURIType: Optional[str] = None
190
+ feedbackCount: Optional[int] = None
191
+ averageValue: Optional[float] = None
192
+ semanticScore: Optional[float] = None
180
193
  extras: Dict[str, Any] = field(default_factory=dict)
181
194
 
182
195
 
@@ -257,32 +270,100 @@ class Feedback:
257
270
 
258
271
 
259
272
  @dataclass
260
- class SearchParams:
261
- """Parameters for agent search."""
273
+ class FeedbackFilters:
274
+ hasFeedback: Optional[bool] = None
275
+ hasNoFeedback: Optional[bool] = None
276
+ includeRevoked: Optional[bool] = None
277
+ minValue: Optional[float] = None
278
+ maxValue: Optional[float] = None
279
+ minCount: Optional[int] = None
280
+ maxCount: Optional[int] = None
281
+ fromReviewers: Optional[List[Address]] = None
282
+ endpoint: Optional[str] = None
283
+ hasResponse: Optional[bool] = None
284
+ tag1: Optional[str] = None
285
+ tag2: Optional[str] = None
286
+ tag: Optional[str] = None
287
+
288
+
289
+ DateLike = Union[datetime, str, int]
290
+
291
+
292
+ @dataclass
293
+ class SearchFilters:
294
+ # Chain / identity
262
295
  chains: Optional[Union[List[ChainId], Literal["all"]]] = None
263
- name: Optional[str] = None # case-insensitive substring
264
- description: Optional[str] = None # semantic; vector distance < threshold
296
+ agentIds: Optional[List[AgentId]] = None
297
+
298
+ # Text
299
+ name: Optional[str] = None
300
+ description: Optional[str] = None
301
+
302
+ # Owners / operators
265
303
  owners: Optional[List[Address]] = None
266
304
  operators: Optional[List[Address]] = None
267
- mcp: Optional[bool] = None
268
- a2a: Optional[bool] = None
269
- ens: Optional[str] = None # exact, case-insensitive
270
- did: Optional[str] = None # exact
305
+
306
+ # Endpoint existence
307
+ hasRegistrationFile: Optional[bool] = None
308
+ hasWeb: Optional[bool] = None
309
+ hasMCP: Optional[bool] = None
310
+ hasA2A: Optional[bool] = None
311
+ hasOASF: Optional[bool] = None
312
+ hasEndpoints: Optional[bool] = None
313
+
314
+ # Endpoint substring contains
315
+ webContains: Optional[str] = None
316
+ mcpContains: Optional[str] = None
317
+ a2aContains: Optional[str] = None
318
+ ensContains: Optional[str] = None
319
+ didContains: Optional[str] = None
320
+
321
+ # Wallet
271
322
  walletAddress: Optional[Address] = None
323
+
324
+ # Capability arrays (ANY semantics)
272
325
  supportedTrust: Optional[List[str]] = None
273
326
  a2aSkills: Optional[List[str]] = None
274
327
  mcpTools: Optional[List[str]] = None
275
328
  mcpPrompts: Optional[List[str]] = None
276
329
  mcpResources: Optional[List[str]] = None
277
- active: Optional[bool] = True
330
+ oasfSkills: Optional[List[str]] = None
331
+ oasfDomains: Optional[List[str]] = None
332
+
333
+ # Status
334
+ active: Optional[bool] = None
278
335
  x402support: Optional[bool] = None
279
- deduplicate_cross_chain: bool = False # Deduplicate same agent across chains
336
+
337
+ # Time filters
338
+ registeredAtFrom: Optional[DateLike] = None
339
+ registeredAtTo: Optional[DateLike] = None
340
+ updatedAtFrom: Optional[DateLike] = None
341
+ updatedAtTo: Optional[DateLike] = None
342
+
343
+ # Metadata filters (two-phase)
344
+ hasMetadataKey: Optional[str] = None
345
+ metadataValue: Optional[Dict[str, str]] = None # { key, value }
346
+
347
+ # Semantic search
348
+ keyword: Optional[str] = None
349
+
350
+ # Feedback filters (two-phase)
351
+ feedback: Optional[FeedbackFilters] = None
280
352
 
281
353
  def to_dict(self) -> Dict[str, Any]:
282
354
  """Convert to dictionary, filtering out None values."""
283
355
  return {k: v for k, v in self.__dict__.items() if v is not None}
284
356
 
285
357
 
358
+ @dataclass
359
+ class SearchOptions:
360
+ sort: Optional[List[str]] = None
361
+ pageSize: Optional[int] = None
362
+ cursor: Optional[str] = None
363
+ semanticMinScore: Optional[float] = None
364
+ semanticTopK: Optional[int] = None
365
+
366
+
286
367
  @dataclass
287
368
  class SearchFeedbackParams:
288
369
  """Parameters for feedback search."""
agent0_sdk/core/sdk.py CHANGED
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
16
16
  from .models import (
17
17
  AgentId, ChainId, Address, URI, Timestamp, IdemKey,
18
18
  EndpointType, TrustModel, Endpoint, RegistrationFile,
19
- AgentSummary, Feedback, SearchParams
19
+ AgentSummary, Feedback, SearchFilters, SearchOptions, FeedbackFilters
20
20
  )
21
21
  from .web3_client import Web3Client
22
22
  from .contracts import (
@@ -460,10 +460,8 @@ class SDK:
460
460
 
461
461
  def searchAgents(
462
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,
463
+ filters: Union[SearchFilters, Dict[str, Any], None] = None,
464
+ options: Union[SearchOptions, Dict[str, Any], None] = None,
467
465
  **kwargs # Accept search criteria as kwargs for better DX
468
466
  ) -> Dict[str, Any]:
469
467
  """Search for agents.
@@ -479,309 +477,32 @@ class SDK:
479
477
  # With pagination
480
478
  sdk.searchAgents(name="Test", page_size=10)
481
479
  """
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)
480
+ # Allow kwargs to populate filters for better DX.
481
+ if kwargs and filters is None:
482
+ if isinstance(kwargs.get("feedback"), dict):
483
+ kwargs["feedback"] = FeedbackFilters(**kwargs["feedback"])
484
+ filters = SearchFilters(**kwargs)
485
+ elif filters is None:
486
+ filters = SearchFilters()
487
+ elif isinstance(filters, dict):
488
+ if isinstance(filters.get("feedback"), dict):
489
+ filters["feedback"] = FeedbackFilters(**filters["feedback"])
490
+ filters = SearchFilters(**filters)
491
+
492
+ if options is None:
493
+ options = SearchOptions()
494
+ elif isinstance(options, dict):
495
+ options = SearchOptions(**options)
496
+
497
+ if options.sort is None:
498
+ options.sort = ["updatedAt:desc"]
499
+
500
+ if options.pageSize is None:
501
+ options.pageSize = 50
502
+
503
+ return self.indexer.search_agents(filters, options)
496
504
 
497
505
  # 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
506
 
786
507
  # Feedback methods - delegate to feedback_manager
787
508
  def prepareFeedbackFile(self, input: Dict[str, Any]) -> Dict[str, Any]:
@@ -0,0 +1,66 @@
1
+ """
2
+ Semantic search client (external endpoint).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import List, Optional
9
+
10
+ import requests
11
+
12
+
13
+ @dataclass
14
+ class SemanticSearchResult:
15
+ chainId: int
16
+ agentId: str
17
+ score: float
18
+
19
+
20
+ class SemanticSearchClient:
21
+ def __init__(
22
+ self,
23
+ base_url: str = "https://semantic-search.ag0.xyz",
24
+ timeout_seconds: float = 10.0,
25
+ ):
26
+ self.base_url = base_url.rstrip("/")
27
+ self.timeout_seconds = timeout_seconds
28
+
29
+ def search(self, query: str, *, min_score: Optional[float] = None, top_k: Optional[int] = None) -> List[SemanticSearchResult]:
30
+ if not query or not query.strip():
31
+ return []
32
+
33
+ body = {"query": query.strip()}
34
+ if min_score is not None:
35
+ body["minScore"] = min_score
36
+ if top_k is not None:
37
+ body["topK"] = top_k
38
+
39
+ resp = requests.post(
40
+ f"{self.base_url}/api/v1/search",
41
+ json=body,
42
+ headers={"Content-Type": "application/json"},
43
+ timeout=self.timeout_seconds,
44
+ )
45
+ resp.raise_for_status()
46
+ data = resp.json()
47
+
48
+ results = data.get("results") if isinstance(data, dict) else data
49
+ if not isinstance(results, list):
50
+ return []
51
+
52
+ out: List[SemanticSearchResult] = []
53
+ for r in results:
54
+ if not isinstance(r, dict):
55
+ continue
56
+ try:
57
+ chain_id = int(r.get("chainId"))
58
+ agent_id = str(r.get("agentId"))
59
+ score = float(r.get("score"))
60
+ except Exception:
61
+ continue
62
+ if ":" not in agent_id:
63
+ continue
64
+ out.append(SemanticSearchResult(chainId=chain_id, agentId=agent_id, score=score))
65
+ return out
66
+