agent0-sdk 0.2.1__tar.gz → 0.3rc1__tar.gz

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.
Files changed (34) hide show
  1. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/PKG-INFO +3 -5
  2. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/README.md +0 -1
  3. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/__init__.py +1 -1
  4. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/contracts.py +7 -0
  5. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/endpoint_crawler.py +79 -19
  6. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/feedback_manager.py +101 -1
  7. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/indexer.py +750 -12
  8. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/ipfs_client.py +1 -1
  9. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/models.py +3 -2
  10. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/sdk.py +207 -4
  11. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/subgraph_client.py +23 -3
  12. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk.egg-info/PKG-INFO +3 -5
  13. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk.egg-info/SOURCES.txt +2 -0
  14. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk.egg-info/requires.txt +3 -3
  15. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/pyproject.toml +4 -4
  16. agent0_sdk-0.3rc1/tests/discover_test_data.py +445 -0
  17. agent0_sdk-0.3rc1/tests/test_multi_chain.py +588 -0
  18. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/test_search.py +145 -1
  19. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/LICENSE +0 -0
  20. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/agent.py +0 -0
  21. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk/core/web3_client.py +0 -0
  22. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk.egg-info/dependency_links.txt +0 -0
  23. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/agent0_sdk.egg-info/top_level.txt +0 -0
  24. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/setup.cfg +0 -0
  25. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/__init__.py +0 -0
  26. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/config.py +0 -0
  27. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/conftest.py +0 -0
  28. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/test_feedback.py +0 -0
  29. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/test_models.py +0 -0
  30. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/test_real_public_servers.py +0 -0
  31. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/test_registration.py +0 -0
  32. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/test_registrationIpfs.py +0 -0
  33. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/test_sdk.py +0 -0
  34. {agent0_sdk-0.2.1 → agent0_sdk-0.3rc1}/tests/test_transfer.py +0 -0
@@ -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
@@ -204,7 +204,6 @@ agent.register("https://example.com/agent-registration.json")
204
204
 
205
205
  - More chains (currently Ethereum Sepolia only)
206
206
  - Support for validations
207
- - Multi-chain agents search
208
207
  - Enhanced x402 payments
209
208
  - Semantic/Vectorial search
210
209
  - Advanced reputation aggregation
@@ -30,7 +30,7 @@ except ImportError:
30
30
  Agent = None
31
31
  _sdk_available = False
32
32
 
