agent0-sdk 1.0.1__py3-none-any.whl → 1.2.0__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 +1 -1
- agent0_sdk/core/agent.py +10 -9
- agent0_sdk/core/contracts.py +23 -8
- agent0_sdk/core/feedback_manager.py +181 -124
- agent0_sdk/core/indexer.py +16 -15
- agent0_sdk/core/ipfs_client.py +8 -6
- agent0_sdk/core/models.py +5 -3
- agent0_sdk/core/sdk.py +64 -147
- agent0_sdk/core/subgraph_client.py +22 -27
- agent0_sdk/core/value_encoding.py +91 -0
- {agent0_sdk-1.0.1.dist-info → agent0_sdk-1.2.0.dist-info}/METADATA +29 -17
- agent0_sdk-1.2.0.dist-info/RECORD +20 -0
- agent0_sdk-1.0.1.dist-info/RECORD +0 -19
- {agent0_sdk-1.0.1.dist-info → agent0_sdk-1.2.0.dist-info}/WHEEL +0 -0
- {agent0_sdk-1.0.1.dist-info → agent0_sdk-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {agent0_sdk-1.0.1.dist-info → agent0_sdk-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
@@ -16,6 +16,7 @@ from .models import (
|
|
|
16
16
|
)
|
|
17
17
|
from .web3_client import Web3Client
|
|
18
18
|
from .ipfs_client import IPFSClient
|
|
19
|
+
from .value_encoding import encode_feedback_value, decode_feedback_value
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
@@ -40,77 +41,66 @@ class FeedbackManager:
|
|
|
40
41
|
self.subgraph_client = subgraph_client
|
|
41
42
|
self.indexer = indexer
|
|
42
43
|
|
|
43
|
-
def
|
|
44
|
-
|
|
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}
|
|
44
|
+
def prepareFeedbackFile(self, input: Dict[str, Any]) -> Dict[str, Any]:
|
|
45
|
+
"""Prepare an off-chain feedback file payload (no on-chain fields).
|
|
96
46
|
|
|
97
|
-
|
|
98
|
-
|
|
47
|
+
This intentionally does NOT attempt to represent on-chain fields like:
|
|
48
|
+
value/tag1/tag2/endpoint (on-chain value), or registry-derived fields.
|
|
99
49
|
|
|
100
|
-
|
|
50
|
+
It may validate/normalize and remove None values.
|
|
51
|
+
"""
|
|
52
|
+
if input is None:
|
|
53
|
+
raise ValueError("prepareFeedbackFile input cannot be None")
|
|
54
|
+
if not isinstance(input, dict):
|
|
55
|
+
raise TypeError(f"prepareFeedbackFile input must be a dict, got {type(input)}")
|
|
56
|
+
|
|
57
|
+
# Shallow copy and strip None values
|
|
58
|
+
out: Dict[str, Any] = {k: v for k, v in dict(input).items() if v is not None}
|
|
59
|
+
|
|
60
|
+
# Minimal normalization for known optional fields
|
|
61
|
+
if "endpoint" in out and out["endpoint"] is not None and not isinstance(out["endpoint"], str):
|
|
62
|
+
out["endpoint"] = str(out["endpoint"])
|
|
63
|
+
if "domain" in out and out["domain"] is not None and not isinstance(out["domain"], str):
|
|
64
|
+
out["domain"] = str(out["domain"])
|
|
65
|
+
|
|
66
|
+
return out
|
|
101
67
|
|
|
102
68
|
def giveFeedback(
|
|
103
69
|
self,
|
|
104
70
|
agentId: AgentId,
|
|
105
|
-
|
|
106
|
-
|
|
71
|
+
value: Union[int, float, str],
|
|
72
|
+
tag1: Optional[str] = None,
|
|
73
|
+
tag2: Optional[str] = None,
|
|
74
|
+
endpoint: Optional[str] = None,
|
|
75
|
+
feedbackFile: Optional[Dict[str, Any]] = None,
|
|
107
76
|
) -> Feedback:
|
|
108
77
|
"""Give feedback (maps 8004 endpoint)."""
|
|
109
|
-
# Parse
|
|
110
|
-
|
|
111
|
-
|
|
78
|
+
# Parse agentId into (chainId, tokenId)
|
|
79
|
+
agent_chain_id: Optional[int] = None
|
|
80
|
+
tokenId: int
|
|
81
|
+
if isinstance(agentId, str) and agentId.startswith("eip155:"):
|
|
82
|
+
parts = agentId.split(":")
|
|
83
|
+
if len(parts) != 3:
|
|
84
|
+
raise ValueError(f"Invalid AgentId (expected eip155:chainId:tokenId): {agentId}")
|
|
85
|
+
agent_chain_id = int(parts[1])
|
|
86
|
+
tokenId = int(parts[2])
|
|
87
|
+
elif isinstance(agentId, str) and ":" in agentId:
|
|
88
|
+
parts = agentId.split(":")
|
|
89
|
+
if len(parts) != 2:
|
|
90
|
+
raise ValueError(f"Invalid AgentId (expected chainId:tokenId): {agentId}")
|
|
91
|
+
agent_chain_id = int(parts[0])
|
|
92
|
+
tokenId = int(parts[1])
|
|
112
93
|
else:
|
|
113
94
|
tokenId = int(agentId)
|
|
95
|
+
agent_chain_id = int(self.web3_client.chain_id)
|
|
96
|
+
|
|
97
|
+
# Ensure we are submitting the tx on the agent's chain
|
|
98
|
+
if int(self.web3_client.chain_id) != int(agent_chain_id):
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Chain mismatch for giveFeedback: agentId={agentId} targets chainId={agent_chain_id}, "
|
|
101
|
+
f"but web3 client is connected to chainId={self.web3_client.chain_id}. "
|
|
102
|
+
f"Initialize the SDK/Web3Client for chainId={agent_chain_id}."
|
|
103
|
+
)
|
|
114
104
|
|
|
115
105
|
# Get client address (the one giving feedback)
|
|
116
106
|
# Keep in checksum format for blockchain calls (web3.py requirement)
|
|
@@ -128,41 +118,105 @@ class FeedbackManager:
|
|
|
128
118
|
except Exception as e:
|
|
129
119
|
raise ValueError(f"Failed to get feedback index: {e}")
|
|
130
120
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
tag1 =
|
|
134
|
-
tag2 =
|
|
135
|
-
|
|
121
|
+
value_raw, value_decimals, _normalized = encode_feedback_value(value)
|
|
122
|
+
|
|
123
|
+
tag1 = tag1 or ""
|
|
124
|
+
tag2 = tag2 or ""
|
|
125
|
+
|
|
126
|
+
feedback_file: Optional[Dict[str, Any]] = feedbackFile
|
|
127
|
+
if feedback_file is not None and not isinstance(feedback_file, dict):
|
|
128
|
+
raise TypeError(f"feedbackFile must be a dict when provided, got {type(feedback_file)}")
|
|
129
|
+
|
|
130
|
+
# Endpoint precedence: explicit arg > file endpoint > empty string
|
|
131
|
+
if endpoint:
|
|
132
|
+
endpoint_onchain = endpoint
|
|
133
|
+
elif feedback_file and isinstance(feedback_file.get("endpoint"), str) and feedback_file.get("endpoint"):
|
|
134
|
+
endpoint_onchain = feedback_file.get("endpoint")
|
|
135
|
+
else:
|
|
136
|
+
endpoint_onchain = ""
|
|
137
|
+
|
|
138
|
+
# If uploading a file and we have an explicit endpoint, inject it for consistency
|
|
139
|
+
if feedback_file is not None and endpoint and isinstance(endpoint, str):
|
|
140
|
+
feedback_file = dict(feedback_file)
|
|
141
|
+
feedback_file["endpoint"] = endpoint
|
|
136
142
|
|
|
137
143
|
# Handle off-chain file storage
|
|
138
144
|
feedbackUri = ""
|
|
139
145
|
feedbackHash = b"\x00" * 32 # Default empty hash
|
|
140
146
|
|
|
141
|
-
if
|
|
142
|
-
|
|
147
|
+
if feedback_file is not None:
|
|
148
|
+
if not self.ipfs_client:
|
|
149
|
+
raise ValueError("feedbackFile was provided, but no IPFS client is configured")
|
|
150
|
+
|
|
151
|
+
# Store an ERC-8004 compliant feedback file on IPFS (explicit opt-in)
|
|
143
152
|
try:
|
|
144
153
|
logger.debug("Storing feedback file on IPFS")
|
|
145
|
-
|
|
154
|
+
# createdAt MUST be present in the off-chain file; use provided value if valid, else now (UTC).
|
|
155
|
+
created_at = feedback_file.get("createdAt")
|
|
156
|
+
if not isinstance(created_at, str) or not created_at:
|
|
157
|
+
created_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
158
|
+
|
|
159
|
+
identity_registry_address = "0x0"
|
|
160
|
+
try:
|
|
161
|
+
if self.identity_registry is not None:
|
|
162
|
+
identity_registry_address = str(getattr(self.identity_registry, "address", "0x0"))
|
|
163
|
+
except Exception:
|
|
164
|
+
identity_registry_address = "0x0"
|
|
165
|
+
|
|
166
|
+
# Remove any user-provided copies of the envelope keys; SDK-owned values must win
|
|
167
|
+
rich = dict(feedback_file)
|
|
168
|
+
for k in [
|
|
169
|
+
"agentRegistry",
|
|
170
|
+
"agentId",
|
|
171
|
+
"clientAddress",
|
|
172
|
+
"createdAt",
|
|
173
|
+
"value",
|
|
174
|
+
"valueDecimals",
|
|
175
|
+
"tag1",
|
|
176
|
+
"tag2",
|
|
177
|
+
"endpoint",
|
|
178
|
+
]:
|
|
179
|
+
rich.pop(k, None)
|
|
180
|
+
|
|
181
|
+
file_for_storage: Dict[str, Any] = {
|
|
182
|
+
# MUST fields (spec)
|
|
183
|
+
"agentRegistry": f"eip155:{agent_chain_id}:{identity_registry_address}",
|
|
184
|
+
"agentId": tokenId,
|
|
185
|
+
"clientAddress": f"eip155:{agent_chain_id}:{clientAddress}",
|
|
186
|
+
"createdAt": created_at,
|
|
187
|
+
# On-chain fields (store raw+decimals for precision)
|
|
188
|
+
"value": str(value_raw),
|
|
189
|
+
"valueDecimals": int(value_decimals),
|
|
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(
|
|
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
|
-
|
|
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
|
-
# Submit to blockchain with new signature: giveFeedback(agentId,
|
|
209
|
+
# Submit to blockchain with new signature: giveFeedback(agentId, value, valueDecimals, tag1, tag2, endpoint, feedbackURI, feedbackHash)
|
|
157
210
|
try:
|
|
158
211
|
txHash = self.web3_client.transact_contract(
|
|
159
212
|
self.reputation_registry,
|
|
160
213
|
"giveFeedback",
|
|
161
214
|
tokenId,
|
|
162
|
-
|
|
215
|
+
value_raw,
|
|
216
|
+
value_decimals,
|
|
163
217
|
tag1,
|
|
164
218
|
tag2,
|
|
165
|
-
|
|
219
|
+
endpoint_onchain,
|
|
166
220
|
feedbackUri,
|
|
167
221
|
feedbackHash
|
|
168
222
|
)
|
|
@@ -176,24 +230,25 @@ class FeedbackManager:
|
|
|
176
230
|
# Create feedback object (address normalization happens in Feedback.create_id)
|
|
177
231
|
feedbackId = Feedback.create_id(agentId, clientAddress, feedbackIndex)
|
|
178
232
|
|
|
233
|
+
ff: Dict[str, Any] = feedback_file or {}
|
|
179
234
|
return Feedback(
|
|
180
235
|
id=feedbackId,
|
|
181
236
|
agentId=agentId,
|
|
182
237
|
reviewer=clientAddress, # create_id normalizes the ID; reviewer field can remain as-is
|
|
183
|
-
|
|
238
|
+
value=decode_feedback_value(value_raw, value_decimals),
|
|
184
239
|
tags=[tag1, tag2] if tag1 or tag2 else [],
|
|
185
|
-
text=
|
|
186
|
-
context=
|
|
187
|
-
proofOfPayment=
|
|
240
|
+
text=ff.get("text"),
|
|
241
|
+
context=ff.get("context"),
|
|
242
|
+
proofOfPayment=ff.get("proofOfPayment"),
|
|
188
243
|
fileURI=feedbackUri if feedbackUri else None,
|
|
189
|
-
endpoint=
|
|
244
|
+
endpoint=endpoint_onchain if endpoint_onchain else None,
|
|
190
245
|
createdAt=int(time.time()),
|
|
191
246
|
isRevoked=False,
|
|
192
247
|
# Off-chain only fields
|
|
193
|
-
capability=
|
|
194
|
-
name=
|
|
195
|
-
skill=
|
|
196
|
-
task=
|
|
248
|
+
capability=ff.get("capability"),
|
|
249
|
+
name=ff.get("name"),
|
|
250
|
+
skill=ff.get("skill"),
|
|
251
|
+
task=ff.get("task")
|
|
197
252
|
)
|
|
198
253
|
|
|
199
254
|
def getFeedback(
|
|
@@ -210,14 +265,14 @@ class FeedbackManager:
|
|
|
210
265
|
except Exception as e:
|
|
211
266
|
logger.debug(f"Indexer/subgraph get_feedback failed, falling back to blockchain: {e}")
|
|
212
267
|
return self._get_feedback_from_blockchain(agentId, clientAddress, feedbackIndex)
|
|
213
|
-
|
|
268
|
+
|
|
214
269
|
if self.subgraph_client:
|
|
215
270
|
try:
|
|
216
271
|
return self._get_feedback_from_subgraph(agentId, clientAddress, feedbackIndex)
|
|
217
272
|
except Exception as e:
|
|
218
273
|
logger.debug(f"Subgraph get feedback failed, falling back to blockchain: {e}")
|
|
219
274
|
return self._get_feedback_from_blockchain(agentId, clientAddress, feedbackIndex)
|
|
220
|
-
|
|
275
|
+
|
|
221
276
|
return self._get_feedback_from_blockchain(agentId, clientAddress, feedbackIndex)
|
|
222
277
|
|
|
223
278
|
def _get_feedback_from_subgraph(
|
|
@@ -267,15 +322,15 @@ class FeedbackManager:
|
|
|
267
322
|
tag1 = feedback_data.get('tag1') or feedback_file.get('tag1')
|
|
268
323
|
tag2 = feedback_data.get('tag2') or feedback_file.get('tag2')
|
|
269
324
|
if isinstance(tag1, str) and tag1:
|
|
270
|
-
|
|
325
|
+
tags.append(tag1)
|
|
271
326
|
if isinstance(tag2, str) and tag2:
|
|
272
|
-
|
|
327
|
+
tags.append(tag2)
|
|
273
328
|
|
|
274
329
|
return Feedback(
|
|
275
330
|
id=Feedback.create_id(agentId, clientAddress, feedbackIndex), # create_id now normalizes
|
|
276
331
|
agentId=agentId,
|
|
277
332
|
reviewer=self.web3_client.normalize_address(clientAddress), # Also normalize reviewer field
|
|
278
|
-
|
|
333
|
+
value=float(feedback_data.get("value")) if feedback_data.get("value") is not None else None,
|
|
279
334
|
tags=tags,
|
|
280
335
|
text=feedback_file.get('text'),
|
|
281
336
|
capability=feedback_file.get('capability'),
|
|
@@ -287,7 +342,8 @@ class FeedbackManager:
|
|
|
287
342
|
'txHash': feedback_file.get('proofOfPaymentTxHash'),
|
|
288
343
|
} if feedback_file.get('proofOfPaymentFromAddress') else None,
|
|
289
344
|
fileURI=feedback_data.get('feedbackURI') or feedback_data.get('feedbackUri'), # Handle both old and new field names
|
|
290
|
-
endpoint
|
|
345
|
+
# Prefer on-chain endpoint; fall back to off-chain file endpoint if missing
|
|
346
|
+
endpoint=feedback_data.get('endpoint') or feedback_file.get('endpoint'),
|
|
291
347
|
createdAt=feedback_data.get('createdAt', int(time.time())),
|
|
292
348
|
answers=answers,
|
|
293
349
|
isRevoked=feedback_data.get('isRevoked', False),
|
|
@@ -322,7 +378,7 @@ class FeedbackManager:
|
|
|
322
378
|
feedbackIndex
|
|
323
379
|
)
|
|
324
380
|
|
|
325
|
-
|
|
381
|
+
value_raw, value_decimals, tag1, tag2, is_revoked = result
|
|
326
382
|
|
|
327
383
|
# Create feedback object (normalize address for consistency)
|
|
328
384
|
normalized_address = self.web3_client.normalize_address(clientAddress)
|
|
@@ -339,7 +395,7 @@ class FeedbackManager:
|
|
|
339
395
|
id=feedbackId,
|
|
340
396
|
agentId=agentId,
|
|
341
397
|
reviewer=normalized_address,
|
|
342
|
-
|
|
398
|
+
value=decode_feedback_value(int(value_raw), int(value_decimals)),
|
|
343
399
|
tags=tags,
|
|
344
400
|
text=None, # Not stored on-chain
|
|
345
401
|
capability=None, # Not stored on-chain
|
|
@@ -363,8 +419,8 @@ class FeedbackManager:
|
|
|
363
419
|
skills: Optional[List[str]] = None,
|
|
364
420
|
tasks: Optional[List[str]] = None,
|
|
365
421
|
names: Optional[List[str]] = None,
|
|
366
|
-
|
|
367
|
-
|
|
422
|
+
minValue: Optional[float] = None,
|
|
423
|
+
maxValue: Optional[float] = None,
|
|
368
424
|
include_revoked: bool = False,
|
|
369
425
|
first: int = 100,
|
|
370
426
|
skip: int = 0,
|
|
@@ -376,14 +432,14 @@ class FeedbackManager:
|
|
|
376
432
|
# This enables future semantic search capabilities
|
|
377
433
|
return self.indexer.search_feedback(
|
|
378
434
|
agentId, clientAddresses, tags, capabilities, skills, tasks, names,
|
|
379
|
-
|
|
435
|
+
minValue, maxValue, include_revoked, first, skip
|
|
380
436
|
)
|
|
381
437
|
|
|
382
438
|
# Fallback: direct subgraph access (if indexer not available)
|
|
383
439
|
if self.subgraph_client:
|
|
384
440
|
return self._search_feedback_subgraph(
|
|
385
441
|
agentId, clientAddresses, tags, capabilities, skills, tasks, names,
|
|
386
|
-
|
|
442
|
+
minValue, maxValue, include_revoked, first, skip
|
|
387
443
|
)
|
|
388
444
|
|
|
389
445
|
# Fallback to blockchain
|
|
@@ -399,7 +455,7 @@ class FeedbackManager:
|
|
|
399
455
|
tag1_filter = tags[0] if tags else ""
|
|
400
456
|
tag2_filter = tags[1] if tags and len(tags) > 1 else ""
|
|
401
457
|
|
|
402
|
-
# Read from blockchain -
|
|
458
|
+
# Read from blockchain - signature returns: (clients, feedbackIndexes, values, valueDecimals, tag1s, tag2s, revokedStatuses)
|
|
403
459
|
result = self.web3_client.call_contract(
|
|
404
460
|
self.reputation_registry,
|
|
405
461
|
"readAllFeedback",
|
|
@@ -410,7 +466,7 @@ class FeedbackManager:
|
|
|
410
466
|
include_revoked
|
|
411
467
|
)
|
|
412
468
|
|
|
413
|
-
clients, feedback_indexes,
|
|
469
|
+
clients, feedback_indexes, values, value_decimals, tag1s, tag2s, revoked_statuses = result
|
|
414
470
|
|
|
415
471
|
# Convert to Feedback objects
|
|
416
472
|
feedbacks = []
|
|
@@ -429,7 +485,7 @@ class FeedbackManager:
|
|
|
429
485
|
id=feedbackId,
|
|
430
486
|
agentId=agentId,
|
|
431
487
|
reviewer=clients[i],
|
|
432
|
-
|
|
488
|
+
value=decode_feedback_value(int(values[i]), int(value_decimals[i])),
|
|
433
489
|
tags=tags_list,
|
|
434
490
|
text=None,
|
|
435
491
|
capability=None,
|
|
@@ -456,8 +512,8 @@ class FeedbackManager:
|
|
|
456
512
|
skills: Optional[List[str]],
|
|
457
513
|
tasks: Optional[List[str]],
|
|
458
514
|
names: Optional[List[str]],
|
|
459
|
-
|
|
460
|
-
|
|
515
|
+
minValue: Optional[float],
|
|
516
|
+
maxValue: Optional[float],
|
|
461
517
|
include_revoked: bool,
|
|
462
518
|
first: int,
|
|
463
519
|
skip: int,
|
|
@@ -472,8 +528,8 @@ class FeedbackManager:
|
|
|
472
528
|
skills=skills,
|
|
473
529
|
tasks=tasks,
|
|
474
530
|
names=names,
|
|
475
|
-
|
|
476
|
-
|
|
531
|
+
minValue=minValue,
|
|
532
|
+
maxValue=maxValue,
|
|
477
533
|
includeRevoked=include_revoked
|
|
478
534
|
)
|
|
479
535
|
|
|
@@ -509,9 +565,9 @@ class FeedbackManager:
|
|
|
509
565
|
tag1 = fb_data.get('tag1') or feedback_file.get('tag1')
|
|
510
566
|
tag2 = fb_data.get('tag2') or feedback_file.get('tag2')
|
|
511
567
|
if isinstance(tag1, str) and tag1:
|
|
512
|
-
|
|
568
|
+
tags_list.append(tag1)
|
|
513
569
|
if isinstance(tag2, str) and tag2:
|
|
514
|
-
|
|
570
|
+
tags_list.append(tag2)
|
|
515
571
|
|
|
516
572
|
# Parse agentId from feedback ID
|
|
517
573
|
feedback_id = fb_data['id']
|
|
@@ -529,7 +585,7 @@ class FeedbackManager:
|
|
|
529
585
|
id=Feedback.create_id(agent_id_str, client_addr, feedback_idx),
|
|
530
586
|
agentId=agent_id_str,
|
|
531
587
|
reviewer=client_addr,
|
|
532
|
-
|
|
588
|
+
value=float(fb_data.get("value")) if fb_data.get("value") is not None else None,
|
|
533
589
|
tags=tags_list,
|
|
534
590
|
text=feedback_file.get('text'),
|
|
535
591
|
capability=feedback_file.get('capability'),
|
|
@@ -702,14 +758,15 @@ class FeedbackManager:
|
|
|
702
758
|
tag2_str
|
|
703
759
|
)
|
|
704
760
|
|
|
705
|
-
count,
|
|
761
|
+
count, summary_value, summary_value_decimals = result
|
|
762
|
+
average_value = decode_feedback_value(int(summary_value), int(summary_value_decimals))
|
|
706
763
|
|
|
707
764
|
# If no grouping requested, return simple summary
|
|
708
765
|
if not groupBy:
|
|
709
766
|
return {
|
|
710
767
|
"agentId": agentId,
|
|
711
768
|
"count": count,
|
|
712
|
-
"
|
|
769
|
+
"averageValue": average_value,
|
|
713
770
|
"filters": {
|
|
714
771
|
"clientAddresses": clientAddresses,
|
|
715
772
|
"tag1": tag1,
|
|
@@ -731,7 +788,7 @@ class FeedbackManager:
|
|
|
731
788
|
return {
|
|
732
789
|
"agentId": agentId,
|
|
733
790
|
"totalCount": count,
|
|
734
|
-
"
|
|
791
|
+
"totalAverageValue": average_value,
|
|
735
792
|
"groupedData": grouped_data,
|
|
736
793
|
"filters": {
|
|
737
794
|
"clientAddresses": clientAddresses,
|
|
@@ -773,15 +830,15 @@ class FeedbackManager:
|
|
|
773
830
|
|
|
774
831
|
# Calculate summary statistics
|
|
775
832
|
count = len(all_feedback)
|
|
776
|
-
|
|
777
|
-
|
|
833
|
+
values = [fb.value for fb in all_feedback if fb.value is not None]
|
|
834
|
+
average_value = sum(values) / len(values) if values else 0.0
|
|
778
835
|
|
|
779
836
|
# If no grouping requested, return simple summary
|
|
780
837
|
if not groupBy:
|
|
781
838
|
return {
|
|
782
839
|
"agentId": agentId,
|
|
783
840
|
"count": count,
|
|
784
|
-
"
|
|
841
|
+
"averageValue": average_value,
|
|
785
842
|
"filters": {
|
|
786
843
|
"clientAddresses": clientAddresses,
|
|
787
844
|
"tag1": tag1,
|
|
@@ -795,7 +852,7 @@ class FeedbackManager:
|
|
|
795
852
|
return {
|
|
796
853
|
"agentId": agentId,
|
|
797
854
|
"totalCount": count,
|
|
798
|
-
"
|
|
855
|
+
"totalAverageValue": average_value,
|
|
799
856
|
"groupedData": grouped_data,
|
|
800
857
|
"filters": {
|
|
801
858
|
"clientAddresses": clientAddresses,
|
|
@@ -816,23 +873,23 @@ class FeedbackManager:
|
|
|
816
873
|
if group_key not in grouped:
|
|
817
874
|
grouped[group_key] = {
|
|
818
875
|
"count": 0,
|
|
819
|
-
"
|
|
820
|
-
"
|
|
821
|
-
"
|
|
876
|
+
"totalValue": 0.0,
|
|
877
|
+
"averageValue": 0.0,
|
|
878
|
+
"values": [],
|
|
822
879
|
"feedback": []
|
|
823
880
|
}
|
|
824
881
|
|
|
825
882
|
# Add feedback to group
|
|
826
883
|
grouped[group_key]["count"] += 1
|
|
827
|
-
if feedback.
|
|
828
|
-
grouped[group_key]["
|
|
829
|
-
grouped[group_key]["
|
|
884
|
+
if feedback.value is not None:
|
|
885
|
+
grouped[group_key]["totalValue"] += float(feedback.value)
|
|
886
|
+
grouped[group_key]["values"].append(float(feedback.value))
|
|
830
887
|
grouped[group_key]["feedback"].append(feedback)
|
|
831
888
|
|
|
832
889
|
# Calculate averages for each group
|
|
833
890
|
for group_data in grouped.values():
|
|
834
891
|
if group_data["count"] > 0:
|
|
835
|
-
group_data["
|
|
892
|
+
group_data["averageValue"] = group_data["totalValue"] / group_data["count"]
|
|
836
893
|
|
|
837
894
|
return grouped
|
|
838
895
|
|
agent0_sdk/core/indexer.py
CHANGED
|
@@ -87,7 +87,7 @@ class AgentIndexer:
|
|
|
87
87
|
def _create_default_embeddings(self):
|
|
88
88
|
"""Create default embeddings model."""
|
|
89
89
|
try:
|
|
90
|
-
from sentence_transformers import SentenceTransformer
|
|
90
|
+
from sentence_transformers import SentenceTransformer # type: ignore[import-not-found]
|
|
91
91
|
return SentenceTransformer('all-MiniLM-L6-v2')
|
|
92
92
|
except ImportError:
|
|
93
93
|
# Return None if sentence-transformers is not available
|
|
@@ -260,7 +260,7 @@ class AgentIndexer:
|
|
|
260
260
|
raise ValueError(f"Failed to get agent data: {e}")
|
|
261
261
|
|
|
262
262
|
# Load registration file
|
|
263
|
-
registration_data = await self._load_registration_data(
|
|
263
|
+
registration_data = await self._load_registration_data(agent_uri)
|
|
264
264
|
|
|
265
265
|
# Create agent summary
|
|
266
266
|
summary = self._create_agent_summary(
|
|
@@ -1082,7 +1082,7 @@ class AgentIndexer:
|
|
|
1082
1082
|
id=Feedback.create_id(agentId, clientAddress, feedbackIndex),
|
|
1083
1083
|
agentId=agentId,
|
|
1084
1084
|
reviewer=self.web3_client.normalize_address(clientAddress),
|
|
1085
|
-
|
|
1085
|
+
value=float(feedback_data.get("value")) if feedback_data.get("value") is not None else None,
|
|
1086
1086
|
tags=tags,
|
|
1087
1087
|
text=feedback_file.get('text'),
|
|
1088
1088
|
capability=feedback_file.get('capability'),
|
|
@@ -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
|
|
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),
|
|
@@ -1112,8 +1113,8 @@ class AgentIndexer:
|
|
|
1112
1113
|
skills: Optional[List[str]] = None,
|
|
1113
1114
|
tasks: Optional[List[str]] = None,
|
|
1114
1115
|
names: Optional[List[str]] = None,
|
|
1115
|
-
|
|
1116
|
-
|
|
1116
|
+
minValue: Optional[float] = None,
|
|
1117
|
+
maxValue: Optional[float] = None,
|
|
1117
1118
|
include_revoked: bool = False,
|
|
1118
1119
|
first: int = 100,
|
|
1119
1120
|
skip: int = 0,
|
|
@@ -1139,7 +1140,7 @@ class AgentIndexer:
|
|
|
1139
1140
|
if subgraph_client:
|
|
1140
1141
|
return self._search_feedback_subgraph(
|
|
1141
1142
|
full_agent_id, clientAddresses, tags, capabilities, skills, tasks, names,
|
|
1142
|
-
|
|
1143
|
+
minValue, maxValue, include_revoked, first, skip, subgraph_client
|
|
1143
1144
|
)
|
|
1144
1145
|
|
|
1145
1146
|
# Fallback not implemented (would require blockchain queries)
|
|
@@ -1155,8 +1156,8 @@ class AgentIndexer:
|
|
|
1155
1156
|
skills: Optional[List[str]],
|
|
1156
1157
|
tasks: Optional[List[str]],
|
|
1157
1158
|
names: Optional[List[str]],
|
|
1158
|
-
|
|
1159
|
-
|
|
1159
|
+
minValue: Optional[float],
|
|
1160
|
+
maxValue: Optional[float],
|
|
1160
1161
|
include_revoked: bool,
|
|
1161
1162
|
first: int,
|
|
1162
1163
|
skip: int,
|
|
@@ -1177,8 +1178,8 @@ class AgentIndexer:
|
|
|
1177
1178
|
skills=skills,
|
|
1178
1179
|
tasks=tasks,
|
|
1179
1180
|
names=names,
|
|
1180
|
-
|
|
1181
|
-
|
|
1181
|
+
minValue=minValue,
|
|
1182
|
+
maxValue=maxValue,
|
|
1182
1183
|
includeRevoked=include_revoked
|
|
1183
1184
|
)
|
|
1184
1185
|
|
|
@@ -1664,7 +1665,7 @@ class AgentIndexer:
|
|
|
1664
1665
|
- updatedAt (timestamp)
|
|
1665
1666
|
- totalFeedback (count)
|
|
1666
1667
|
- name (alphabetical)
|
|
1667
|
-
-
|
|
1668
|
+
- averageValue (reputation, if available)
|
|
1668
1669
|
"""
|
|
1669
1670
|
if not sort or len(sort) == 0:
|
|
1670
1671
|
# Default: sort by createdAt descending (newest first)
|
|
@@ -1699,9 +1700,9 @@ class AgentIndexer:
|
|
|
1699
1700
|
reg_file = agent.get('registrationFile', {})
|
|
1700
1701
|
return reg_file.get('name', '').lower()
|
|
1701
1702
|
|
|
1702
|
-
elif field == '
|
|
1703
|
-
# If reputation search was done,
|
|
1704
|
-
return agent.get('
|
|
1703
|
+
elif field == 'averageValue':
|
|
1704
|
+
# If reputation search was done, averageValue may be available
|
|
1705
|
+
return agent.get('averageValue', 0)
|
|
1705
1706
|
|
|
1706
1707
|
else:
|
|
1707
1708
|
logger.warning(f"Unknown sort field: {field}, defaulting to createdAt")
|