agent0-sdk 0.3rc1__py3-none-any.whl → 0.5__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.
@@ -0,0 +1,98 @@
1
+ """
2
+ OASF taxonomy validation utilities.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ # Cache for loaded taxonomy data
11
+ _skills_cache: Optional[dict] = None
12
+ _domains_cache: Optional[dict] = None
13
+
14
+
15
+ def _get_taxonomy_path(filename: str) -> Path:
16
+ """Get the path to a taxonomy file."""
17
+ # Get the directory where this file is located
18
+ current_dir = Path(__file__).parent
19
+ # Go up one level to agent0_sdk, then into taxonomies
20
+ taxonomy_dir = current_dir.parent / "taxonomies"
21
+ return taxonomy_dir / filename
22
+
23
+
24
+ def _load_skills() -> dict:
25
+ """Load skills taxonomy file with caching."""
26
+ global _skills_cache
27
+ if _skills_cache is None:
28
+ skills_path = _get_taxonomy_path("all_skills.json")
29
+ try:
30
+ with open(skills_path, "r", encoding="utf-8") as f:
31
+ _skills_cache = json.load(f)
32
+ except FileNotFoundError:
33
+ raise FileNotFoundError(
34
+ f"Skills taxonomy file not found: {skills_path}"
35
+ )
36
+ except json.JSONDecodeError as e:
37
+ raise ValueError(
38
+ f"Invalid JSON in skills taxonomy file: {e}"
39
+ )
40
+ return _skills_cache
41
+
42
+
43
+ def _load_domains() -> dict:
44
+ """Load domains taxonomy file with caching."""
45
+ global _domains_cache
46
+ if _domains_cache is None:
47
+ domains_path = _get_taxonomy_path("all_domains.json")
48
+ try:
49
+ with open(domains_path, "r", encoding="utf-8") as f:
50
+ _domains_cache = json.load(f)
51
+ except FileNotFoundError:
52
+ raise FileNotFoundError(
53
+ f"Domains taxonomy file not found: {domains_path}"
54
+ )
55
+ except json.JSONDecodeError as e:
56
+ raise ValueError(
57
+ f"Invalid JSON in domains taxonomy file: {e}"
58
+ )
59
+ return _domains_cache
60
+
61
+
62
+ def validate_skill(slug: str) -> bool:
63
+ """
64
+ Validate if a skill slug exists in the OASF taxonomy.
65
+
66
+ Args:
67
+ slug: The skill slug to validate (e.g., "natural_language_processing/natural_language_generation/summarization")
68
+
69
+ Returns:
70
+ True if the skill exists in the taxonomy, False otherwise
71
+
72
+ Raises:
73
+ FileNotFoundError: If the taxonomy file cannot be found
74
+ ValueError: If the taxonomy file is invalid JSON
75
+ """
76
+ skills_data = _load_skills()
77
+ skills = skills_data.get("skills", {})
78
+ return slug in skills
79
+
80
+
81
+ def validate_domain(slug: str) -> bool:
82
+ """
83
+ Validate if a domain slug exists in the OASF taxonomy.
84
+
85
+ Args:
86
+ slug: The domain slug to validate (e.g., "finance_and_business/investment_services")
87
+
88
+ Returns:
89
+ True if the domain exists in the taxonomy, False otherwise
90
+
91
+ Raises:
92
+ FileNotFoundError: If the taxonomy file cannot be found
93
+ ValueError: If the taxonomy file is invalid JSON
94
+ """
95
+ domains_data = _load_domains()
96
+ domains = domains_data.get("domains", {})
97
+ return slug in domains
98
+
agent0_sdk/core/sdk.py CHANGED
@@ -278,7 +278,12 @@ class SDK:
278
278
  return Agent(sdk=self, registration_file=registration_file)
279
279
 
280
280
  def loadAgent(self, agentId: AgentId) -> Agent:
281
- """Load an existing agent (hydrates from registration file if registered)."""
281
+ """Load an existing agent (hydrates from registration file if registered).
282
+
283
+ Note: Agents can be minted with an empty token URI (e.g. IPFS flow where publish fails).
284
+ In that case we return a partially-hydrated Agent with an empty registration file so the
285
+ caller can resume publishing and set the URI later.
286
+ """
282
287
  # Convert agentId to string if it's an integer
283
288
  agentId = str(agentId)
284
289
 
@@ -292,16 +297,22 @@ class SDK:
292
297
 
293
298
  # Get token URI from contract
294
299
  try:
295
- token_uri = self.web3_client.call_contract(
296
- self.identity_registry, "tokenURI", int(token_id)
300
+ agent_uri = self.web3_client.call_contract(
301
+ self.identity_registry, "tokenURI", int(token_id) # tokenURI is ERC-721 standard, but represents agentURI
297
302
  )
298
303
  except Exception as e:
299
304
  raise ValueError(f"Failed to load agent {agentId}: {e}")
300
305
 
301
- # Load registration file
302
- registration_file = self._load_registration_file(token_uri)
306
+ # Load registration file (or fall back to a minimal file if agent URI is missing)
307
+ registration_file = self._load_registration_file(agent_uri)
303
308
  registration_file.agentId = agentId
304
- registration_file.agentURI = token_uri if token_uri else None
309
+ registration_file.agentURI = agent_uri if agent_uri else None
310
+
311
+ if not agent_uri or not str(agent_uri).strip():
312
+ logger.warning(
313
+ f"Agent {agentId} has no agentURI set on-chain yet. "
314
+ "Returning a partial agent; update info and call registerIPFS() to publish and set URI."
315
+ )
305
316
 
306
317
  # Store registry address for proper JSON generation
307
318
  registry_address = self._registries.get("IDENTITY")
@@ -315,7 +326,13 @@ class SDK:
315
326
  return Agent(sdk=self, registration_file=registration_file)
316
327
 
317
328
  def _load_registration_file(self, uri: str) -> RegistrationFile:
318
- """Load registration file from URI."""
329
+ """Load registration file from URI.
330
+
331
+ If uri is empty/None/whitespace, returns an empty RegistrationFile to allow resume flows.
332
+ """
333
+ if not uri or not str(uri).strip():
334
+ return RegistrationFile()
335
+
319
336
  if uri.startswith("ipfs://"):
320
337
  if not self.ipfs_client:
321
338
  raise ValueError("IPFS client not configured")
@@ -346,21 +363,20 @@ class SDK:
346
363
  # For now, we'll leave it empty
347
364
  registration_file.operators = []
348
365
 
349
- # Hydrate metadata from on-chain (agentWallet, agentName, custom metadata)
366
+ # Hydrate agentWallet from on-chain (now uses getAgentWallet() instead of metadata)
350
367
  agent_id = token_id
351
368
  try:
352
- # Try to get agentWallet from on-chain metadata
353
- wallet_bytes = self.web3_client.call_contract(
354
- self.identity_registry, "getMetadata", agent_id, "agentWallet"
369
+ # Get agentWallet using the new dedicated function
370
+ wallet_address = self.web3_client.call_contract(
371
+ self.identity_registry, "getAgentWallet", agent_id
355
372
  )
356
- if wallet_bytes and len(wallet_bytes) > 0:
357
- wallet_address = "0x" + wallet_bytes.hex()
373
+ if wallet_address and wallet_address != "0x0000000000000000000000000000000000000000":
358
374
  registration_file.walletAddress = wallet_address
359
375
  # If wallet is read from on-chain, use current chain ID
360
376
  # (the chain ID from the registration file might be outdated)
361
377
  registration_file.walletChainId = self.chainId
362
378
  except Exception as e:
363
- # No on-chain wallet, will fall back to registration file
379
+ # No on-chain wallet set, will fall back to registration file
364
380
  pass
365
381
 
366
382
  try:
@@ -904,11 +920,10 @@ class SDK:
904
920
  agentId: "AgentId",
905
921
  feedbackFile: Dict[str, Any],
906
922
  idem: Optional["IdemKey"] = None,
907
- feedbackAuth: Optional[bytes] = None,
908
923
  ) -> "Feedback":
909
924
  """Give feedback (maps 8004 endpoint)."""
910
925
  return self.feedback_manager.giveFeedback(
911
- agentId, feedbackFile, idem, feedbackAuth
926
+ agentId, feedbackFile, idem
912
927
  )
913
928
 
914
929
  def getFeedback(
@@ -30,25 +30,38 @@ class SubgraphClient:
30
30
  Returns:
31
31
  JSON response from the subgraph
32
32
  """
