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.
@@ -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 []