agent0-sdk 0.2.1__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.
@@ -80,7 +80,7 @@ class IPFSClient:
80
80
  except (subprocess.CalledProcessError, FileNotFoundError):
81
81
  raise RuntimeError(
82
82
  "filecoin-pin CLI not found. "
83
- "Install it from: https://github.com/filecoin-shipyard/filecoin-pin"
83
+ "Install it from: https://github.com/filecoin-project/filecoin-pin?tab=readme-ov-file#cli"
84
84
  )
85
85
 
86
86
  def _pin_to_filecoin(self, file_path: str) -> str:
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.1
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
@@ -47,13 +47,12 @@ Requires-Dist: eth-account>=0.8.0
47
47
  Requires-Dist: requests>=2.28.0
48
48
  Requires-Dist: pydantic>=2.0.0
49
49
  Requires-Dist: ipfshttpclient>=0.8.0a2
50
- Requires-Dist: numpy>=1.21.0
51
- Requires-Dist: scikit-learn>=1.0.0
52
- Requires-Dist: sentence-transformers>=2.2.0
53
50
  Requires-Dist: aiohttp>=3.8.0
54
51
  Requires-Dist: asyncio-throttle>=1.0.0
55
52
  Requires-Dist: python-dotenv>=1.0.0
56
53
  Requires-Dist: typing-extensions>=4.0.0
54
+ Provides-Extra: indexer
55
+ Requires-Dist: sentence-transformers>=2.2.0; extra == "indexer"
57
56
  Provides-Extra: dev
58
57
  Requires-Dist: pytest>=7.0.0; extra == "dev"
59
58
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -275,7 +274,6 @@ agent.register("https://example.com/agent-registration.json")
275
274
 
276
275
  - More chains (currently Ethereum Sepolia only)
277
276
  - Support for validations
278
- - Multi-chain agents search
279
277
  - Enhanced x402 payments
280
278
  - Semantic/Vectorial search
281
279
  - Advanced reputation aggregation
@@ -0,0 +1,29 @@
1
+ agent0_sdk/__init__.py,sha256=XrY2VSPzM6axhVSb1yTY-oYhJBbTTNJP5mKSFeMRPvk,920
2
+ agent0_sdk/core/agent.py,sha256=RQND8F3Hmmpnt-PqCXBP7T8KfrjTXo0_X2nZoMJh01w,33343
3
+ agent0_sdk/core/contracts.py,sha256=LJY07PIyAb3ZSH_m2xD4OdXawsXKxaTUGhYHwGWUpmI,20435
4
+ agent0_sdk/core/endpoint_crawler.py,sha256=QBkFc3tBSQqHj6PtSTZ5D3_HVB00KJZJdxE3uYpI9po,13611
5
+ agent0_sdk/core/feedback_manager.py,sha256=MV3srP-rgFaDXXELeLTJBj2aDUPdENy0tzWUsJUqrL4,39860
6
+ agent0_sdk/core/indexer.py,sha256=erkDKrtyvAtfrblz0YHekPB2Hpw7tJm-1KJjafTMaHc,68470
7
+ agent0_sdk/core/ipfs_client.py,sha256=fml1ai1BdBkgb95xAkf-ft8QsahV1HL30hBYRz7rQwI,13929
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
+ agent0_sdk/core/web3_client.py,sha256=859ntu5dAmNlcJ3YM1w_VV2gI3mpCC9QEr-GN1236zU,6850
12
+ agent0_sdk-0.3rc1.dist-info/licenses/LICENSE,sha256=rhZZbZm_Ovz4Oa9LNQ-ms8a1tA36wWh90ZkC0OR7WMw,1072
13
+ tests/__init__.py,sha256=60ffheccPhuMCtwiiKP1X-CJJXKpxJ_Ywa0aXGHR9bY,23
14
+ tests/config.py,sha256=1uePvkLBNubOQsvYkQSno0m007PMD1VACgm33fCYY6s,1429
15
+ tests/conftest.py,sha256=P-HCtVVYwSvscuaJqhrgZcv39XXNnr932ekEamzIqis,589
16
+ tests/discover_test_data.py,sha256=Fu0uQKnFk8m7qEqEp293BWbo_mT1CK5szVZcdcdlJQw,17678
17
+ tests/test_feedback.py,sha256=7lszWYSmseJE0I4BhKzZdBiIzf2bgpPqZTZvhRrCTjY,14638
18
+ tests/test_models.py,sha256=kCZdoPasIIcOjEw7ToPldqARdbGVK8v8byOhFwVo7OI,7115
19
+ tests/test_multi_chain.py,sha256=6C-HD037Lsjd7_H0pwp7NDU04jLTjhardT1LHBjahFM,26711
20
+ tests/test_real_public_servers.py,sha256=pCo4aLSCG9qv4D6T7jbyVmP1gt3r1jWxdes6z5XSNhU,3433
21
+ tests/test_registration.py,sha256=pYanDPLAFETIfabBUvO34ZDmyD0Rbcv8vecSfgSrQ70,9542
22
+ tests/test_registrationIpfs.py,sha256=9O3IBiN2CVMKzB19bqb-jN-nhqsN22kQINMpe9THqiI,8400
23
+ tests/test_sdk.py,sha256=dALLFm_A6aXx0ec-CNOLGQoadaSPZ08EEeCS6Tgnm0M,9362
24
+ tests/test_search.py,sha256=SiUio8H-M6Za8OXD_h9tUZdln0ayzkPJ3doTrkHv-Fs,18382
25
+ tests/test_transfer.py,sha256=zRBllpoMs6NhagAmaZWmD4ckbYjSvsSUerBK4oS-HlA,9258
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,,