33
- try:
33
+ def _do_query(q: str) -> Dict[str, Any]:
34
34
  response = requests.post(
35
35
  self.subgraph_url,
36
- json={
37
- 'query': query,
38
- 'variables': variables or {}
39
- },
36
+ json={'query': q, 'variables': variables or {}},
40
37
  headers={'Content-Type': 'application/json'},
41
- timeout=10
38
+ timeout=10,
42
39
  )
43
40
  response.raise_for_status()
44
41
  result = response.json()
45
-
46
- # Check for GraphQL errors
47
42
  if 'errors' in result:
48
43
  error_messages = [err.get('message', 'Unknown error') for err in result['errors']]
49
44
  raise ValueError(f"GraphQL errors: {', '.join(error_messages)}")
50
-
51
45
  return result.get('data', {})
46
+
47
+ try:
48
+ return _do_query(query)
49
+ except ValueError as e:
50
+ # Backwards/forwards compatibility for hosted subgraphs:
51
+ # Some deployments still expose `responseUri` instead of `responseURI`.
52
+ msg = str(e)
53
+ if ("has no field" in msg and "responseURI" in msg) and ("responseURI" in query):
54
+ logger.debug("Subgraph schema missing responseURI; retrying query with responseUri")
55
+ return _do_query(query.replace("responseURI", "responseUri"))
56
+ # Some deployments don't expose agentWallet fields on AgentRegistrationFile.
57
+ if (
58
+ "Type `AgentRegistrationFile` has no field `agentWallet`" in msg
59
+ or "Type `AgentRegistrationFile` has no field `agentWalletChainId`" in msg
60
+ ):
61
+ logger.debug("Subgraph schema missing agentWallet fields; retrying query without them")
62
+ q2 = query.replace("agentWalletChainId", "").replace("agentWallet", "")
63
+ return _do_query(q2)
64
+ raise
52
65
  except requests.exceptions.RequestException as e:
