agent0-sdk 0.2.2__py3-none-any.whl → 0.3rc1__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
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
  import json
8
8
  from dataclasses import dataclass, field
9
9
  from enum import Enum
10
- from typing import Any, Dict, List, Optional, Union
10
+ from typing import Any, Dict, List, Optional, Union, Literal
11
11
  from datetime import datetime
12
12
 
13
13
 
@@ -269,7 +269,7 @@ class Feedback:
269
269
  @dataclass
270
270
  class SearchParams:
271
271
  """Parameters for agent search."""
272
- chains: Optional[List[ChainId]] = None
272
+ chains: Optional[Union[List[ChainId], Literal["all"]]] = None
273
273
  name: Optional[str] = None # case-insensitive substring
274
274
  description: Optional[str] = None # semantic; vector distance < threshold
275
275
  owners: Optional[List[Address]] = None
@@ -286,6 +286,7 @@ class SearchParams:
286
286
  mcpResources: Optional[List[str]] = None
287
287
  active: Optional[bool] = True
288
288
  x402support: Optional[bool] = None
289
+ deduplicate_cross_chain: bool = False # Deduplicate same agent across chains
289
290
 
290
291
  def to_dict(self) -> Dict[str, Any]:
291
292
  """Convert to dictionary, filtering out None values."""
agent0_sdk/core/sdk.py CHANGED
@@ -6,10 +6,13 @@ from __future__ import annotations
6
6
 
7
7
  import asyncio
8
8
  import json
9
+ import logging
9
10
  import time
10
- from typing import Any, Dict, List, Optional, Union
11
+ from typing import Any, Dict, List, Optional, Union, Literal
11
12
  from datetime import datetime
12
13
 
14
+ logger = logging.getLogger(__name__)
15
+
13
16
  from .models import (
14
17
  AgentId, ChainId, Address, URI, Timestamp, IdemKey,
15
18
  EndpointType, TrustModel, Endpoint, RegistrationFile,
@@ -102,7 +105,8 @@ class SDK:
102
105
  web3_client=self.web3_client,
103
106
  store=indexingStore,
104
107
  embeddings=embeddings,
105
- subgraph_client=self.subgraph_client
108
+ subgraph_client=self.subgraph_client,
109
+ subgraph_url_overrides=self._subgraph_urls
106
110
  )
107
111
 
108
112
  # Initialize IPFS client based on configuration
@@ -438,7 +442,7 @@ class SDK:
438
442
  def searchAgents(
439
443
  self,
440
444
  params: Union[SearchParams, Dict[str, Any], None] = None,
441
- sort: List[str] = None,
445
+ sort: Union[str, List[str], None] = None,
442
446
  page_size: int = 50,
443
447
  cursor: Optional[str] = None,
444
448
  **kwargs # Accept search criteria as kwargs for better DX
@@ -466,7 +470,9 @@ class SDK:
466
470
 
467
471
  if sort is None:
468
472
  sort = ["updatedAt:desc"]
469
-
473
+ elif isinstance(sort, str):
474
+ sort = [sort]
475
+
470
476
  return self.indexer.search_agents(params, sort, page_size, cursor)
471
477
 
472
478
  # Feedback methods
@@ -588,8 +594,26 @@ class SDK:
588
594
  page_size: int = 50,
589
595
  cursor: Optional[str] = None,
590
596
  sort: Optional[List[str]] = None,
597
+ chains: Optional[Union[List[ChainId], Literal["all"]]] = None,
591
598
  ) -> Dict[str, Any]:
592
599
  """Search agents filtered by reputation criteria."""
600
+ # Handle multi-chain search
601
+ if chains:
602
+ # Expand "all" if needed
603
+ if chains == "all":
604
+ chains = self.indexer._get_all_configured_chains()
605
+
606
+ # If multiple chains or single chain different from default
607
+ if isinstance(chains, list) and len(chains) > 0:
608
+ if len(chains) > 1 or (len(chains) == 1 and chains[0] != self.chainId):
609
+ return asyncio.run(
610
+ self._search_agents_by_reputation_across_chains(
611
+ agents, tags, reviewers, capabilities, skills, tasks, names,
612
+ minAverageScore, includeRevoked, page_size, cursor, sort, chains
613
+ )
614
+ )
615
+
616
+ # Single chain search (existing behavior)
593
617
  if not self.subgraph_client:
594
618
  raise ValueError("Subgraph client required for searchAgentsByReputation")
