agent0-sdk 1.4.2__py3-none-any.whl → 1.5.1b1__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,6 @@ class AgentSummary:
165
165
  description: str
166
166
  owners: List[Address]
167
167
  operators: List[Address]
168
- mcp: bool
169
- a2a: bool
170
168
  ens: Optional[str]
171
169
  did: Optional[str]
172
170
  walletAddress: Optional[Address]
@@ -176,7 +174,22 @@ class AgentSummary:
176
174
  mcpPrompts: List[str]
177
175
  mcpResources: List[str]
178
176
  active: bool
177
+ # Endpoint strings (new unified search + Jan 2026 schema)
178
+ mcp: Optional[str] = None
179
+ a2a: Optional[str] = None
180
+ web: Optional[str] = None
181
+ email: Optional[str] = None
182
+ oasfSkills: List[str] = field(default_factory=list)
183
+ oasfDomains: List[str] = field(default_factory=list)
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,98 @@ 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
+ semanticMinScore: Optional[float] = None
362
+ semanticTopK: Optional[int] = None
363
+
364
+
286
365
  @dataclass
287
366
  class SearchFeedbackParams:
288
367
  """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,12 +460,10 @@ 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
- ) -> Dict[str, Any]:
466
+ ) -> List[AgentSummary]:
469
467
  """Search for agents.
470
468
 
471
469
  Examples:
@@ -475,313 +473,31 @@ class SDK:
475
473
 
476
474
  # Explicit SearchParams (for complex queries or IDE autocomplete)
477
475
  sdk.searchAgents(SearchParams(name="Test", mcpTools=["code_generation"]))
478
-
479
- # With pagination
480
- sdk.searchAgents(name="Test", page_size=10)
481
476
  """
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)
477
+ # Allow kwargs to populate filters for better DX.
478
+ if kwargs and filters is None:
479
+ if isinstance(kwargs.get("feedback"), dict):
480
+ kwargs["feedback"] = FeedbackFilters(**kwargs["feedback"])
481
+ filters = SearchFilters(**kwargs)
482
+ elif filters is None:
483
+ filters = SearchFilters()
484
+ elif isinstance(filters, dict):
485
+ if isinstance(filters.get("feedback"), dict):
486
+ filters["feedback"] = FeedbackFilters(**filters["feedback"])
487
+ filters = SearchFilters(**filters)
488
+
489
+ if options is None:
490
+ options = SearchOptions()
491
+ elif isinstance(options, dict):
492
+ options = SearchOptions(**options)
493
+
494
+ # Do not force a default sort here; the indexer chooses keyword-aware defaults.
495
+ out = self.indexer.search_agents(filters, options)
496
+ if isinstance(out, dict):
497
+ return out.get("items") or []
498
+ return out or []
496
499
 
497
500
  # 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
501
 
786
502
  # Feedback methods - delegate to feedback_manager
787
503
  def prepareFeedbackFile(self, input: Dict[str, Any]) -> Dict[str, Any]:
@@ -838,8 +554,6 @@ class SDK:
838
554
  minValue: Optional[float] = None,
839
555
  maxValue: Optional[float] = None,
840
556
  include_revoked: bool = False,
841
- first: int = 100,
842
- skip: int = 0,
843
557
  agents: Optional[List["AgentId"]] = None,
844
558
  ) -> List["Feedback"]:
845
559
  """Search feedback.
@@ -881,8 +595,6 @@ class SDK:
881
595
  minValue=minValue,
882
596
  maxValue=maxValue,
883
597
  include_revoked=include_revoked,
884
- first=first,
885
- skip=skip,
886
598
  )
887
599
 
888
600
  def revokeFeedback(
@@ -0,0 +1,70 @@
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 = 20.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
+ # SDK defaults (applied here so developers don't need to pass them).
34
+ # - minScore: 0.5
35
+ # - topK: 5000 (API expects "limit" not "topK" in v1)
36
+ if min_score is None:
37
+ min_score = 0.5
38
+ if top_k is None:
39
+ top_k = 5000
40
+
41
+ body = {"query": query.strip(), "minScore": min_score, "limit": top_k}
42
+
43
+ resp = requests.post(
44
+ f"{self.base_url}/api/v1/search",
45
+ json=body,
46
+ headers={"Content-Type": "application/json"},
47
+ timeout=self.timeout_seconds,
48
+ )
49
+ resp.raise_for_status()
50
+ data = resp.json()
51
+
52
+ results = data.get("results") if isinstance(data, dict) else data
53
+ if not isinstance(results, list):
54
+ return []
55
+
56
+ out: List[SemanticSearchResult] = []
57
+ for r in results:
58
+ if not isinstance(r, dict):
59
+ continue
60
+ try:
61
+ chain_id = int(r.get("chainId"))
62
+ agent_id = str(r.get("agentId"))
63
+ score = float(r.get("score"))
64
+ except Exception:
65
+ continue
66
+ if ":" not in agent_id:
67
+ continue
68
+ out.append(SemanticSearchResult(chainId=chain_id, agentId=agent_id, score=score))
69
+ return out
70
+