53
66
  raise ConnectionError(f"Failed to query subgraph: {e}")
54
67
 
@@ -248,10 +261,12 @@ class SubgraphClient:
248
261
  ) {{
249
262
  id
250
263
  score
264
+ feedbackIndex
251
265
  tag1
252
266
  tag2
267
+ endpoint
253
268
  clientAddress
254
- feedbackUri
269
+ feedbackURI
255
270
  feedbackURIType
256
271
  feedbackHash
257
272
  isRevoked
@@ -276,7 +291,7 @@ class SubgraphClient:
276
291
  responses {{
277
292
  id
278
293
  responder
279
- responseUri
294
+ responseURI
280
295
  responseHash
281
296
  createdAt
282
297
  }}
@@ -400,10 +415,12 @@ class SubgraphClient:
400
415
  id
401
416
  agent { id agentId chainId }
402
417
  clientAddress
418
+ feedbackIndex
403
419
  score
404
420
  tag1
405
421
  tag2
406
- feedbackUri
422
+ endpoint
423
+ feedbackURI
407
424
  feedbackURIType
408
425
  feedbackHash
409
426
  isRevoked
@@ -429,7 +446,7 @@ class SubgraphClient:
429
446
  responses {
430
447
  id
431
448
  responder
432
- responseUri
449
+ responseURI
433
450
  responseHash
434
451
  createdAt
435
452
  }
@@ -548,10 +565,12 @@ class SubgraphClient:
548
565
  id
549
566
  agent {{ id agentId chainId }}
550
567
  clientAddress
568
+ feedbackIndex
551
569
  score
552
570
  tag1
553
571
  tag2
554
- feedbackUri
572
+ endpoint
573
+ feedbackURI
555
574
  feedbackURIType
556
575
  feedbackHash
557
576
  isRevoked
@@ -577,7 +596,7 @@ class SubgraphClient:
577
596
  responses {{
578
597
  id
579
598
  responder
580
- responseUri
599
+ responseURI
581
600
  responseHash
582
601
  createdAt
583
602
  }}
@@ -5,7 +5,7 @@ Web3 integration layer for smart contract interactions.
5
5
  from __future__ import annotations
6
6
 
7
7
  import json
8
- from typing import Any, Dict, List, Optional, Tuple, Union
8
+ from typing import Any, Dict, List, Optional, Tuple, Union, Callable
9
9
 
10
10
  try:
11
11
  from web3 import Web3
@@ -123,22 +123,6 @@ class Web3Client:
123
123
 
124
124
  return event_filter.get_all_entries()
125
125
 
126
- def encodeFeedbackAuth(
127
- self,
128
- agentId: int,
129
- clientAddress: str,
130
- indexLimit: int,
131
- expiry: int,
132
- chainId: int,
133
- identityRegistry: str,
134
- signerAddress: str
135
- ) -> bytes:
136
- """Encode feedback authorization data."""
137
- return self.w3.codec.encode(
138
- ['uint256', 'address', 'uint64', 'uint256', 'uint256', 'address', 'address'],
139
- [agentId, clientAddress, indexLimit, expiry, chainId, identityRegistry, signerAddress]
140
- )
141
-
142
126
  def signMessage(self, message: bytes) -> bytes:
143
127
  """Sign a message with the account's private key."""
144
128
  # Create a SignableMessage from the raw bytes
@@ -190,3 +174,186 @@ class Web3Client:
190
174
  def get_transaction_count(self, address: str) -> int:
191
175
  """Get transaction count (nonce) of an address."""
192
176
  return self.w3.eth.get_transaction_count(address)
177
+
178
+ def encodeEIP712Domain(
179
+ self,
180
+ name: str,
181
+ version: str,
182
+ chain_id: int,
183
+ verifying_contract: str
184
+ ) -> Dict[str, Any]:
185
+ """Encode EIP-712 domain separator.
186
+
187
+ Args:
188
+ name: Contract name
189
+ version: Contract version
190
+ chain_id: Chain ID
191
+ verifying_contract: Contract address
192
+
193
+ Returns:
194
+ Domain separator dictionary
195
+ """
196
+ return {
197
+ "name": name,
198
+ "version": version,
199
+ "chainId": chain_id,
200
+ "verifyingContract": verifying_contract
201
+ }
202
+
203
+ def build_agent_wallet_set_typed_data(
204
+ self,
205
+ agent_id: int,
206
+ new_wallet: str,
207
+ owner: str,
208
+ deadline: int,
209
+ verifying_contract: str,
210
+ chain_id: int,
211
+ ) -> Dict[str, Any]:
212
+ """Build EIP-712 typed data for ERC-8004 IdentityRegistry setAgentWallet.
213
+
214
+ Contract expects:
215
+ - domain: name="ERC8004IdentityRegistry", version="1"
216
+ - primaryType: "AgentWalletSet"
217
+ - message: { agentId, newWallet, owner, deadline }
218
+ """
219
+ domain = self.encodeEIP712Domain(
220
+ name="ERC8004IdentityRegistry",
221
+ version="1",
222
+ chain_id=chain_id,
223
+ verifying_contract=verifying_contract,
224
+ )
225
+
226
+ message_types = {
227
+ "AgentWalletSet": [
228
+ {"name": "agentId", "type": "uint256"},
229
+ {"name": "newWallet", "type": "address"},
230
+ {"name": "owner", "type": "address"},
231
+ {"name": "deadline", "type": "uint256"},
232
+ ]
233
+ }
234
+
235
+ message = {
236
+ "agentId": agent_id,
237
+ "newWallet": new_wallet,
238
+ "owner": owner,
239
+ "deadline": deadline,
240
+ }
241
+
242
+ # eth_account.messages.encode_typed_data expects the "full_message" format
243
+ return {
244
+ "types": {
245
+ "EIP712Domain": [
246
+ {"name": "name", "type": "string"},
247
+ {"name": "version", "type": "string"},
248
+ {"name": "chainId", "type": "uint256"},
249
+ {"name": "verifyingContract", "type": "address"},
250
+ ],
251
+ **message_types,
252
+ },
253
+ "domain": domain,
254
+ "primaryType": "AgentWalletSet",
255
+ "message": message,
256
+ }
257
+
258
+ def sign_typed_data(
259
+ self,
260
+ full_message: Dict[str, Any],
261
+ signer: Union[str, BaseAccount],
262
+ ) -> bytes:
263
+ """Sign EIP-712 typed data with a provided signer (EOA).
264
+
265
+ Args:
266
+ full_message: Typed data dict compatible with encode_typed_data(full_message=...)
267
+ signer: Private key string or eth_account BaseAccount/LocalAccount
268
+
269
+ Returns:
270
+ Signature bytes
271
+ """
272
+ from eth_account.messages import encode_typed_data
273
+
274
+ if isinstance(signer, str):
275
+ acct: BaseAccount = Account.from_key(signer)
276
+ else:
277
+ acct = signer
278
+
279
+ encoded = encode_typed_data(full_message=full_message)
280
+ signed = acct.sign_message(encoded)
281
+ return signed.signature
282
+
283
+ def signEIP712Message(
284
+ self,
285
+ domain: Dict[str, Any],
286
+ message_types: Dict[str, List[Dict[str, str]]],
287
+ message: Dict[str, Any]
288
+ ) -> bytes:
289
+ """Sign an EIP-712 typed message.
290
+
291
+ Args:
292
+ domain: EIP-712 domain separator
293
+ message_types: Type definitions for the message
294
+ message: Message data to sign
295
+
296
+ Returns:
297
+ Signature bytes
298
+ """
299
+ if not self.account:
300
+ raise ValueError("Cannot sign message: SDK is in read-only mode. Provide a signer to enable signing.")
301
+
302
+ from eth_account.messages import encode_typed_data
303
+
304
+ structured_data = {
305
+ "types": {
306
+ "EIP712Domain": [
307
+ {"name": "name", "type": "string"},
308
+ {"name": "version", "type": "string"},
309
+ {"name": "chainId", "type": "uint256"},
310
+ {"name": "verifyingContract", "type": "address"}
311
+ ],
312
+ **message_types
313
+ },
314
+ "domain": domain,
315
+ "primaryType": list(message_types.keys())[0] if message_types else "Message",
316
+ "message": message
317
+ }
318
+
319
+ encoded = encode_typed_data(full_message=structured_data)
320
+ signed = self.account.sign_message(encoded)
321
+ return signed.signature
322
+
323
+ def verifyEIP712Signature(
324
+ self,
325
+ domain: Dict[str, Any],
326
+ message_types: Dict[str, List[Dict[str, str]]],
327
+ message: Dict[str, Any],
328
+ signature: bytes
329
+ ) -> str:
330
+ """Verify an EIP-712 signature and recover the signer address.
331
+
332
+ Args:
333
+ domain: EIP-712 domain separator
334
+ message_types: Type definitions for the message
335
+ message: Message data that was signed
336
+ signature: Signature bytes to verify
337
+
338
+ Returns:
339
+ Recovered signer address
340
+ """
341
+ from eth_account.messages import encode_typed_data
342
+
343
+ structured_data = {
344
+ "types": {
345
+ "EIP712Domain": [
346
+ {"name": "name", "type": "string"},
347
+ {"name": "version", "type": "string"},
348
+ {"name": "chainId", "type": "uint256"},
349
+ {"name": "verifyingContract", "type": "address"}
350
+ ],
351
+ **message_types
352
+ },
353
+ "domain": domain,
354
+ "primaryType": list(message_types.keys())[0] if message_types else "Message",
355
+ "message": message
356
+ }
357
+
358
+ encoded = encode_typed_data(full_message=structured_data)
359
+ return self.w3.eth.account.recover_message(encoded, signature=signature)