33
- __version__ = "0.2.1"
33
+ __version__ = "0.3rc1"
34
34
  __all__ = [
35
35
  "SDK",
36
36
  "Agent",
@@ -477,6 +477,11 @@ DEFAULT_REGISTRIES: Dict[int, Dict[str, str]] = {
477
477
  "REPUTATION": "0x8004bd8daB57f14Ed299135749a5CB5c42d341BF",
478
478
  "VALIDATION": "0x8004C269D0A5647E51E121FeB226200ECE932d55",
479
479
  },
480
+ 80002: { # Polygon Amoy
481
+ "IDENTITY": "0x8004ad19E14B9e0654f73353e8a0B600D46C2898",
482
+ "REPUTATION": "0x8004B12F4C2B42d00c46479e859C92e39044C930",
483
+ "VALIDATION": "0x8004C11C213ff7BaD36489bcBDF947ba5eee289B",
484
+ },
480
485
  59141: { # Linea Sepolia
481
486
  "IDENTITY": "0x8004aa7C931bCE1233973a0C6A667f73F66282e7",
482
487
  "REPUTATION": "0x8004bd8483b99310df121c46ED8858616b2Bba02",
@@ -487,4 +492,6 @@ DEFAULT_REGISTRIES: Dict[int, Dict[str, str]] = {
487
492
  # Default subgraph URLs for different chains
488
493
  DEFAULT_SUBGRAPH_URLS: Dict[int, str] = {
489
494
  11155111: "https://gateway.thegraph.com/api/00a452ad3cd1900273ea62c1bf283f93/subgraphs/id/6wQRC7geo9XYAhckfmfo8kbMRLeWU8KQd3XsJqFKmZLT", # Ethereum Sepolia
495
+ 84532: "https://gateway.thegraph.com/api/00a452ad3cd1900273ea62c1bf283f93/subgraphs/id/GjQEDgEKqoh5Yc8MUgxoQoRATEJdEiH7HbocfR1aFiHa", # Base Sepolia
496
+ 80002: "https://gateway.thegraph.com/api/00a452ad3cd1900273ea62c1bf283f93/subgraphs/id/2A1JB18r1mF2VNP4QBH4mmxd74kbHoM6xLXC8ABAKf7j", # Polygon Amoy
490
497
  }
@@ -178,13 +178,13 @@ class EndpointCrawler:
178
178
  def fetch_a2a_capabilities(self, endpoint: str) -> Optional[Dict[str, Any]]:
179
179
  """
180
180
  Fetch A2A capabilities (skills) from an A2A server.
181
-
181
+
182
182
  A2A Protocol uses agent cards to describe agent capabilities.
183
- Tries multiple well-known paths: agentcard.json, .well-known/agent.json
184
-
183
+ Tries multiple well-known paths: agentcard.json, .well-known/agent.json, .well-known/agent-card.json
184
+
185
185
  Args:
186
186
  endpoint: A2A endpoint URL (must be http:// or https://)
187
-
187
+
188
188
  Returns:
189
189
  Dict with key: 'a2aSkills'
190
190
  Returns None if unable to fetch
@@ -194,37 +194,97 @@ class EndpointCrawler:
194
194
  if not endpoint.startswith(('http://', 'https://')):
195
195
  logger.warning(f"A2A endpoint must be HTTP/HTTPS, got: {endpoint}")
196
196
  return None
197
-
197
+
198
198
  # Try multiple well-known paths for A2A agent cards
199
+ # Per ERC-8004, endpoint may already be full URL to agent card
200
+ # Per A2A spec section 5.3, recommended discovery path is /.well-known/agent-card.json
199
201
  agentcard_urls = [
200
- f"{endpoint}/agentcard.json",
201
- f"{endpoint}/.well-known/agent.json",
202
- f"{endpoint.rstrip('/')}/.well-known/agent.json"
202
+ endpoint, # Try exact URL first (ERC-8004 format: full path to agent card)
203
+ f"{endpoint}/.well-known/agent-card.json", # Spec-recommended discovery path
204
+ f"{endpoint.rstrip('/')}/.well-known/agent-card.json",
205
+ f"{endpoint}/.well-known/agent.json", # Alternative well-known path
206
+ f"{endpoint.rstrip('/')}/.well-known/agent.json",
207
+ f"{endpoint}/agentcard.json" # Legacy path
203
208
  ]
204
-
209
+
205
210
  for agentcard_url in agentcard_urls:
206
211
  logger.debug(f"Attempting to fetch A2A capabilities from {agentcard_url}")
207
-
212
+
208
213
  try:
209
214
  response = requests.get(agentcard_url, timeout=self.timeout, allow_redirects=True)
210
-
215
+
211
216
  if response.status_code == 200:
212
217
  data = response.json()
213
-
214
- # Extract skills from agentcard
215
- skills = self._extract_list(data, 'skills')
216
-
218
+
219
+ # Extract skill tags from agentcard
220
+ skills = self._extract_a2a_skills(data)
221
+
217
222
  if skills:
218
- logger.info(f"Successfully fetched A2A capabilities from {endpoint}")
223
+ logger.info(f"Successfully fetched A2A capabilities from {agentcard_url}: {len(skills)} skills")
219
224
  return {'a2aSkills': skills}
220
- except requests.exceptions.RequestException:
225
+ except requests.exceptions.RequestException as e:
221
226
  # Try next URL
227
+ logger.debug(f"Failed to fetch from {agentcard_url}: {e}")
222
228
  continue
223
-
229
+
224
230
  except Exception as e:
225
231
  logger.debug(f"Unexpected error fetching A2A capabilities from {endpoint}: {e}")
226
-
232
+
227
233
  return None
234
+
235
+ def _extract_a2a_skills(self, data: Dict[str, Any]) -> List[str]:
236
+ """
237
+ Extract skill tags from A2A agent card.
238
+
239
+ Per A2A Protocol spec (v0.3.0), agent cards should have:
240
+ skills: AgentSkill[] where each AgentSkill has a tags[] array
241
+
242
+ This method also handles non-standard formats for backward compatibility:
243
+ - detailedSkills[].tags[] (custom extension)
244
+ - skills: ["tag1", "tag2"] (non-compliant flat array)
245
+
246
+ Args:
247
+ data: Agent card JSON data
248
+
249
+ Returns:
250
+ List of unique skill tags (strings)
251
+ """
252
+ result = []
253
+
254
+ # Try spec-compliant format first: skills[].tags[]
255
+ if 'skills' in data and isinstance(data['skills'], list):
256
+ for skill in data['skills']:
257
+ if isinstance(skill, dict) and 'tags' in skill:
258
+ # Spec-compliant: AgentSkill object with tags
259
+ tags = skill['tags']
260
+ if isinstance(tags, list):
261
+ for tag in tags:
262
+ if isinstance(tag, str):
263
+ result.append(tag)
264
+ elif isinstance(skill, str):
265
+ # Non-compliant: flat string array (fallback)
266
+ result.append(skill)
267
+
268
+ # Fallback to detailedSkills if no tags found in skills
269
+ # (custom extension used by some implementations)
270
+ if not result and 'detailedSkills' in data and isinstance(data['detailedSkills'], list):
271
+ for skill in data['detailedSkills']:
272
+ if isinstance(skill, dict) and 'tags' in skill:
273
+ tags = skill['tags']
274
+ if isinstance(tags, list):
275
+ for tag in tags:
276
+ if isinstance(tag, str):
277
+ result.append(tag)
278
+
279
+ # Remove duplicates while preserving order
280
+ seen = set()
281
+ unique_result = []
282
+ for item in result:
283
+ if item not in seen:
284
+ seen.add(item)
285
+ unique_result.append(item)
286
+
287
+ return unique_result
228
288
 
229
289
  def _extract_list(self, data: Dict[str, Any], key: str) -> List[str]:
230
290
  """
@@ -703,7 +703,46 @@ class FeedbackManager:
703
703
  groupBy: Optional[List[str]] = None,
704
704
  ) -> Dict[str, Any]:
705
705
  """Get reputation summary for an agent with optional grouping."""
706
- # Parse agent ID
706
+ # Parse chainId from agentId
707
+ chain_id = None
708
+ if ":" in agentId:
709
+ try:
710
+ chain_id = int(agentId.split(":", 1)[0])
711
+ except ValueError:
712
+ chain_id = None
713
+
714
+ # Try subgraph first (if available and indexer supports it)
715
+ if self.indexer and self.subgraph_client:
716
+ # Get correct subgraph client for the chain
717
+ subgraph_client = None
718
+ full_agent_id = agentId
719
+
720
+ if chain_id is not None:
721
+ subgraph_client = self.indexer._get_subgraph_client_for_chain(chain_id)
722
+ else:
723
+ # No chainId in agentId, use SDK's default
724
+ # Construct full agentId format for subgraph query
725
+ default_chain_id = self.web3_client.chain_id
726
+ token_id = agentId.split(":")[-1] if ":" in agentId else agentId
727
+ full_agent_id = f"{default_chain_id}:{token_id}"
728
+ subgraph_client = self.subgraph_client
729
+
730
+ if subgraph_client:
731
+ # Use subgraph to calculate reputation
732
+ return self._get_reputation_summary_from_subgraph(
733
+ full_agent_id, clientAddresses, tag1, tag2, groupBy
734
+ )
735
+
736
+ # Fallback to blockchain (requires chain-specific web3 client)
737
+ # For now, only works if chain matches SDK's default
738
+ if chain_id is not None and chain_id != self.web3_client.chain_id:
739
+ raise ValueError(
740
+ f"Blockchain reputation summary not supported for chain {chain_id}. "
741
+ f"SDK is configured for chain {self.web3_client.chain_id}. "
742
+ f"Use subgraph-based summary instead."
743
+ )
744
+
745
+ # Parse agent ID for blockchain call
707
746
  if ":" in agentId:
708
747
  tokenId = int(agentId.split(":")[-1])
709
748
  else:
@@ -765,6 +804,67 @@ class FeedbackManager:
765
804
  except Exception as e:
766
805
  raise ValueError(f"Failed to get reputation summary: {e}")
767
806
 
807
+ def _get_reputation_summary_from_subgraph(
808
+ self,
809
+ agentId: AgentId,
810
+ clientAddresses: Optional[List[Address]] = None,
811
+ tag1: Optional[str] = None,
812
+ tag2: Optional[str] = None,
813
+ groupBy: Optional[List[str]] = None,
814
+ ) -> Dict[str, Any]:
815
+ """Get reputation summary from subgraph."""
816
+ # Build tags list
817
+ tags = []
818
+ if tag1:
819
+ tags.append(tag1)
820
+ if tag2:
821
+ tags.append(tag2)
822
+
823
+ # Get all feedback for the agent using indexer (which handles multi-chain)
824
+ # Use searchFeedback with a large limit to get all feedback
825
+ all_feedback = self.searchFeedback(
826
+ agentId=agentId,
827
+ clientAddresses=clientAddresses,
828
+ tags=tags if tags else None,
829
+ include_revoked=False,
830
+ first=1000, # Large limit to get all feedback
831
+ skip=0
832
+ )
833
+
834
+ # Calculate summary statistics
835
+ count = len(all_feedback)
836
+ scores = [fb.score for fb in all_feedback if fb.score is not None]
837
+ average_score = sum(scores) / len(scores) if scores else 0.0
838
+
839
+ # If no grouping requested, return simple summary
840
+ if not groupBy:
841
+ return {
842
+ "agentId": agentId,
843
+ "count": count,
844
+ "averageScore": average_score,
845
+ "filters": {
846
+ "clientAddresses": clientAddresses,
847
+ "tag1": tag1,
848
+ "tag2": tag2
849
+ }
850
+ }
851
+
852
+ # Group feedback by requested dimensions
853
+ grouped_data = self._groupFeedback(all_feedback, groupBy)
854
+
855
+ return {
856
+ "agentId": agentId,
857
+ "totalCount": count,
858
+ "totalAverageScore": average_score,
859
+ "groupedData": grouped_data,
860
+ "filters": {
861
+ "clientAddresses": clientAddresses,
862
+ "tag1": tag1,
863
+ "tag2": tag2
864
+ },
865
+ "groupBy": groupBy
866
+ }
867
+
768
868
  def _groupFeedback(self, feedbackList: List[Feedback], groupBy: List[str]) -> Dict[str, Any]:
769
869
  """Group feedback by specified dimensions."""
770
870
  grouped = {}