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