595
619
 
@@ -664,6 +688,185 @@ class SDK:
664
688
  except Exception as e:
665
689
  raise ValueError(f"Failed to search agents by reputation: {e}")
666
690
 
691
+ async def _search_agents_by_reputation_across_chains(
692
+ self,
693
+ agents: Optional[List[AgentId]],
694
+ tags: Optional[List[str]],
695
+ reviewers: Optional[List[Address]],
696
+ capabilities: Optional[List[str]],
697
+ skills: Optional[List[str]],
698
+ tasks: Optional[List[str]],
699
+ names: Optional[List[str]],
700
+ minAverageScore: Optional[int],
701
+ includeRevoked: bool,
702
+ page_size: int,
703
+ cursor: Optional[str],
704
+ sort: Optional[List[str]],
705
+ chains: List[ChainId],
706
+ ) -> Dict[str, Any]:
707
+ """
708
+ Search agents by reputation across multiple chains in parallel.
709
+
710
+ Similar to indexer._search_agents_across_chains() but for reputation-based search.
711
+ """
712
+ import time
713
+ start_time = time.time()
714
+
715
+ if sort is None:
716
+ sort = ["createdAt:desc"]
717
+
718
+ order_by = "createdAt"
719
+ order_direction = "desc"
720
+ if sort and len(sort) > 0:
721
+ sort_field = sort[0].split(":")
722
+ order_by = sort_field[0] if len(sort_field) >= 1 else order_by
723
+ order_direction = sort_field[1] if len(sort_field) >= 2 else order_direction
724
+
725
+ skip = 0
726
+ if cursor:
727
+ try:
728
+ skip = int(cursor)
729
+ except ValueError:
730
+ skip = 0
731
+
732
+ # Define async function for querying a single chain
733
+ async def query_single_chain(chain_id: int) -> Dict[str, Any]:
734
+ """Query one chain and return its results with metadata."""
735
+ try:
736
+ # Get subgraph client for this chain
737
+ subgraph_client = self.indexer._get_subgraph_client_for_chain(chain_id)
738
+
739
+ if subgraph_client is None:
740
+ logger.warning(f"No subgraph client available for chain {chain_id}")
741
+ return {
742
+ "chainId": chain_id,
743
+ "status": "unavailable",
744
+ "agents": [],
745
+ "error": f"No subgraph configured for chain {chain_id}"
746
+ }
747
+
748
+ # Execute reputation search query
749
+ try:
750
+ agents_data = subgraph_client.search_agents_by_reputation(
751
+ agents=agents,
752
+ tags=tags,
753
+ reviewers=reviewers,
754
+ capabilities=capabilities,
755
+ skills=skills,
756
+ tasks=tasks,
757
+ names=names,
758
+ minAverageScore=minAverageScore,
759
+ includeRevoked=includeRevoked,
760
+ first=page_size * 3, # Fetch extra to allow for filtering/sorting
761
+ skip=0, # We'll handle pagination after aggregation
762
+ order_by=order_by,
763
+ order_direction=order_direction
764
+ )
765
+
766
+ logger.info(f"Chain {chain_id}: fetched {len(agents_data)} agents by reputation")
767
+ except Exception as e:
768
+ logger.error(f"Error in search_agents_by_reputation for chain {chain_id}: {e}", exc_info=True)
769
+ agents_data = []
770
+
771
+ return {
772
+ "chainId": chain_id,
773
+ "status": "success",
774
+ "agents": agents_data,
775
+ "count": len(agents_data),
776
+ }
777
+
778
+ except Exception as e:
779
+ logger.error(f"Error querying chain {chain_id} for reputation search: {e}", exc_info=True)
780
+ return {
781
+ "chainId": chain_id,
782
+ "status": "error",
783
+ "agents": [],
784
+ "error": str(e),
785
+ "count": 0
786
+ }
787
+
788
+ # Execute queries in parallel
789
+ chain_tasks = [query_single_chain(chain_id) for chain_id in chains]
790
+ chain_results = await asyncio.gather(*chain_tasks)
791
+
792
+ # Aggregate results from all chains
793
+ all_agents = []
794
+ successful_chains = []
795
+ failed_chains = []
796
+
797
+ for result in chain_results:
798
+ chain_id = result["chainId"]
799
+ if result["status"] == "success":
800
+ successful_chains.append(chain_id)
801
+ agents_count = len(result.get("agents", []))
802
+ logger.debug(f"Chain {chain_id}: aggregating {agents_count} agents")
803
+ all_agents.extend(result["agents"])
804
+ else:
805
+ failed_chains.append(chain_id)
806
+ logger.warning(f"Chain {chain_id}: status={result.get('status')}, error={result.get('error', 'N/A')}")
807
+
808
+ logger.debug(f"Total agents aggregated: {len(all_agents)} from {len(successful_chains)} chains")
809
+
810
+ # Transform to AgentSummary objects
811
+ from .models import AgentSummary
812
+ results = []
813
+ for agent_data in all_agents:
814
+ reg_file = agent_data.get('registrationFile') or {}
815
+ if not isinstance(reg_file, dict):
816
+ reg_file = {}
817
+
818
+ agent_summary = AgentSummary(
819
+ chainId=int(agent_data.get('chainId', 0)),
820
+ agentId=agent_data.get('id'),
821
+ name=reg_file.get('name', f"Agent {agent_data.get('id')}"),
822
+ image=reg_file.get('image'),
823
+ description=reg_file.get('description', ''),
824
+ owners=[agent_data.get('owner', '')],
825
+ operators=agent_data.get('operators', []),
826
+ mcp=reg_file.get('mcpEndpoint') is not None,
827
+ a2a=reg_file.get('a2aEndpoint') is not None,
828
+ ens=reg_file.get('ens'),
829
+ did=reg_file.get('did'),
830
+ walletAddress=reg_file.get('agentWallet'),
831
+ supportedTrusts=reg_file.get('supportedTrusts', []),
832
+ a2aSkills=reg_file.get('a2aSkills', []),
833
+ mcpTools=reg_file.get('mcpTools', []),
834
+ mcpPrompts=reg_file.get('mcpPrompts', []),
835
+ mcpResources=reg_file.get('mcpResources', []),
836
+ active=reg_file.get('active', True),
837
+ x402support=reg_file.get('x402support', False),
838
+ extras={'averageScore': agent_data.get('averageScore')}
839
+ )
840
+ results.append(agent_summary)
841
+
842
+ # Sort by averageScore (descending) if available, otherwise by createdAt
843
+ results.sort(
844
+ key=lambda x: (
845
+ x.extras.get('averageScore') if x.extras.get('averageScore') is not None else 0,
846
+ x.chainId,
847
+ x.agentId
848
+ ),
849
+ reverse=True
850
+ )
851
+
852
+ # Apply pagination
853
+ paginated_results = results[skip:skip + page_size]
854
+ next_cursor = str(skip + len(paginated_results)) if len(paginated_results) == page_size and skip + len(paginated_results) < len(results) else None
855
+
856
+ elapsed_ms = int((time.time() - start_time) * 1000)
857
+
858
+ return {
859
+ "items": paginated_results,
860
+ "nextCursor": next_cursor,
861
+ "meta": {
862
+ "chains": chains,
863
+ "successfulChains": successful_chains,
864
+ "failedChains": failed_chains,
865
+ "totalResults": len(results),
866
+ "timing": {"totalMs": elapsed_ms}
867
+ }
868
+ }
869
+
667
870
  # Feedback methods - delegate to feedback_manager
