agent0-sdk 0.31__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 +52 -0
- agent0_sdk/core/agent.py +992 -0
- agent0_sdk/core/contracts.py +497 -0
- agent0_sdk/core/endpoint_crawler.py +330 -0
- agent0_sdk/core/feedback_manager.py +1023 -0
- agent0_sdk/core/indexer.py +1754 -0
- agent0_sdk/core/ipfs_client.py +355 -0
- agent0_sdk/core/models.py +313 -0
- agent0_sdk/core/oasf_validator.py +98 -0
- agent0_sdk/core/sdk.py +1045 -0
- agent0_sdk/core/subgraph_client.py +833 -0
- agent0_sdk/core/web3_client.py +192 -0
- agent0_sdk/taxonomies/all_domains.json +1565 -0
- agent0_sdk/taxonomies/all_skills.json +1030 -0
- agent0_sdk-0.31.dist-info/METADATA +367 -0
- agent0_sdk-0.31.dist-info/RECORD +33 -0
- agent0_sdk-0.31.dist-info/WHEEL +5 -0
- agent0_sdk-0.31.dist-info/licenses/LICENSE +22 -0
- agent0_sdk-0.31.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/config.py +46 -0
- tests/conftest.py +22 -0
- tests/discover_test_data.py +445 -0
- tests/test_feedback.py +417 -0
- tests/test_models.py +224 -0
- tests/test_multi_chain.py +588 -0
- tests/test_oasf_management.py +404 -0
- tests/test_real_public_servers.py +103 -0
- tests/test_registration.py +267 -0
- tests/test_registrationIpfs.py +227 -0
- tests/test_sdk.py +240 -0
- tests/test_search.py +415 -0
- tests/test_transfer.py +255 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subgraph client for querying The Graph network.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SubgraphClient:
|
|
16
|
+
"""Client for querying the subgraph GraphQL API."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, subgraph_url: str):
|
|
19
|
+
"""Initialize subgraph client."""
|
|
20
|
+
self.subgraph_url = subgraph_url
|
|
21
|
+
|
|
22
|
+
def query(self, query: str, variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
23
|
+
"""
|
|
24
|
+
Execute a GraphQL query against the subgraph.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
query: GraphQL query string
|
|
28
|
+
variables: Optional variables for the query
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
JSON response from the subgraph
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
response = requests.post(
|
|
35
|
+
self.subgraph_url,
|
|
36
|
+
json={
|
|
37
|
+
'query': query,
|
|
38
|
+
'variables': variables or {}
|
|
39
|
+
},
|
|
40
|
+
headers={'Content-Type': 'application/json'},
|
|
41
|
+
timeout=10
|
|
42
|
+
)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
result = response.json()
|
|
45
|
+
|
|
46
|
+
# Check for GraphQL errors
|
|
47
|
+
if 'errors' in result:
|
|
48
|
+
error_messages = [err.get('message', 'Unknown error') for err in result['errors']]
|
|
49
|
+
raise ValueError(f"GraphQL errors: {', '.join(error_messages)}")
|
|
50
|
+
|
|
51
|
+
return result.get('data', {})
|
|
52
|
+
except requests.exceptions.RequestException as e:
|
|
53
|
+
raise ConnectionError(f"Failed to query subgraph: {e}")
|
|
54
|
+
|
|
55
|
+
def get_agents(
|
|
56
|
+
self,
|
|
57
|
+
where: Optional[Dict[str, Any]] = None,
|
|
58
|
+
first: int = 100,
|
|
59
|
+
skip: int = 0,
|
|
60
|
+
order_by: str = "createdAt",
|
|
61
|
+
order_direction: str = "desc",
|
|
62
|
+
include_registration_file: bool = True
|
|
63
|
+
) -> List[Dict[str, Any]]:
|
|
64
|
+
"""
|
|
65
|
+
Query agents from the subgraph.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
where: Filter conditions
|
|
69
|
+
first: Number of results to return
|
|
70
|
+
skip: Number of results to skip
|
|
71
|
+
order_by: Field to order by
|
|
72
|
+
order_direction: Sort direction (asc/desc)
|
|
73
|
+
include_registration_file: Whether to include full registration file data
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of agent records
|
|
77
|
+
"""
|
|
78
|
+
# Build WHERE clause
|
|
79
|
+
where_clause = ""
|
|
80
|
+
if where:
|
|
81
|
+
conditions = []
|
|
82
|
+
for key, value in where.items():
|
|
83
|
+
if isinstance(value, bool):
|
|
84
|
+
conditions.append(f"{key}: {str(value).lower()}")
|
|
85
|
+
elif isinstance(value, str):
|
|
86
|
+
conditions.append(f'{key}: "{value}"')
|
|
87
|
+
elif isinstance(value, (int, float)):
|
|
88
|
+
conditions.append(f"{key}: {value}")
|
|
89
|
+
elif isinstance(value, list):
|
|
90
|
+
conditions.append(f"{key}: {json.dumps(value)}")
|
|
91
|
+
if conditions:
|
|
92
|
+
where_clause = f"where: {{ {', '.join(conditions)} }}"
|
|
93
|
+
|
|
94
|
+
# Build registration file fragment
|
|
95
|
+
reg_file_fragment = ""
|
|
96
|
+
if include_registration_file:
|
|
97
|
+
reg_file_fragment = """
|
|
98
|
+
registrationFile {
|
|
99
|
+
id
|
|
100
|
+
agentId
|
|
101
|
+
name
|
|
102
|
+
description
|
|
103
|
+
image
|
|
104
|
+
active
|
|
105
|
+
x402support
|
|
106
|
+
supportedTrusts
|
|
107
|
+
mcpEndpoint
|
|
108
|
+
mcpVersion
|
|
109
|
+
a2aEndpoint
|
|
110
|
+
a2aVersion
|
|
111
|
+
ens
|
|
112
|
+
did
|
|
113
|
+
agentWallet
|
|
114
|
+
agentWalletChainId
|
|
115
|
+
mcpTools
|
|
116
|
+
mcpPrompts
|
|
117
|
+
mcpResources
|
|
118
|
+
a2aSkills
|
|
119
|
+
createdAt
|
|
120
|
+
}
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
query = f"""
|
|
124
|
+
{{
|
|
125
|
+
agents(
|
|
126
|
+
{where_clause}
|
|
127
|
+
first: {first}
|
|
128
|
+
skip: {skip}
|
|
129
|
+
orderBy: {order_by}
|
|
130
|
+
orderDirection: {order_direction}
|
|
131
|
+
) {{
|
|
132
|
+
id
|
|
133
|
+
chainId
|
|
134
|
+
agentId
|
|
135
|
+
agentURI
|
|
136
|
+
agentURIType
|
|
137
|
+
owner
|
|
138
|
+
operators
|
|
139
|
+
totalFeedback
|
|
140
|
+
createdAt
|
|
141
|
+
updatedAt
|
|
142
|
+
lastActivity
|
|
143
|
+
{reg_file_fragment}
|
|
144
|
+
}}
|
|
145
|
+
}}
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
result = self.query(query)
|
|
149
|
+
return result.get('agents', [])
|
|
150
|
+
|
|
151
|
+
def get_agent_by_id(self, agent_id: str, include_registration_file: bool = True) -> Optional[Dict[str, Any]]:
|
|
152
|
+
"""
|
|
153
|
+
Get a specific agent by ID.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
agent_id: Agent ID in format "chainId:tokenId"
|
|
157
|
+
include_registration_file: Whether to include full registration file data
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Agent record or None if not found
|
|
161
|
+
"""
|
|
162
|
+
# Build registration file fragment
|
|
163
|
+
reg_file_fragment = ""
|
|
164
|
+
if include_registration_file:
|
|
165
|
+
reg_file_fragment = """
|
|
166
|
+
registrationFile {
|
|
167
|
+
id
|
|
168
|
+
agentId
|
|
169
|
+
name
|
|
170
|
+
description
|
|
171
|
+
image
|
|
172
|
+
active
|
|
173
|
+
x402support
|
|
174
|
+
supportedTrusts
|
|
175
|
+
mcpEndpoint
|
|
176
|
+
mcpVersion
|
|
177
|
+
a2aEndpoint
|
|
178
|
+
a2aVersion
|
|
179
|
+
ens
|
|
180
|
+
did
|
|
181
|
+
agentWallet
|
|
182
|
+
agentWalletChainId
|
|
183
|
+
mcpTools
|
|
184
|
+
mcpPrompts
|
|
185
|
+
mcpResources
|
|
186
|
+
a2aSkills
|
|
187
|
+
createdAt
|
|
188
|
+
}
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
query = f"""
|
|
192
|
+
{{
|
|
193
|
+
agent(id: "{agent_id}") {{
|
|
194
|
+
id
|
|
195
|
+
chainId
|
|
196
|
+
agentId
|
|
197
|
+
agentURI
|
|
198
|
+
agentURIType
|
|
199
|
+
owner
|
|
200
|
+
operators
|
|
201
|
+
totalFeedback
|
|
202
|
+
createdAt
|
|
203
|
+
updatedAt
|
|
204
|
+
lastActivity
|
|
205
|
+
{reg_file_fragment}
|
|
206
|
+
}}
|
|
207
|
+
}}
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
result = self.query(query)
|
|
211
|
+
agent = result.get('agent')
|
|
212
|
+
|
|
213
|
+
if agent is None:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
return agent
|
|
217
|
+
|
|
218
|
+
def get_feedback_for_agent(
|
|
219
|
+
self,
|
|
220
|
+
agent_id: str,
|
|
221
|
+
first: int = 100,
|
|
222
|
+
skip: int = 0,
|
|
223
|
+
include_revoked: bool = False
|
|
224
|
+
) -> List[Dict[str, Any]]:
|
|
225
|
+
"""
|
|
226
|
+
Get feedback for a specific agent.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
agent_id: Agent ID in format "chainId:tokenId"
|
|
230
|
+
first: Number of results to return
|
|
231
|
+
skip: Number of results to skip
|
|
232
|
+
include_revoked: Whether to include revoked feedback
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List of feedback records
|
|
236
|
+
"""
|
|
237
|
+
query = f"""
|
|
238
|
+
{{
|
|
239
|
+
agent(id: "{agent_id}") {{
|
|
240
|
+
id
|
|
241
|
+
agentId
|
|
242
|
+
feedback(
|
|
243
|
+
first: {first}
|
|
244
|
+
skip: {skip}
|
|
245
|
+
where: {{ isRevoked: {'false' if not include_revoked else 'true'} }}
|
|
246
|
+
orderBy: createdAt
|
|
247
|
+
orderDirection: desc
|
|
248
|
+
) {{
|
|
249
|
+
id
|
|
250
|
+
score
|
|
251
|
+
tag1
|
|
252
|
+
tag2
|
|
253
|
+
clientAddress
|
|
254
|
+
feedbackUri
|
|
255
|
+
feedbackURIType
|
|
256
|
+
feedbackHash
|
|
257
|
+
isRevoked
|
|
258
|
+
createdAt
|
|
259
|
+
revokedAt
|
|
260
|
+
feedbackFile {{
|
|
261
|
+
id
|
|
262
|
+
text
|
|
263
|
+
capability
|
|
264
|
+
name
|
|
265
|
+
skill
|
|
266
|
+
task
|
|
267
|
+
context
|
|
268
|
+
proofOfPaymentFromAddress
|
|
269
|
+
proofOfPaymentToAddress
|
|
270
|
+
proofOfPaymentChainId
|
|
271
|
+
proofOfPaymentTxHash
|
|
272
|
+
tag1
|
|
273
|
+
tag2
|
|
274
|
+
createdAt
|
|
275
|
+
}}
|
|
276
|
+
responses {{
|
|
277
|
+
id
|
|
278
|
+
responder
|
|
279
|
+
responseUri
|
|
280
|
+
responseHash
|
|
281
|
+
createdAt
|
|
282
|
+
}}
|
|
283
|
+
}}
|
|
284
|
+
}}
|
|
285
|
+
}}
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
result = self.query(query)
|
|
289
|
+
agent = result.get('agent')
|
|
290
|
+
|
|
291
|
+
if agent is None:
|
|
292
|
+
return []
|
|
293
|
+
|
|
294
|
+
return agent.get('feedback', [])
|
|
295
|
+
|
|
296
|
+
def get_agent_stats(self, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
297
|
+
"""
|
|
298
|
+
Get statistics for a specific agent.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
agent_id: Agent ID in format "chainId:tokenId"
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Agent statistics or None if not found
|
|
305
|
+
"""
|
|
306
|
+
query = f"""
|
|
307
|
+
{{
|
|
308
|
+
agentStats(id: "{agent_id}") {{
|
|
309
|
+
agent {{
|
|
310
|
+
id
|
|
311
|
+
agentId
|
|
312
|
+
}}
|
|
313
|
+
totalFeedback
|
|
314
|
+
averageScore
|
|
315
|
+
scoreDistribution
|
|
316
|
+
totalValidations
|
|
317
|
+
completedValidations
|
|
318
|
+
averageValidationScore
|
|
319
|
+
lastActivity
|
|
320
|
+
updatedAt
|
|
321
|
+
}}
|
|
322
|
+
}}
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
result = self.query(query)
|
|
326
|
+
return result.get('agentStats')
|
|
327
|
+
|
|
328
|
+
def get_protocol_stats(self, chain_id: int) -> Optional[Dict[str, Any]]:
|
|
329
|
+
"""
|
|
330
|
+
Get statistics for a specific protocol/chain.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
chain_id: Chain ID
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Protocol statistics or None if not found
|
|
337
|
+
"""
|
|
338
|
+
query = f"""
|
|
339
|
+
{{
|
|
340
|
+
protocol(id: "{chain_id}") {{
|
|
341
|
+
id
|
|
342
|
+
chainId
|
|
343
|
+
name
|
|
344
|
+
identityRegistry
|
|
345
|
+
reputationRegistry
|
|
346
|
+
validationRegistry
|
|
347
|
+
totalAgents
|
|
348
|
+
totalFeedback
|
|
349
|
+
totalValidations
|
|
350
|
+
agents
|
|
351
|
+
tags
|
|
352
|
+
trustModels
|
|
353
|
+
createdAt
|
|
354
|
+
updatedAt
|
|
355
|
+
}}
|
|
356
|
+
}}
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
result = self.query(query)
|
|
360
|
+
return result.get('protocol')
|
|
361
|
+
|
|
362
|
+
def get_global_stats(self) -> Optional[Dict[str, Any]]:
|
|
363
|
+
"""
|
|
364
|
+
Get global statistics across all chains.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Global statistics or None if not found
|
|
368
|
+
"""
|
|
369
|
+
query = """
|
|
370
|
+
{
|
|
371
|
+
globalStats(id: "stats") {
|
|
372
|
+
totalAgents
|
|
373
|
+
totalFeedback
|
|
374
|
+
totalValidations
|
|
375
|
+
totalProtocols
|
|
376
|
+
agents
|
|
377
|
+
tags
|
|
378
|
+
createdAt
|
|
379
|
+
updatedAt
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
result = self.query(query)
|
|
385
|
+
return result.get('globalStats')
|
|
386
|
+
|
|
387
|
+
def get_feedback_by_id(self, feedback_id: str) -> Optional[Dict[str, Any]]:
|
|
388
|
+
"""
|
|
389
|
+
Get a specific feedback entry by ID with responses.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
feedback_id: Feedback ID in format "chainId:agentId:clientAddress:feedbackIndex"
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Feedback record with nested feedbackFile and responses, or None if not found
|
|
396
|
+
"""
|
|
397
|
+
query = """
|
|
398
|
+
query GetFeedbackById($feedbackId: ID!) {
|
|
399
|
+
feedback(id: $feedbackId) {
|
|
400
|
+
id
|
|
401
|
+
agent { id agentId chainId }
|
|
402
|
+
clientAddress
|
|
403
|
+
score
|
|
404
|
+
tag1
|
|
405
|
+
tag2
|
|
406
|
+
feedbackUri
|
|
407
|
+
feedbackURIType
|
|
408
|
+
feedbackHash
|
|
409
|
+
isRevoked
|
|
410
|
+
createdAt
|
|
411
|
+
revokedAt
|
|
412
|
+
feedbackFile {
|
|
413
|
+
id
|
|
414
|
+
feedbackId
|
|
415
|
+
text
|
|
416
|
+
capability
|
|
417
|
+
name
|
|
418
|
+
skill
|
|
419
|
+
task
|
|
420
|
+
context
|
|
421
|
+
proofOfPaymentFromAddress
|
|
422
|
+
proofOfPaymentToAddress
|
|
423
|
+
proofOfPaymentChainId
|
|
424
|
+
proofOfPaymentTxHash
|
|
425
|
+
tag1
|
|
426
|
+
tag2
|
|
427
|
+
createdAt
|
|
428
|
+
}
|
|
429
|
+
responses {
|
|
430
|
+
id
|
|
431
|
+
responder
|
|
432
|
+
responseUri
|
|
433
|
+
responseHash
|
|
434
|
+
createdAt
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
"""
|
|
439
|
+
variables = {"feedbackId": feedback_id}
|
|
440
|
+
result = self.query(query, variables)
|
|
441
|
+
return result.get('feedback')
|
|
442
|
+
|
|
443
|
+
def search_feedback(
|
|
444
|
+
self,
|
|
445
|
+
params: Any, # SearchFeedbackParams
|
|
446
|
+
first: int = 100,
|
|
447
|
+
skip: int = 0,
|
|
448
|
+
order_by: str = "createdAt",
|
|
449
|
+
order_direction: str = "desc",
|
|
450
|
+
) -> List[Dict[str, Any]]:
|
|
451
|
+
"""
|
|
452
|
+
Search for feedback entries with filtering.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
params: SearchFeedbackParams object with filter criteria
|
|
456
|
+
first: Number of results to return
|
|
457
|
+
skip: Number of results to skip
|
|
458
|
+
order_by: Field to order by
|
|
459
|
+
order_direction: Sort direction (asc/desc)
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
List of feedback records with nested feedbackFile and responses
|
|
463
|
+
"""
|
|
464
|
+
# Build WHERE clause from params
|
|
465
|
+
where_conditions = []
|
|
466
|
+
|
|
467
|
+
if params.agents is not None and len(params.agents) > 0:
|
|
468
|
+
agent_ids = [f'"{aid}"' for aid in params.agents]
|
|
469
|
+
where_conditions.append(f'agent_in: [{", ".join(agent_ids)}]')
|
|
470
|
+
|
|
471
|
+
if params.reviewers is not None and len(params.reviewers) > 0:
|
|
472
|
+
reviewers = [f'"{addr}"' for addr in params.reviewers]
|
|
473
|
+
where_conditions.append(f'clientAddress_in: [{", ".join(reviewers)}]')
|
|
474
|
+
|
|
475
|
+
if not params.includeRevoked:
|
|
476
|
+
where_conditions.append('isRevoked: false')
|
|
477
|
+
|
|
478
|
+
# Build all non-tag conditions first
|
|
479
|
+
non_tag_conditions = list(where_conditions)
|
|
480
|
+
where_conditions = non_tag_conditions
|
|
481
|
+
|
|
482
|
+
# Handle tag filtering separately - it needs to be at the top level
|
|
483
|
+
tag_filter_condition = None
|
|
484
|
+
if params.tags is not None and len(params.tags) > 0:
|
|
485
|
+
# Tag search: any of the tags must match in tag1 OR tag2
|
|
486
|
+
# Tags are now stored as human-readable strings in the subgraph
|
|
487
|
+
|
|
488
|
+
# Build complete condition with all filters for each tag alternative
|
|
489
|
+
# For each tag, create two alternatives: matching tag1 OR matching tag2
|
|
490
|
+
tag_where_items = []
|
|
491
|
+
for tag in params.tags:
|
|
492
|
+
# For tag1 match
|
|
493
|
+
all_conditions_tag1 = non_tag_conditions + [f'tag1: "{tag}"']
|
|
494
|
+
tag_where_items.append(", ".join(all_conditions_tag1))
|
|
495
|
+
# For tag2 match
|
|
496
|
+
all_conditions_tag2 = non_tag_conditions + [f'tag2: "{tag}"']
|
|
497
|
+
tag_where_items.append(", ".join(all_conditions_tag2))
|
|
498
|
+
|
|
499
|
+
# Join all tag alternatives (each already contains complete filter set)
|
|
500
|
+
tag_filter_condition = ", ".join([f"{{ {item} }}" for item in tag_where_items])
|
|
501
|
+
|
|
502
|
+
if params.minScore is not None:
|
|
503
|
+
where_conditions.append(f'score_gte: {params.minScore}')
|
|
504
|
+
|
|
505
|
+
if params.maxScore is not None:
|
|
506
|
+
where_conditions.append(f'score_lte: {params.maxScore}')
|
|
507
|
+
|
|
508
|
+
# Feedback file filters
|
|
509
|
+
feedback_file_filters = []
|
|
510
|
+
|
|
511
|
+
if params.capabilities is not None and len(params.capabilities) > 0:
|
|
512
|
+
capabilities = [f'"{cap}"' for cap in params.capabilities]
|
|
513
|
+
feedback_file_filters.append(f'capability_in: [{", ".join(capabilities)}]')
|
|
514
|
+
|
|
515
|
+
if params.skills is not None and len(params.skills) > 0:
|
|
516
|
+
skills = [f'"{skill}"' for skill in params.skills]
|
|
517
|
+
feedback_file_filters.append(f'skill_in: [{", ".join(skills)}]')
|
|
518
|
+
|
|
519
|
+
if params.tasks is not None and len(params.tasks) > 0:
|
|
520
|
+
tasks = [f'"{task}"' for task in params.tasks]
|
|
521
|
+
feedback_file_filters.append(f'task_in: [{", ".join(tasks)}]')
|
|
522
|
+
|
|
523
|
+
if params.names is not None and len(params.names) > 0:
|
|
524
|
+
names = [f'"{name}"' for name in params.names]
|
|
525
|
+
feedback_file_filters.append(f'name_in: [{", ".join(names)}]')
|
|
526
|
+
|
|
527
|
+
if feedback_file_filters:
|
|
528
|
+
where_conditions.append(f'feedbackFile_: {{ {", ".join(feedback_file_filters)} }}')
|
|
529
|
+
|
|
530
|
+
# Use tag_filter_condition if tags were provided, otherwise use standard where clause
|
|
531
|
+
if tag_filter_condition:
|
|
532
|
+
# tag_filter_condition already contains properly formatted items: "{ condition1 }, { condition2 }"
|
|
533
|
+
where_clause = f"where: {{ or: [{tag_filter_condition}] }}"
|
|
534
|
+
elif where_conditions:
|
|
535
|
+
where_clause = f"where: {{ {', '.join(where_conditions)} }}"
|
|
536
|
+
else:
|
|
537
|
+
where_clause = ""
|
|
538
|
+
|
|
539
|
+
query = f"""
|
|
540
|
+
{{
|
|
541
|
+
feedbacks(
|
|
542
|
+
{where_clause}
|
|
543
|
+
first: {first}
|
|
544
|
+
skip: {skip}
|
|
545
|
+
orderBy: {order_by}
|
|
546
|
+
orderDirection: {order_direction}
|
|
547
|
+
) {{
|
|
548
|
+
id
|
|
549
|
+
agent {{ id agentId chainId }}
|
|
550
|
+
clientAddress
|
|
551
|
+
score
|
|
552
|
+
tag1
|
|
553
|
+
tag2
|
|
554
|
+
feedbackUri
|
|
555
|
+
feedbackURIType
|
|
556
|
+
feedbackHash
|
|
557
|
+
isRevoked
|
|
558
|
+
createdAt
|
|
559
|
+
revokedAt
|
|
560
|
+
feedbackFile {{
|
|
561
|
+
id
|
|
562
|
+
feedbackId
|
|
563
|
+
text
|
|
564
|
+
capability
|
|
565
|
+
name
|
|
566
|
+
skill
|
|
567
|
+
task
|
|
568
|
+
context
|
|
569
|
+
proofOfPaymentFromAddress
|
|
570
|
+
proofOfPaymentToAddress
|
|
571
|
+
proofOfPaymentChainId
|
|
572
|
+
proofOfPaymentTxHash
|
|
573
|
+
tag1
|
|
574
|
+
tag2
|
|
575
|
+
createdAt
|
|
576
|
+
}}
|
|
577
|
+
responses {{
|
|
578
|
+
id
|
|
579
|
+
responder
|
|
580
|
+
responseUri
|
|
581
|
+
responseHash
|
|
582
|
+
createdAt
|
|
583
|
+
}}
|
|
584
|
+
}}
|
|
585
|
+
}}
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
result = self.query(query)
|
|
589
|
+
return result.get('feedbacks', [])
|
|
590
|
+
|
|
591
|
+
def search_agents_by_reputation(
|
|
592
|
+
self,
|
|
593
|
+
agents: Optional[List[str]] = None,
|
|
594
|
+
tags: Optional[List[str]] = None,
|
|
595
|
+
reviewers: Optional[List[str]] = None,
|
|
596
|
+
capabilities: Optional[List[str]] = None,
|
|
597
|
+
skills: Optional[List[str]] = None,
|
|
598
|
+
tasks: Optional[List[str]] = None,
|
|
599
|
+
names: Optional[List[str]] = None,
|
|
600
|
+
minAverageScore: Optional[int] = None, # 0-100
|
|
601
|
+
includeRevoked: bool = False,
|
|
602
|
+
first: int = 100,
|
|
603
|
+
skip: int = 0,
|
|
604
|
+
order_by: str = "createdAt",
|
|
605
|
+
order_direction: str = "desc",
|
|
606
|
+
) -> List[Dict[str, Any]]:
|
|
607
|
+
"""
|
|
608
|
+
Search agents filtered by reputation criteria.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
agents: List of agent IDs to filter by
|
|
612
|
+
tags: List of tags to filter feedback by
|
|
613
|
+
reviewers: List of reviewer addresses to filter feedback by
|
|
614
|
+
capabilities: List of capabilities to filter feedback by
|
|
615
|
+
skills: List of skills to filter feedback by
|
|
616
|
+
tasks: List of tasks to filter feedback by
|
|
617
|
+
minAverageScore: Minimum average score (0-100) for included agents
|
|
618
|
+
includeRevoked: Whether to include revoked feedback in calculations
|
|
619
|
+
first: Number of results to return
|
|
620
|
+
skip: Number of results to skip
|
|
621
|
+
order_by: Field to order by
|
|
622
|
+
order_direction: Sort direction (asc/desc)
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
List of agents with averageScore field calculated from filtered feedback
|
|
626
|
+
"""
|
|
627
|
+
# Build feedback filter
|
|
628
|
+
feedback_filters = []
|
|
629
|
+
|
|
630
|
+
if not includeRevoked:
|
|
631
|
+
feedback_filters.append('isRevoked: false')
|
|
632
|
+
|
|
633
|
+
if tags is not None and len(tags) > 0:
|
|
634
|
+
# Tags are now stored as human-readable strings in the subgraph
|
|
635
|
+
tag_filter = []
|
|
636
|
+
for tag in tags:
|
|
637
|
+
tag_filter.append(f'{{or: [{{tag1: "{tag}"}}, {{tag2: "{tag}"}}]}}')
|
|
638
|
+
feedback_filters.append(f'or: [{", ".join(tag_filter)}]')
|
|
639
|
+
|
|
640
|
+
if reviewers is not None and len(reviewers) > 0:
|
|
641
|
+
reviewers_list = [f'"{addr}"' for addr in reviewers]
|
|
642
|
+
feedback_filters.append(f'clientAddress_in: [{", ".join(reviewers_list)}]')
|
|
643
|
+
|
|
644
|
+
# Feedback file filters
|
|
645
|
+
feedback_file_filters = []
|
|
646
|
+
|
|
647
|
+
if capabilities is not None and len(capabilities) > 0:
|
|
648
|
+
capabilities_list = [f'"{cap}"' for cap in capabilities]
|
|
649
|
+
feedback_file_filters.append(f'capability_in: [{", ".join(capabilities_list)}]')
|
|
650
|
+
|
|
651
|
+
if skills is not None and len(skills) > 0:
|
|
652
|
+
skills_list = [f'"{skill}"' for skill in skills]
|
|
653
|
+
feedback_file_filters.append(f'skill_in: [{", ".join(skills_list)}]')
|
|
654
|
+
|
|
655
|
+
if tasks is not None and len(tasks) > 0:
|
|
656
|
+
tasks_list = [f'"{task}"' for task in tasks]
|
|
657
|
+
feedback_file_filters.append(f'task_in: [{", ".join(tasks_list)}]')
|
|
658
|
+
|
|
659
|
+
if names is not None and len(names) > 0:
|
|
660
|
+
names_list = [f'"{name}"' for name in names]
|
|
661
|
+
feedback_file_filters.append(f'name_in: [{", ".join(names_list)}]')
|
|
662
|
+
|
|
663
|
+
if feedback_file_filters:
|
|
664
|
+
feedback_filters.append(f'feedbackFile_: {{ {", ".join(feedback_file_filters)} }}')
|
|
665
|
+
|
|
666
|
+
# If we have feedback filters (tags, capabilities, skills, etc.), we need to first
|
|
667
|
+
# query feedback to get agent IDs, then query those agents
|
|
668
|
+
# Otherwise, query agents directly
|
|
669
|
+
if tags or capabilities or skills or tasks or names or reviewers:
|
|
670
|
+
# First, query feedback to get unique agent IDs that have matching feedback
|
|
671
|
+
feedback_where = f"{{ {', '.join(feedback_filters)} }}" if feedback_filters else "{}"
|
|
672
|
+
|
|
673
|
+
feedback_query = f"""
|
|
674
|
+
{{
|
|
675
|
+
feedbacks(
|
|
676
|
+
where: {feedback_where}
|
|
677
|
+
first: 1000
|
|
678
|
+
skip: 0
|
|
679
|
+
) {{
|
|
680
|
+
agent {{
|
|
681
|
+
id
|
|
682
|
+
}}
|
|
683
|
+
}}
|
|
684
|
+
}}
|
|
685
|
+
"""
|
|
686
|
+
|
|
687
|
+
try:
|
|
688
|
+
feedback_result = self.query(feedback_query)
|
|
689
|
+
feedbacks_data = feedback_result.get('feedbacks', [])
|
|
690
|
+
|
|
691
|
+
# Extract unique agent IDs
|
|
692
|
+
agent_ids_set = set()
|
|
693
|
+
for fb in feedbacks_data:
|
|
694
|
+
agent = fb.get('agent', {})
|
|
695
|
+
agent_id = agent.get('id')
|
|
696
|
+
if agent_id:
|
|
697
|
+
agent_ids_set.add(agent_id)
|
|
698
|
+
|
|
699
|
+
if not agent_ids_set:
|
|
700
|
+
# No agents have matching feedback
|
|
701
|
+
return []
|
|
702
|
+
|
|
703
|
+
# Now query only those agents
|
|
704
|
+
agent_ids_list = list(agent_ids_set)
|
|
705
|
+
# Apply any agent filters if specified
|
|
706
|
+
if agents is not None and len(agents) > 0:
|
|
707
|
+
agent_ids_list = [aid for aid in agent_ids_list if aid in agents]
|
|
708
|
+
if not agent_ids_list:
|
|
709
|
+
return []
|
|
710
|
+
|
|
711
|
+
# Query agents (limit to first N based on pagination)
|
|
712
|
+
agent_ids_str = ', '.join([f'"{aid}"' for aid in agent_ids_list])
|
|
713
|
+
agent_where = f"where: {{ id_in: [{agent_ids_str}] }}"
|
|
714
|
+
except Exception as e:
|
|
715
|
+
logger.warning(f"Failed to query feedback for agent IDs: {e}")
|
|
716
|
+
return []
|
|
717
|
+
else:
|
|
718
|
+
# No feedback filters - query agents directly
|
|
719
|
+
# For reputation search, we want agents that have feedback
|
|
720
|
+
# Filter by totalFeedback > 0 to only get agents with feedback
|
|
721
|
+
agent_filters = ['totalFeedback_gt: 0'] # Only agents with feedback (BigInt comparison)
|
|
722
|
+
if agents is not None and len(agents) > 0:
|
|
723
|
+
agent_ids = [f'"{aid}"' for aid in agents]
|
|
724
|
+
agent_filters.append(f'id_in: [{", ".join(agent_ids)}]')
|
|
725
|
+
|
|
726
|
+
agent_where = f"where: {{ {', '.join(agent_filters)} }}"
|
|
727
|
+
|
|
728
|
+
# Build feedback where for agent query (to calculate scores)
|
|
729
|
+
feedback_where_for_agents = f"{{ {', '.join(feedback_filters)} }}" if feedback_filters else "{}"
|
|
730
|
+
|
|
731
|
+
query = f"""
|
|
732
|
+
{{
|
|
733
|
+
agents(
|
|
734
|
+
{agent_where}
|
|
735
|
+
first: {first}
|
|
736
|
+
skip: {skip}
|
|
737
|
+
orderBy: {order_by}
|
|
738
|
+
orderDirection: {order_direction}
|
|
739
|
+
) {{
|
|
740
|
+
id
|
|
741
|
+
chainId
|
|
742
|
+
agentId
|
|
743
|
+
agentURI
|
|
744
|
+
agentURIType
|
|
745
|
+
owner
|
|
746
|
+
operators
|
|
747
|
+
createdAt
|
|
748
|
+
updatedAt
|
|
749
|
+
totalFeedback
|
|
750
|
+
lastActivity
|
|
751
|
+
registrationFile {{
|
|
752
|
+
id
|
|
753
|
+
name
|
|
754
|
+
description
|
|
755
|
+
image
|
|
756
|
+
active
|
|
757
|
+
x402support
|
|
758
|
+
supportedTrusts
|
|
759
|
+
mcpEndpoint
|
|
760
|
+
mcpVersion
|
|
761
|
+
a2aEndpoint
|
|
762
|
+
a2aVersion
|
|
763
|
+
ens
|
|
764
|
+
did
|
|
765
|
+
agentWallet
|
|
766
|
+
agentWalletChainId
|
|
767
|
+
mcpTools
|
|
768
|
+
mcpPrompts
|
|
769
|
+
mcpResources
|
|
770
|
+
a2aSkills
|
|
771
|
+
createdAt
|
|
772
|
+
}}
|
|
773
|
+
feedback(where: {feedback_where_for_agents}) {{
|
|
774
|
+
score
|
|
775
|
+
isRevoked
|
|
776
|
+
feedbackFile {{
|
|
777
|
+
capability
|
|
778
|
+
skill
|
|
779
|
+
task
|
|
780
|
+
name
|
|
781
|
+
}}
|
|
782
|
+
}}
|
|
783
|
+
}}
|
|
784
|
+
}}
|
|
785
|
+
"""
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
result = self.query(query)
|
|
789
|
+
|
|
790
|
+
# Check for GraphQL errors
|
|
791
|
+
if 'errors' in result:
|
|
792
|
+
logger.error(f"GraphQL errors in search_agents_by_reputation: {result['errors']}")
|
|
793
|
+
return []
|
|
794
|
+
|
|
795
|
+
agents_result = result.get('agents', [])
|
|
796
|
+
|
|
797
|
+
# Calculate average scores
|
|
798
|
+
for agent in agents_result:
|
|
799
|
+
feedbacks = agent.get('feedback', [])
|
|
800
|
+
if feedbacks:
|
|
801
|
+
scores = [int(fb['score']) for fb in feedbacks if fb.get('score', 0) > 0]
|
|
802
|
+
if scores:
|
|
803
|
+
avg_score = sum(scores) / len(scores)
|
|
804
|
+
agent['averageScore'] = avg_score
|
|
805
|
+
else:
|
|
806
|
+
agent['averageScore'] = None
|
|
807
|
+
else:
|
|
808
|
+
agent['averageScore'] = None
|
|
809
|
+
|
|
810
|
+
# Filter by minAverageScore
|
|
811
|
+
if minAverageScore is not None:
|
|
812
|
+
agents_result = [
|
|
813
|
+
agent for agent in agents_result
|
|
814
|
+
if agent.get('averageScore') is not None and agent['averageScore'] >= minAverageScore
|
|
815
|
+
]
|
|
816
|
+
|
|
817
|
+
# For reputation search, filter logic:
|
|
818
|
+
# - If specific agents were requested, return them even if averageScore is None
|
|
819
|
+
# (the user explicitly asked for these agents, so return them)
|
|
820
|
+
# - If general search (no specific agents), only return agents with reputation data
|
|
821
|
+
if agents is None or len(agents) == 0:
|
|
822
|
+
# General search - only return agents with reputation
|
|
823
|
+
agents_result = [
|
|
824
|
+
agent for agent in agents_result
|
|
825
|
+
if agent.get('averageScore') is not None
|
|
826
|
+
]
|
|
827
|
+
# else: specific agents requested - return all requested agents (even if averageScore is None)
|
|
828
|
+
|
|
829
|
+
return agents_result
|
|
830
|
+
|
|
831
|
+
except Exception as e:
|
|
832
|
+
logger.warning(f"Subgraph reputation search failed: {e}")
|
|
833
|
+
return []
|