agent0-sdk 1.0.1__py3-none-any.whl → 1.0.2__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/__init__.py CHANGED
@@ -30,7 +30,7 @@ except ImportError:
30
30
  Agent = None
31
31
  _sdk_available = False
32
32
 
33
- __version__ = "1.0.1"
33
+ __version__ = "1.0.2"
34
34
  __all__ = [
35
35
  "SDK",
36
36
  "Agent",
agent0_sdk/core/agent.py CHANGED
@@ -327,7 +327,8 @@ class Agent:
327
327
  oasf_endpoint = Endpoint(
328
328
  type=EndpointType.OASF,
329
329
  value="https://github.com/agntcy/oasf/",
330
- meta={"version": "v0.8.0", "skills": [], "domains": []}
330
+ # Version string follows ERC-8004 spec example ("0.8")
331
+ meta={"version": "0.8", "skills": [], "domains": []}
331
332
  )
332
333
  self.registration_file.endpoints.append(oasf_endpoint)
333
334
  return oasf_endpoint
@@ -529,14 +530,14 @@ class Agent:
529
530
  raise ValueError("Wallet address cannot be empty. Use a non-zero address.")
530
531
 
531
532
  # Validate address format
532
- if not addr.startswith("0x") or len(addr) != 42:
533
- raise ValueError(f"Invalid Ethereum address format: {addr}. Must be 42 characters starting with '0x'")
534
-
535
- # Validate hexadecimal characters
536
- try:
537
- int(addr[2:], 16)
538
- except ValueError:
539
- raise ValueError(f"Invalid hexadecimal characters in address: {addr}")
533
+ if not addr.startswith("0x") or len(addr) != 42:
534
+ raise ValueError(f"Invalid Ethereum address format: {addr}. Must be 42 characters starting with '0x'")
535
+
536
+ # Validate hexadecimal characters
537
+ try:
538
+ int(addr[2:], 16)
539
+ except ValueError:
540
+ raise ValueError(f"Invalid hexadecimal characters in address: {addr}")
540
541
 
541
542
  # Determine chain ID to use (local bookkeeping)
542
543
  if chainId is None:
@@ -346,7 +346,8 @@ REPUTATION_REGISTRY_ABI = [
346
346
  {"indexed": True, "internalType": "address", "name": "clientAddress", "type": "address"},
347
347
  {"indexed": False, "internalType": "uint64", "name": "feedbackIndex", "type": "uint64"},
348
348
  {"indexed": False, "internalType": "uint8", "name": "score", "type": "uint8"},
349
- {"indexed": True, "internalType": "string", "name": "tag1", "type": "string"},
349
+ {"indexed": True, "internalType": "string", "name": "indexedTag1", "type": "string"},
350
+ {"indexed": False, "internalType": "string", "name": "tag1", "type": "string"},
350
351
  {"indexed": False, "internalType": "string", "name": "tag2", "type": "string"},
351
352
  {"indexed": False, "internalType": "string", "name": "endpoint", "type": "string"},
352
353
  {"indexed": False, "internalType": "string", "name": "feedbackURI", "type": "string"},
@@ -8,7 +8,7 @@ import json
8
8
  import logging
9
9
  import time
10
10
  from typing import Any, Dict, List, Optional, Tuple, Union
11
- from datetime import datetime
11
+ from datetime import datetime, timezone
12
12
 
13
13
  from .models import (
14
14
  AgentId, Address, URI, Timestamp, IdemKey,
@@ -40,77 +40,66 @@ class FeedbackManager:
40
40
  self.subgraph_client = subgraph_client
41
41
  self.indexer = indexer
42
42
 
43
- def prepareFeedback(
44
- self,
45
- agentId: AgentId,
46
- score: Optional[int] = None, # 0-100
47
- tags: List[str] = None,
48
- text: Optional[str] = None,
49
- capability: Optional[str] = None,
50
- name: Optional[str] = None,
51
- skill: Optional[str] = None,
52
- task: Optional[str] = None,
53
- endpoint: Optional[str] = None,
54
- domain: Optional[str] = None,
55
- context: Optional[Dict[str, Any]] = None,
56
- proofOfPayment: Optional[Dict[str, Any]] = None,
57
- extra: Optional[Dict[str, Any]] = None,
58
- ) -> Dict[str, Any]:
59
- """Prepare feedback file (local file/object) according to spec."""
60
- if tags is None:
61
- tags = []
62
-
63
- # Parse agent ID to get token ID
64
- if ":" in agentId:
65
- tokenId = int(agentId.split(":")[-1])
66
- else:
67
- tokenId = int(agentId)
68
-
69
- # Get current timestamp in ISO format
70
- createdAt = datetime.fromtimestamp(time.time()).isoformat() + "Z"
71
-
72
- # Build feedback data according to spec
73
- feedbackData = {
74
- # MUST FIELDS
75
- "agentRegistry": f"eip155:{self.web3_client.chain_id}:{self.identity_registry.address if self.identity_registry else '0x0'}",
76
- "agentId": tokenId,
77
- "clientAddress": f"eip155:{self.web3_client.chain_id}:{self.web3_client.account.address}",
78
- "createdAt": createdAt,
79
- "score": int(score) if score else 0, # Score as integer (0-100)
80
-
81
- # MAY FIELDS
82
- "tag1": tags[0] if tags else None,
83
- "tag2": tags[1] if len(tags) > 1 else None,
84
- "endpoint": endpoint,
85
- "domain": domain,
86
- "skill": skill,
87
- "context": context,
88
- "task": task,
89
- "capability": capability,
90
- "name": name,
91
- "proofOfPayment": proofOfPayment,
92
- }
93
-
94
- # Remove None values to keep the structure clean
95
- feedbackData = {k: v for k, v in feedbackData.items() if v is not None}
43
+ def prepareFeedbackFile(self, input: Dict[str, Any]) -> Dict[str, Any]:
44
+ """Prepare an off-chain feedback file payload (no on-chain fields).
96
45
 
97
- if extra:
98
- feedbackData.update(extra)
46
+ This intentionally does NOT attempt to represent on-chain fields like:
47
+ score/tag1/tag2/endpoint (on-chain value), or registry-derived fields.
99
48
 
100
- return feedbackData
49
+ It may validate/normalize and remove None values.
50
+ """
51
+ if input is None:
52
+ raise ValueError("prepareFeedbackFile input cannot be None")
53
+ if not isinstance(input, dict):
54
+ raise TypeError(f"prepareFeedbackFile input must be a dict, got {type(input)}")
55
+
56
+ # Shallow copy and strip None values
57
+ out: Dict[str, Any] = {k: v for k, v in dict(input).items() if v is not None}
58
+
59
+ # Minimal normalization for known optional fields
60
+ if "endpoint" in out and out["endpoint"] is not None and not isinstance(out["endpoint"], str):
61
+ out["endpoint"] = str(out["endpoint"])
62
+ if "domain" in out and out["domain"] is not None and not isinstance(out["domain"], str):
63
+ out["domain"] = str(out["domain"])
64
+
65
+ return out
101
66
 
102
67
  def giveFeedback(
103
68
  self,
104
69
  agentId: AgentId,
105
- feedbackFile: Dict[str, Any],
106
- idem: Optional[IdemKey] = None,
70
+ score: int,
71
+ tag1: Optional[str] = None,
72
+ tag2: Optional[str] = None,
73
+ endpoint: Optional[str] = None,
74
+ feedbackFile: Optional[Dict[str, Any]] = None,
107
75
  ) -> Feedback:
108
76
  """Give feedback (maps 8004 endpoint)."""
109
- # Parse agent ID
110
- if ":" in agentId:
111
- tokenId = int(agentId.split(":")[-1])
77
+ # Parse agentId into (chainId, tokenId)
78
+ agent_chain_id: Optional[int] = None
79
+ tokenId: int
80
+ if isinstance(agentId, str) and agentId.startswith("eip155:"):
81
+ parts = agentId.split(":")
82
+ if len(parts) != 3:
83
+ raise ValueError(f"Invalid AgentId (expected eip155:chainId:tokenId): {agentId}")
84
+ agent_chain_id = int(parts[1])
85
+ tokenId = int(parts[2])
86
+ elif isinstance(agentId, str) and ":" in agentId:
87
+ parts = agentId.split(":")
88
+ if len(parts) != 2:
89
+ raise ValueError(f"Invalid AgentId (expected chainId:tokenId): {agentId}")
90
+ agent_chain_id = int(parts[0])
91
+ tokenId = int(parts[1])
112
92
  else:
113
93
  tokenId = int(agentId)
94
+ agent_chain_id = int(self.web3_client.chain_id)
95
+
96
+ # Ensure we are submitting the tx on the agent's chain
97
+ if int(self.web3_client.chain_id) != int(agent_chain_id):
98
+ raise ValueError(
99
+ f"Chain mismatch for giveFeedback: agentId={agentId} targets chainId={agent_chain_id}, "
100
+ f"but web3 client is connected to chainId={self.web3_client.chain_id}. "
101
+ f"Initialize the SDK/Web3Client for chainId={agent_chain_id}."
102
+ )
114
103
 
115
104
  # Get client address (the one giving feedback)
116
105
  # Keep in checksum format for blockchain calls (web3.py requirement)
@@ -128,30 +117,94 @@ class FeedbackManager:
128
117
  except Exception as e:
129
118
  raise ValueError(f"Failed to get feedback index: {e}")
130
119
 
131
- # Prepare on-chain data
132
- score = feedbackFile.get("score", 0) # Already in 0-100 range
133
- tag1 = feedbackFile.get("tag1", "") or ""
134
- tag2 = feedbackFile.get("tag2", "") or ""
135
- endpoint = feedbackFile.get("endpoint", "") or ""
120
+ if score is None:
121
+ raise ValueError("score is required")
122
+ if not isinstance(score, int):
123
+ # Allow numeric strings / floats if passed accidentally
124
+ score = int(score)
125
+
126
+ tag1 = tag1 or ""
127
+ tag2 = tag2 or ""
128
+
129
+ feedback_file: Optional[Dict[str, Any]] = feedbackFile
130
+ if feedback_file is not None and not isinstance(feedback_file, dict):
131
+ raise TypeError(f"feedbackFile must be a dict when provided, got {type(feedback_file)}")
132
+
133
+ # Endpoint precedence: explicit arg > file endpoint > empty string
134
+ if endpoint:
135
+ endpoint_onchain = endpoint
136
+ elif feedback_file and isinstance(feedback_file.get("endpoint"), str) and feedback_file.get("endpoint"):
137
+ endpoint_onchain = feedback_file.get("endpoint")
138
+ else:
139
+ endpoint_onchain = ""
140
+
141
+ # If uploading a file and we have an explicit endpoint, inject it for consistency
142
+ if feedback_file is not None and endpoint and isinstance(endpoint, str):
143
+ feedback_file = dict(feedback_file)
144
+ feedback_file["endpoint"] = endpoint
136
145
 
137
146
  # Handle off-chain file storage
138
147
  feedbackUri = ""
139
148
  feedbackHash = b"\x00" * 32 # Default empty hash
140
149
 
141
- if self.ipfs_client:
142
- # Store feedback file on IPFS using Filecoin Pin
150
+ if feedback_file is not None:
151
+ if not self.ipfs_client:
152
+ raise ValueError("feedbackFile was provided, but no IPFS client is configured")
153
+
154
+ # Store an ERC-8004 compliant feedback file on IPFS (explicit opt-in)
143
155
  try:
144
156
  logger.debug("Storing feedback file on IPFS")
145
- cid = self.ipfs_client.add_json(feedbackFile)
157
+ # createdAt MUST be present in the off-chain file; use provided value if valid, else now (UTC).
158
+ created_at = feedback_file.get("createdAt")
159
+ if not isinstance(created_at, str) or not created_at:
160
+ created_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
161
+
162
+ identity_registry_address = "0x0"
163
+ try:
164
+ if self.identity_registry is not None:
165
+ identity_registry_address = str(getattr(self.identity_registry, "address", "0x0"))
166
+ except Exception:
167
+ identity_registry_address = "0x0"
168
+
169
+ # Remove any user-provided copies of the envelope keys; SDK-owned values must win
170
+ rich = dict(feedback_file)
171
+ for k in [
172
+ "agentRegistry",
173
+ "agentId",
174
+ "clientAddress",
175
+ "createdAt",
176
+ "score",
177
+ "tag1",
178
+ "tag2",
179
+ "endpoint",
180
+ ]:
181
+ rich.pop(k, None)
182
+
183
+ file_for_storage: Dict[str, Any] = {
184
+ # MUST fields (spec)
185
+ "agentRegistry": f"eip155:{agent_chain_id}:{identity_registry_address}",
186
+ "agentId": tokenId,
187
+ "clientAddress": f"eip155:{agent_chain_id}:{clientAddress}",
188
+ "createdAt": created_at,
189
+ "score": int(score),
190
+
191
+ # OPTIONAL fields that mirror on-chain
192
+ **({"tag1": tag1} if tag1 else {}),
193
+ **({"tag2": tag2} if tag2 else {}),
194
+ **({"endpoint": endpoint_onchain} if endpoint_onchain else {}),
195
+
196
+ # Rich/off-chain fields
197
+ **rich,
198
+ }
199
+
200
+ cid = self.ipfs_client.addFeedbackFile(file_for_storage)
146
201
  feedbackUri = f"ipfs://{cid}"
147
- feedbackHash = self.web3_client.keccak256(json.dumps(feedbackFile, sort_keys=True).encode())
202
+ feedbackHash = self.web3_client.keccak256(
203
+ json.dumps(file_for_storage, sort_keys=True).encode()
204
+ )
148
205
  logger.debug(f"Feedback file stored on IPFS: {cid}")
149
206
  except Exception as e:
150
- logger.warning(f"Failed to store feedback on IPFS: {e}")
151
- # Continue without IPFS storage
152
- elif feedbackFile.get("context") or feedbackFile.get("capability") or feedbackFile.get("name"):
153
- # If we have rich data but no IPFS, we need to store it somewhere
154
- raise ValueError("Rich feedback data requires IPFS client for storage")
207
+ raise ValueError(f"Failed to store feedback on IPFS: {e}")
155
208
 
156
209
  # Submit to blockchain with new signature: giveFeedback(agentId, score, tag1, tag2, endpoint, feedbackURI, feedbackHash)
157
210
  try:
@@ -162,7 +215,7 @@ class FeedbackManager:
162
215
  score,
163
216
  tag1,
164
217
  tag2,
165
- endpoint,
218
+ endpoint_onchain,
166
219
  feedbackUri,
167
220
  feedbackHash
168
221
  )
@@ -176,24 +229,25 @@ class FeedbackManager:
176
229
  # Create feedback object (address normalization happens in Feedback.create_id)
177
230
  feedbackId = Feedback.create_id(agentId, clientAddress, feedbackIndex)
178
231
 
232
+ ff: Dict[str, Any] = feedback_file or {}
179
233
  return Feedback(
180
234
  id=feedbackId,
181
235
  agentId=agentId,
182
236
  reviewer=clientAddress, # create_id normalizes the ID; reviewer field can remain as-is
183
237
  score=int(score) if score and score > 0 else None,
184
238
  tags=[tag1, tag2] if tag1 or tag2 else [],
185
- text=feedbackFile.get("text"),
186
- context=feedbackFile.get("context"),
187
- proofOfPayment=feedbackFile.get("proofOfPayment"),
239
+ text=ff.get("text"),
240
+ context=ff.get("context"),
241
+ proofOfPayment=ff.get("proofOfPayment"),
188
242
  fileURI=feedbackUri if feedbackUri else None,
189
- endpoint=endpoint if endpoint else None,
243
+ endpoint=endpoint_onchain if endpoint_onchain else None,
190
244
  createdAt=int(time.time()),
191
245
  isRevoked=False,
192
246
  # Off-chain only fields
193
- capability=feedbackFile.get("capability"),
194
- name=feedbackFile.get("name"),
195
- skill=feedbackFile.get("skill"),
196
- task=feedbackFile.get("task")
247
+ capability=ff.get("capability"),
248
+ name=ff.get("name"),
249
+ skill=ff.get("skill"),
250
+ task=ff.get("task")
197
251
  )
198
252
 
199
253
  def getFeedback(
@@ -210,14 +264,14 @@ class FeedbackManager:
210
264
  except Exception as e:
211
265
  logger.debug(f"Indexer/subgraph get_feedback failed, falling back to blockchain: {e}")
212
266
  return self._get_feedback_from_blockchain(agentId, clientAddress, feedbackIndex)
213
-
267
+
214
268
  if self.subgraph_client:
215
269
  try:
216
270
  return self._get_feedback_from_subgraph(agentId, clientAddress, feedbackIndex)
217
271
  except Exception as e:
218
272
  logger.debug(f"Subgraph get feedback failed, falling back to blockchain: {e}")
219
273
  return self._get_feedback_from_blockchain(agentId, clientAddress, feedbackIndex)
220
-
274
+
221
275
  return self._get_feedback_from_blockchain(agentId, clientAddress, feedbackIndex)
222
276
 
223
277
  def _get_feedback_from_subgraph(
@@ -267,9 +321,9 @@ class FeedbackManager:
267
321
  tag1 = feedback_data.get('tag1') or feedback_file.get('tag1')
268
322
  tag2 = feedback_data.get('tag2') or feedback_file.get('tag2')
269
323
  if isinstance(tag1, str) and tag1:
270
- tags.append(tag1)
324
+ tags.append(tag1)
271
325
  if isinstance(tag2, str) and tag2:
272
- tags.append(tag2)
326
+ tags.append(tag2)
273
327
 
274
328
  return Feedback(
275
329
  id=Feedback.create_id(agentId, clientAddress, feedbackIndex), # create_id now normalizes
@@ -287,7 +341,8 @@ class FeedbackManager:
287
341
  'txHash': feedback_file.get('proofOfPaymentTxHash'),
288
342
  } if feedback_file.get('proofOfPaymentFromAddress') else None,
289
343
  fileURI=feedback_data.get('feedbackURI') or feedback_data.get('feedbackUri'), # Handle both old and new field names
290
- endpoint=feedback_data.get('endpoint'),
344
+ # Prefer on-chain endpoint; fall back to off-chain file endpoint if missing
345
+ endpoint=feedback_data.get('endpoint') or feedback_file.get('endpoint'),
291
346
  createdAt=feedback_data.get('createdAt', int(time.time())),
292
347
  answers=answers,
293
348
  isRevoked=feedback_data.get('isRevoked', False),
@@ -509,9 +564,9 @@ class FeedbackManager:
509
564
  tag1 = fb_data.get('tag1') or feedback_file.get('tag1')
510
565
  tag2 = fb_data.get('tag2') or feedback_file.get('tag2')
511
566
  if isinstance(tag1, str) and tag1:
512
- tags_list.append(tag1)
567
+ tags_list.append(tag1)
513
568
  if isinstance(tag2, str) and tag2:
514
- tags_list.append(tag2)
569
+ tags_list.append(tag2)
515
570
 
516
571
  # Parse agentId from feedback ID
517
572
  feedback_id = fb_data['id']
@@ -1094,7 +1094,8 @@ class AgentIndexer:
1094
1094
  'txHash': feedback_file.get('proofOfPaymentTxHash'),
1095
1095
  } if feedback_file.get('proofOfPaymentFromAddress') else None,
1096
1096
  fileURI=feedback_data.get('feedbackURI') or feedback_data.get('feedbackUri'), # Handle both old and new field names
1097
- endpoint=feedback_data.get('endpoint'),
1097
+ # Prefer on-chain endpoint; fall back to off-chain file endpoint if missing
1098
+ endpoint=feedback_data.get('endpoint') or feedback_file.get('endpoint'),
1098
1099
  createdAt=feedback_data.get('createdAt', int(time.time())),
1099
1100
  answers=answers,
1100
1101
  isRevoked=feedback_data.get('isRevoked', False),
@@ -160,7 +160,7 @@ class IPFSClient:
160
160
  # add_str returns the CID directly as a string
161
161
  return result if isinstance(result, str) else result['Hash']
162
162
 
163
- def _pin_to_pinata(self, data: str) -> str:
163
+ def _pin_to_pinata(self, data: str, file_name: str = "file.json") -> str:
164
164
  """Pin data to Pinata using JWT authentication with v3 API."""
165
165
  import requests
166
166
  import tempfile
@@ -185,7 +185,7 @@ class IPFSClient:
185
185
  # Prepare the file for upload with public network setting
186
186
  with open(temp_path, 'rb') as file:
187
187
  files = {
188
- 'file': ('registration.json', file, 'application/json')
188
+ 'file': (file_name, file, 'application/json')
189
189
  }
190
190
 
191
191
  # Add network parameter to make file public
@@ -233,8 +233,9 @@ class IPFSClient:
233
233
 
234
234
  def add(self, data: str, **kwargs) -> str:
235
235
  """Add data to IPFS and return CID."""
236
+ file_name = kwargs.pop("file_name", None)
236
237
  if self.pinata_enabled:
237
- return self._pin_to_pinata(data)
238
+ return self._pin_to_pinata(data, file_name=file_name or "file.json")
238
239
  elif self.filecoin_pin_enabled:
239
240
  # Create temporary file for Filecoin Pin
240
241
  with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
@@ -251,11 +252,12 @@ class IPFSClient:
251
252
 
252
253
  def add_file(self, filepath: str, **kwargs) -> str:
253
254
  """Add file to IPFS and return CID."""
255
+ file_name = kwargs.pop("file_name", None)
254
256
  if self.pinata_enabled:
255
257
  # Read file and send to Pinata
256
258
  with open(filepath, 'r') as f:
257
259
  data = f.read()
258
- return self._pin_to_pinata(data)
260
+ return self._pin_to_pinata(data, file_name=file_name or "file.json")
259
261
  elif self.filecoin_pin_enabled:
260
262
  return self._pin_to_filecoin(filepath)
261
263
  else:
@@ -332,7 +334,7 @@ class IPFSClient:
332
334
  def addRegistrationFile(self, registrationFile: "RegistrationFile", chainId: Optional[int] = None, identityRegistryAddress: Optional[str] = None, **kwargs) -> str:
333
335
  """Add registration file to IPFS and return CID."""
334
336
  data = registrationFile.to_dict(chain_id=chainId, identity_registry_address=identityRegistryAddress)
335
- return self.add_json(data, **kwargs)
337
+ return self.add_json(data, file_name="agent-registration.json", **kwargs)
336
338
 
337
339
  def getRegistrationFile(self, cid: str) -> "RegistrationFile":
338
340
  """Get registration file from IPFS by CID."""
@@ -342,7 +344,7 @@ class IPFSClient:
342
344
 
343
345
  def addFeedbackFile(self, feedbackData: Dict[str, Any], **kwargs) -> str:
344
346
  """Add feedback file to IPFS and return CID."""
345
- return self.add_json(feedbackData, **kwargs)
347
+ return self.add_json(feedbackData, file_name="feedback.json", **kwargs)
346
348
 
347
349
  def getFeedbackFile(self, cid: str) -> Dict[str, Any]:
348
350
  """Get feedback file from IPFS by CID."""
agent0_sdk/core/sdk.py CHANGED
@@ -491,111 +491,8 @@ class SDK:
491
491
 
492
492
  return self.indexer.search_agents(params, sort, page_size, cursor)
493
493
 
494
- # Feedback methods
495
- def prepareFeedback(
496
- self,
497
- agentId: AgentId,
498
- score: Optional[int] = None, # 0-100
499
- tags: List[str] = None,
500
- text: Optional[str] = None,
501
- capability: Optional[str] = None,
502
- name: Optional[str] = None,
503
- skill: Optional[str] = None,
504
- task: Optional[str] = None,
505
- context: Optional[Dict[str, Any]] = None,
506
- proofOfPayment: Optional[Dict[str, Any]] = None,
507
- extra: Optional[Dict[str, Any]] = None,
508
- ) -> Dict[str, Any]:
509
- """Prepare feedback file (local file/object)."""
510
- return self.feedback_manager.prepareFeedback(
511
- agentId=agentId,
512
- score=score,
513
- tags=tags,
514
- text=text,
515
- capability=capability,
516
- name=name,
517
- skill=skill,
518
- task=task,
519
- context=context,
520
- proofOfPayment=proofOfPayment,
521
- extra=extra
522
- )
523
-
524
- def giveFeedback(
525
- self,
526
- agentId: AgentId,
527
- feedbackFile: Dict[str, Any],
528
- idem: Optional[IdemKey] = None,
529
- feedback_auth: Optional[bytes] = None,
530
- ) -> Feedback:
531
- """Give feedback (maps 8004 endpoint)."""
532
- return self.feedback_manager.giveFeedback(
533
- agentId=agentId,
534
- feedbackFile=feedbackFile,
535
- idem=idem,
536
- feedback_auth=feedback_auth
537
- )
538
-
539
- def getFeedback(self, feedbackId: str) -> Feedback:
540
- """Get single feedback by ID string."""
541
- # Parse feedback ID
542
- agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
543
- return self.feedback_manager.getFeedback(agentId, clientAddress, feedbackIndex)
544
-
545
- def searchFeedback(
546
- self,
547
- agentId: AgentId,
548
- reviewers: Optional[List[Address]] = None,
549
- tags: Optional[List[str]] = None,
550
- capabilities: Optional[List[str]] = None,
551
- skills: Optional[List[str]] = None,
552
- tasks: Optional[List[str]] = None,
553
- names: Optional[List[str]] = None,
554
- minScore: Optional[int] = None,
555
- maxScore: Optional[int] = None,
556
- include_revoked: bool = False,
557
- first: int = 100,
558
- skip: int = 0,
559
- ) -> List[Feedback]:
560
- """Search feedback for an agent."""
561
- return self.feedback_manager.searchFeedback(
562
- agentId=agentId,
563
- clientAddresses=reviewers,
564
- tags=tags,
565
- capabilities=capabilities,
566
- skills=skills,
567
- tasks=tasks,
568
- names=names,
569
- minScore=minScore,
570
- maxScore=maxScore,
571
- include_revoked=include_revoked,
572
- first=first,
573
- skip=skip
574
- )
575
-
576
- def revokeFeedback(
577
- self,
578
- feedbackId: str,
579
- reason: Optional[str] = None,
580
- idem: Optional[IdemKey] = None,
581
- ) -> Dict[str, Any]:
582
- """Revoke feedback."""
583
- # Parse feedback ID
584
- agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
585
- return self.feedback_manager.revokeFeedback(agentId, feedbackIndex)
586
-
587
- def appendResponse(
588
- self,
589
- feedbackId: str,
590
- response: Dict[str, Any],
591
- idem: Optional[IdemKey] = None,
592
- ) -> Feedback:
593
- """Append a response/follow-up to existing feedback."""
594
- # Parse feedback ID
595
- agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
596
- return self.feedback_manager.appendResponse(agentId, clientAddress, feedbackIndex, response)
494
+ # Feedback methods are defined later in this class (single authoritative API).
597
495
 
598
-
599
496
  def searchAgentsByReputation(
600
497
  self,
601
498
  agents: Optional[List[AgentId]] = None,
@@ -884,46 +781,35 @@ class SDK:
884
781
  }
885
782
 
886
783
  # Feedback methods - delegate to feedback_manager
887
- def signFeedbackAuth(
888
- self,
889
- agentId: "AgentId",
890
- clientAddress: "Address",
891
- indexLimit: Optional[int] = None,
892
- expiryHours: int = 24,
893
- ) -> bytes:
894
- """Sign feedback authorization for a client."""
895
- return self.feedback_manager.signFeedbackAuth(
896
- agentId, clientAddress, indexLimit, expiryHours
897
- )
898
-
899
- def prepareFeedback(
900
- self,
901
- agentId: "AgentId",
902
- score: Optional[int] = None, # 0-100
903
- tags: List[str] = None,
904
- text: Optional[str] = None,
905
- capability: Optional[str] = None,
906
- name: Optional[str] = None,
907
- skill: Optional[str] = None,
908
- task: Optional[str] = None,
909
- context: Optional[Dict[str, Any]] = None,
910
- proofOfPayment: Optional[Dict[str, Any]] = None,
911
- extra: Optional[Dict[str, Any]] = None,
912
- ) -> Dict[str, Any]:
913
- """Prepare feedback file (local file/object) according to spec."""
914
- return self.feedback_manager.prepareFeedback(
915
- agentId, score, tags, text, capability, name, skill, task, context, proofOfPayment, extra
916
- )
784
+ def prepareFeedbackFile(self, input: Dict[str, Any]) -> Dict[str, Any]:
785
+ """Prepare an off-chain feedback file payload.
786
+
787
+ This is intentionally off-chain-only; it does not attempt to represent
788
+ the on-chain fields (score/tag1/tag2/endpoint-on-chain).
789
+ """
790
+ return self.feedback_manager.prepareFeedbackFile(input)
917
791
 
918
792
  def giveFeedback(
919
793
  self,
920
794
  agentId: "AgentId",
921
- feedbackFile: Dict[str, Any],
922
- idem: Optional["IdemKey"] = None,
795
+ score: int,
796
+ tag1: Optional[str] = None,
797
+ tag2: Optional[str] = None,
798
+ endpoint: Optional[str] = None,
799
+ feedbackFile: Optional[Dict[str, Any]] = None,
923
800
  ) -> "Feedback":
924
- """Give feedback (maps 8004 endpoint)."""
801
+ """Give feedback (on-chain first; optional off-chain file upload).
802
+
803
+ - If feedbackFile is None: submit on-chain only (no upload even if IPFS is configured).
804
+ - If feedbackFile is provided: requires IPFS configured; uploads and commits URI/hash on-chain.
805
+ """
925
806
  return self.feedback_manager.giveFeedback(
926
- agentId, feedbackFile, idem
807
+ agentId=agentId,
808
+ score=score,
809
+ tag1=tag1,
810
+ tag2=tag2,
811
+ endpoint=endpoint,
812
+ feedbackFile=feedbackFile,
927
813
  )
928
814
 
929
815
  def getFeedback(
@@ -936,6 +822,37 @@ class SDK:
936
822
  return self.feedback_manager.getFeedback(
937
823
  agentId, clientAddress, feedbackIndex
938
824
  )
825
+
826
+ def searchFeedback(
827
+ self,
828
+ agentId: "AgentId",
829
+ reviewers: Optional[List["Address"]] = None,
830
+ tags: Optional[List[str]] = None,
831
+ capabilities: Optional[List[str]] = None,
832
+ skills: Optional[List[str]] = None,
833
+ tasks: Optional[List[str]] = None,
834
+ names: Optional[List[str]] = None,
835
+ minScore: Optional[int] = None,
836
+ maxScore: Optional[int] = None,
837
+ include_revoked: bool = False,
838
+ first: int = 100,
839
+ skip: int = 0,
840
+ ) -> List["Feedback"]:
841
+ """Search feedback for an agent."""
842
+ return self.feedback_manager.searchFeedback(
843
+ agentId=agentId,
844
+ clientAddresses=reviewers,
845
+ tags=tags,
846
+ capabilities=capabilities,
847
+ skills=skills,
848
+ tasks=tasks,
849
+ names=names,
850
+ minScore=minScore,
851
+ maxScore=maxScore,
852
+ include_revoked=include_revoked,
853
+ first=first,
854
+ skip=skip,
855
+ )
939
856
 
940
857
  def revokeFeedback(
941
858
  self,
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent0-sdk
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Python SDK for agent portability, discovery and trust based on ERC-8004
5
- Author-email: Marco De Rossi <marco@ag0.xyz>
5
+ Author-email: Agent0 Team <team@ag0.xyz>
6
6
  License: MIT License
7
7
 
8
8
  Copyright (c) 2025 Marco De Rossi
@@ -86,7 +86,7 @@ Agent0 SDK enables you to:
86
86
  - **Cross-chain registration** - One-line registration with IPFS nodes, Pinata, Filecoin, or HTTP URIs
87
87
  - **Public indexing** - Subgraph indexing both on-chain and IPFS data for fast search and retrieval
88
88
 
89
- **Bug reports & feedback:** GitHub: [Report issues](https://github.com/agent0lab/agent0-py/issues) | Telegram: [@marcoderossi](https://t.me/marcoderossi) | Email: marco@ag0.xyz
89
+ **Bug reports & feedback:** GitHub: [Report issues](https://github.com/agent0lab/agent0-py/issues) | Telegram: [Agent0 channel](https://t.me/agent0kitchen) | Email: team@ag0.xyz
90
90
 
91
91
  ## Installation
92
92
 
@@ -213,19 +213,31 @@ agent_summary = sdk.getAgent("11155111:123")
213
213
  ### 5. Give and Retrieve Feedback
214
214
 
215
215
  ```python
216
- # Prepare feedback (only score is mandatory)
217
- feedback_file = sdk.prepareFeedback(
216
+ # On-chain-only feedback (no off-chain upload, even if IPFS is configured)
217
+ feedback = sdk.giveFeedback(
218
218
  agentId="11155111:123",
219
219
  score=85, # 0-100 (mandatory)
220
- tags=["data_analyst", "finance"], # Optional: tags are now strings (not bytes32)
221
- endpoint="https://example.com/endpoint", # Optional: endpoint URI associated with feedback
222
- capability="tools", # Optional: MCP capability
223
- name="code_generation", # Optional: MCP tool name
224
- skill="python" # Optional: A2A skill
220
+ tag1="data_analyst", # Optional: tags are strings
221
+ tag2="finance",
222
+ endpoint="https://example.com/endpoint", # Optional: saved on-chain
225
223
  )
226
224
 
227
- # Give feedback
228
- feedback = sdk.giveFeedback(agentId="11155111:123", feedbackFile=feedback_file)
225
+ # Rich feedback (optional off-chain file + on-chain fields)
226
+ feedback_file = sdk.prepareFeedbackFile({
227
+ "capability": "tools", # Optional: MCP capability
228
+ "name": "code_generation", # Optional: MCP tool name
229
+ "skill": "python", # Optional: A2A skill
230
+ "text": "Great agent!", # Optional
231
+ })
232
+
233
+ feedback = sdk.giveFeedback(
234
+ agentId="11155111:123",
235
+ score=85,
236
+ tag1="data_analyst",
237
+ tag2="finance",
238
+ endpoint="https://example.com/endpoint",
239
+ feedbackFile=feedback_file, # If provided, requires IPFS configured
240
+ )
229
241
 
230
242
  # Search feedback
231
243
  results = sdk.searchFeedback(
@@ -305,7 +317,7 @@ OASF skills and domains appear in your agent's registration file:
305
317
  {
306
318
  "name": "OASF",
307
319
  "endpoint": "https://github.com/agntcy/oasf/",
308
- "version": "v0.8.0",
320
+ "version": "0.8",
309
321
  "skills": [
310
322
  "advanced_reasoning_planning/strategic_planning",
311
323
  "data_engineering/data_transformation_pipeline"
@@ -0,0 +1,19 @@
1
+ agent0_sdk/__init__.py,sha256=qmIKaQIVmgJk0tOa-kGdj0qxdTKdb6msPnrIPX0SO-g,919
2
+ agent0_sdk/core/agent.py,sha256=NJo0pjigXznP0zDIrpryrL7pMj5bVvZIhfv4abKtUKk,45157
3
+ agent0_sdk/core/contracts.py,sha256=N7lN29zgV_sfValX8515OUm1Lvu6O185gK863dFPCj0,21717
4
+ agent0_sdk/core/endpoint_crawler.py,sha256=QBkFc3tBSQqHj6PtSTZ5D3_HVB00KJZJdxE3uYpI9po,13611
5
+ agent0_sdk/core/feedback_manager.py,sha256=RFl9EYa-GtLahYatIzMf-IOO_X3oLTCk5B6UH8CM_Ic,40704
6
+ agent0_sdk/core/indexer.py,sha256=09NhrPM5dxaBWOsVgBwjzJFpg9Bg9hAastc2YQ0UmHM,70447
7
+ agent0_sdk/core/ipfs_client.py,sha256=17XXMpJgLWhcNUSkmduAZt409c8oJXlj9C_eTGVk-Io,14185
8
+ agent0_sdk/core/models.py,sha256=1BSAX2LVbw0kL_qHK7DxBrIFx8PF3wQvzkzblcQTMUg,12042
9
+ agent0_sdk/core/oasf_validator.py,sha256=ZOtYYzQd7cJj3eJegi7Ch5ydoapJEjaJSxMvwzKSp5o,2980
10
+ agent0_sdk/core/sdk.py,sha256=JhYBNj78JxGhPHeOVl9atdxDZqInnGKy2D5DylO4ImU,38223
11
+ agent0_sdk/core/subgraph_client.py,sha256=VSK9gCB5uYc2OqAVQ659IgeF0N-tXwxPUbv7NK8Kz0U,29575
12
+ agent0_sdk/core/web3_client.py,sha256=h7s-Al3E1xfbb3QNcPvmQBotKJRg23Jm9xot4emr-hU,12283
13
+ agent0_sdk/taxonomies/all_domains.json,sha256=buM8_6mpY8_AMbBIPzM-gtu14Tl30QDmhuQxxrlJU4c,74625
14
+ agent0_sdk/taxonomies/all_skills.json,sha256=WVsutw3fqoj1jfDPru3CyWTr1kc1a5-EhBOWPfXnEi8,47483
15
+ agent0_sdk-1.0.2.dist-info/licenses/LICENSE,sha256=rhZZbZm_Ovz4Oa9LNQ-ms8a1tA36wWh90ZkC0OR7WMw,1072
16
+ agent0_sdk-1.0.2.dist-info/METADATA,sha256=llO2PMTI8QMCcz-Lae0EjqlB9gZ6v_gwQfbGhVxnM24,14565
17
+ agent0_sdk-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ agent0_sdk-1.0.2.dist-info/top_level.txt,sha256=p4520WUKNfhU0lVWJgkrB_jdeUfvHSY3K18k4oYLNfI,11
19
+ agent0_sdk-1.0.2.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- agent0_sdk/__init__.py,sha256=Rtb9_6FYcrqQw0sV7gmox6nj3W3IawaY4LLZriMbb20,919
2
- agent0_sdk/core/agent.py,sha256=L8pDTXUWZ5ZmFgGjBwdheKTvMcGu9SlUP9MQD9TQ0MU,45061
3
- agent0_sdk/core/contracts.py,sha256=euFfjnmpYHNfzzQFvYTvSI-xJGgzFTo2CNny6YaUKTg,21618
4
- agent0_sdk/core/endpoint_crawler.py,sha256=QBkFc3tBSQqHj6PtSTZ5D3_HVB00KJZJdxE3uYpI9po,13611
5
- agent0_sdk/core/feedback_manager.py,sha256=ndGROPISrQvfKjwrWiB_xJ9QIh0cLs4oHh666Ru-A_8,37650
6
- agent0_sdk/core/indexer.py,sha256=BUL4QbbL9sN8eZ1osUdIn_Kgj-MF0SET8RtjbBERQm0,70326
7
- agent0_sdk/core/ipfs_client.py,sha256=fml1ai1BdBkgb95xAkf-ft8QsahV1HL30hBYRz7rQwI,13929
8
- agent0_sdk/core/models.py,sha256=1BSAX2LVbw0kL_qHK7DxBrIFx8PF3wQvzkzblcQTMUg,12042
9
- agent0_sdk/core/oasf_validator.py,sha256=ZOtYYzQd7cJj3eJegi7Ch5ydoapJEjaJSxMvwzKSp5o,2980
10
- agent0_sdk/core/sdk.py,sha256=c9vSKTer50aJr8VcJ6huj6N1pTRwCUtbCxl5G_5oXhk,40955
11
- agent0_sdk/core/subgraph_client.py,sha256=VSK9gCB5uYc2OqAVQ659IgeF0N-tXwxPUbv7NK8Kz0U,29575
12
- agent0_sdk/core/web3_client.py,sha256=h7s-Al3E1xfbb3QNcPvmQBotKJRg23Jm9xot4emr-hU,12283
13
- agent0_sdk/taxonomies/all_domains.json,sha256=buM8_6mpY8_AMbBIPzM-gtu14Tl30QDmhuQxxrlJU4c,74625
14
- agent0_sdk/taxonomies/all_skills.json,sha256=WVsutw3fqoj1jfDPru3CyWTr1kc1a5-EhBOWPfXnEi8,47483
15
- agent0_sdk-1.0.1.dist-info/licenses/LICENSE,sha256=rhZZbZm_Ovz4Oa9LNQ-ms8a1tA36wWh90ZkC0OR7WMw,1072
16
- agent0_sdk-1.0.1.dist-info/METADATA,sha256=WkgYGLtZzuaE57yJqmVS0pyNPcb0ByNfaWNBHfR0qI0,14268
17
- agent0_sdk-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
- agent0_sdk-1.0.1.dist-info/top_level.txt,sha256=p4520WUKNfhU0lVWJgkrB_jdeUfvHSY3K18k4oYLNfI,11
19
- agent0_sdk-1.0.1.dist-info/RECORD,,