668
871
  def signFeedbackAuth(
669
872
  self,
@@ -38,7 +38,7 @@ class SubgraphClient:
38
38
  'variables': variables or {}
39
39
  },
40
40
  headers={'Content-Type': 'application/json'},
41
- timeout=30
41
+ timeout=10
42
42
  )
43
43
  response.raise_for_status()
44
44
  result = response.json()
@@ -716,12 +716,14 @@ class SubgraphClient:
716
716
  return []
717
717
  else:
718
718
  # No feedback filters - query agents directly
719
- agent_filters = []
719
+ # For reputation search, we want agents that have feedback
720
+ # Filter by totalFeedback > 0 to only get agents with feedback
721
+ agent_filters = ['totalFeedback_gt: 0'] # Only agents with feedback (BigInt comparison)
720
722
  if agents is not None and len(agents) > 0:
721
723
  agent_ids = [f'"{aid}"' for aid in agents]
722
724
  agent_filters.append(f'id_in: [{", ".join(agent_ids)}]')
723
725
 
724
- agent_where = f"where: {{ {', '.join(agent_filters)} }}" if agent_filters else ""
726
+ agent_where = f"where: {{ {', '.join(agent_filters)} }}"
725
727
 
726
728
  # Build feedback where for agent query (to calculate scores)
727
729
  feedback_where_for_agents = f"{{ {', '.join(feedback_filters)} }}" if feedback_filters else "{}"
@@ -784,6 +786,12 @@ class SubgraphClient:
784
786
 
785
787
  try:
786
788
  result = self.query(query)
789
+
790
+ # Check for GraphQL errors
791
+ if 'errors' in result:
792
+ logger.error(f"GraphQL errors in search_agents_by_reputation: {result['errors']}")
793
+ return []
794
+
787
795
  agents_result = result.get('agents', [])
788
796
 
789
797
  # Calculate average scores
@@ -806,6 +814,18 @@ class SubgraphClient:
806
814
  if agent.get('averageScore') is not None and agent['averageScore'] >= minAverageScore
807
815
  ]
808
816
 
817
+ # For reputation search, filter logic:
818
+ # - If specific agents were requested, return them even if averageScore is None
819
+ # (the user explicitly asked for these agents, so return them)
820
+ # - If general search (no specific agents), only return agents with reputation data
821
+ if agents is None or len(agents) == 0:
822
+ # General search - only return agents with reputation
823
+ agents_result = [
824
+ agent for agent in agents_result
825
+ if agent.get('averageScore') is not None
826
+ ]
827
+ # else: specific agents requested - return all requested agents (even if averageScore is None)
828
+
809
829
  return agents_result
810
830
 
811
831
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent0-sdk
3
- Version: 0.2.2
3
+ Version: 0.3rc1
4
4
  Summary: Python SDK for agent portability, discovery and trust based on ERC-8004
5
5
  Author-email: Marco De Rossi <marco.derossi@consensys.net>
6
6
  License: MIT License
@@ -274,7 +274,6 @@ agent.register("https://example.com/agent-registration.json")
274
274
 
275
275
  - More chains (currently Ethereum Sepolia only)
276
276
  - Support for validations
277
- - Multi-chain agents search
278
277
  - Enhanced x402 payments
279
278
  - Semantic/Vectorial search
280
279
  - Advanced reputation aggregation
@@ -1,27 +1,29 @@
1
- agent0_sdk/__init__.py,sha256=SOJFcN81--KPhBSpHV0T85Q7Z1qZxxjLedZDCSbfkm0,919
1
+ agent0_sdk/__init__.py,sha256=XrY2VSPzM6axhVSb1yTY-oYhJBbTTNJP5mKSFeMRPvk,920
2
2
  agent0_sdk/core/agent.py,sha256=RQND8F3Hmmpnt-PqCXBP7T8KfrjTXo0_X2nZoMJh01w,33343
3
- agent0_sdk/core/contracts.py,sha256=mER1pSae4-fiGBwaVTi7oqJ_QYaDnrtyIF7g6dkdE-0,19889
3
+ agent0_sdk/core/contracts.py,sha256=LJY07PIyAb3ZSH_m2xD4OdXawsXKxaTUGhYHwGWUpmI,20435
4
4
  agent0_sdk/core/endpoint_crawler.py,sha256=QBkFc3tBSQqHj6PtSTZ5D3_HVB00KJZJdxE3uYpI9po,13611
5
- agent0_sdk/core/feedback_manager.py,sha256=wkV-P5wgDwOLwhaf22qOl-3S2F_PNI7oCHBBZMAxugI,36017
6
- agent0_sdk/core/indexer.py,sha256=bl5TqTSytt_qQyGqUsqZvrnXFlmQEuOgm6uKEE-u-BQ,41161
5
+ agent0_sdk/core/feedback_manager.py,sha256=MV3srP-rgFaDXXELeLTJBj2aDUPdENy0tzWUsJUqrL4,39860
6
+ agent0_sdk/core/indexer.py,sha256=erkDKrtyvAtfrblz0YHekPB2Hpw7tJm-1KJjafTMaHc,68470
7
7
  agent0_sdk/core/ipfs_client.py,sha256=fml1ai1BdBkgb95xAkf-ft8QsahV1HL30hBYRz7rQwI,13929
8
- agent0_sdk/core/models.py,sha256=bQARVmKETiVL5uJpHxXGN2wY2GFrdzxhPHqjzL8TBlQ,12373
9
- agent0_sdk/core/sdk.py,sha256=stOurnsSoedUFKaHKhrjaM1sghBzjighizV6AaZD7-U,31560
10
- agent0_sdk/core/subgraph_client.py,sha256=XXFVFAoHcgEfqxc2w2OksSp8vtbKMtsNIuNbNcNQOzE,27280
8
+ agent0_sdk/core/models.py,sha256=5uXZmAtkLa5wKfnwU0-yuGkpmz00H_83Ta8zq2K7B-8,12487
9
+ agent0_sdk/core/sdk.py,sha256=CQUpKaP7g1Lu7aokRYs-QChv78XIdkfz6Yax6nhaWqQ,40166
10
+ agent0_sdk/core/subgraph_client.py,sha256=Iw-YEtT1-Rm9f-kY4EXZlCuwEkJQAq5-bHDpxrMVyGg,28419
11
11
  agent0_sdk/core/web3_client.py,sha256=859ntu5dAmNlcJ3YM1w_VV2gI3mpCC9QEr-GN1236zU,6850
12
- agent0_sdk-0.2.2.dist-info/licenses/LICENSE,sha256=rhZZbZm_Ovz4Oa9LNQ-ms8a1tA36wWh90ZkC0OR7WMw,1072
12
+ agent0_sdk-0.3rc1.dist-info/licenses/LICENSE,sha256=rhZZbZm_Ovz4Oa9LNQ-ms8a1tA36wWh90ZkC0OR7WMw,1072
13
13
  tests/__init__.py,sha256=60ffheccPhuMCtwiiKP1X-CJJXKpxJ_Ywa0aXGHR9bY,23
14
14
  tests/config.py,sha256=1uePvkLBNubOQsvYkQSno0m007PMD1VACgm33fCYY6s,1429
15
15
  tests/conftest.py,sha256=P-HCtVVYwSvscuaJqhrgZcv39XXNnr932ekEamzIqis,589
16
+ tests/discover_test_data.py,sha256=Fu0uQKnFk8m7qEqEp293BWbo_mT1CK5szVZcdcdlJQw,17678
16
17
  tests/test_feedback.py,sha256=7lszWYSmseJE0I4BhKzZdBiIzf2bgpPqZTZvhRrCTjY,14638
17
18
  tests/test_models.py,sha256=kCZdoPasIIcOjEw7ToPldqARdbGVK8v8byOhFwVo7OI,7115
19
+ tests/test_multi_chain.py,sha256=6C-HD037Lsjd7_H0pwp7NDU04jLTjhardT1LHBjahFM,26711
18
20
  tests/test_real_public_servers.py,sha256=pCo4aLSCG9qv4D6T7jbyVmP1gt3r1jWxdes6z5XSNhU,3433
19
21
  tests/test_registration.py,sha256=pYanDPLAFETIfabBUvO34ZDmyD0Rbcv8vecSfgSrQ70,9542
20
22
  tests/test_registrationIpfs.py,sha256=9O3IBiN2CVMKzB19bqb-jN-nhqsN22kQINMpe9THqiI,8400
21
23
  tests/test_sdk.py,sha256=dALLFm_A6aXx0ec-CNOLGQoadaSPZ08EEeCS6Tgnm0M,9362
22
24
  tests/test_search.py,sha256=SiUio8H-M6Za8OXD_h9tUZdln0ayzkPJ3doTrkHv-Fs,18382
23
25
  tests/test_transfer.py,sha256=zRBllpoMs6NhagAmaZWmD4ckbYjSvsSUerBK4oS-HlA,9258
24
- agent0_sdk-0.2.2.dist-info/METADATA,sha256=mw1WxxAWC29tQ-FhDD_c7NJWPwyEGSYJ_w6atYM8CtY,11404
25
- agent0_sdk-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- agent0_sdk-0.2.2.dist-info/top_level.txt,sha256=rgGBfOJlLi1zInQ85jBL2MpDu_ZJNbPjIGz-3Vn5rZs,17
27
- agent0_sdk-0.2.2.dist-info/RECORD,,
26
+ agent0_sdk-0.3rc1.dist-info/METADATA,sha256=AHT6_T6pc63NLD3g1YS01A_TELbzmxRT8tEVN5kSpFI,11377
27
+ agent0_sdk-0.3rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ agent0_sdk-0.3rc1.dist-info/top_level.txt,sha256=rgGBfOJlLi1zInQ85jBL2MpDu_ZJNbPjIGz-3Vn5rZs,17
29
+ agent0_sdk-0.3rc1.dist-info/RECORD,,