genai-otel-instrument 0.1.24__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.
Files changed (69) hide show
  1. genai_otel/__init__.py +132 -0
  2. genai_otel/__version__.py +34 -0
  3. genai_otel/auto_instrument.py +602 -0
  4. genai_otel/cli.py +92 -0
  5. genai_otel/config.py +333 -0
  6. genai_otel/cost_calculator.py +467 -0
  7. genai_otel/cost_enriching_exporter.py +207 -0
  8. genai_otel/cost_enrichment_processor.py +174 -0
  9. genai_otel/evaluation/__init__.py +76 -0
  10. genai_otel/evaluation/bias_detector.py +364 -0
  11. genai_otel/evaluation/config.py +261 -0
  12. genai_otel/evaluation/hallucination_detector.py +525 -0
  13. genai_otel/evaluation/pii_detector.py +356 -0
  14. genai_otel/evaluation/prompt_injection_detector.py +262 -0
  15. genai_otel/evaluation/restricted_topics_detector.py +316 -0
  16. genai_otel/evaluation/span_processor.py +962 -0
  17. genai_otel/evaluation/toxicity_detector.py +406 -0
  18. genai_otel/exceptions.py +17 -0
  19. genai_otel/gpu_metrics.py +516 -0
  20. genai_otel/instrumentors/__init__.py +71 -0
  21. genai_otel/instrumentors/anthropic_instrumentor.py +134 -0
  22. genai_otel/instrumentors/anyscale_instrumentor.py +27 -0
  23. genai_otel/instrumentors/autogen_instrumentor.py +394 -0
  24. genai_otel/instrumentors/aws_bedrock_instrumentor.py +94 -0
  25. genai_otel/instrumentors/azure_openai_instrumentor.py +69 -0
  26. genai_otel/instrumentors/base.py +919 -0
  27. genai_otel/instrumentors/bedrock_agents_instrumentor.py +398 -0
  28. genai_otel/instrumentors/cohere_instrumentor.py +140 -0
  29. genai_otel/instrumentors/crewai_instrumentor.py +311 -0
  30. genai_otel/instrumentors/dspy_instrumentor.py +661 -0
  31. genai_otel/instrumentors/google_ai_instrumentor.py +310 -0
  32. genai_otel/instrumentors/groq_instrumentor.py +106 -0
  33. genai_otel/instrumentors/guardrails_ai_instrumentor.py +510 -0
  34. genai_otel/instrumentors/haystack_instrumentor.py +503 -0
  35. genai_otel/instrumentors/huggingface_instrumentor.py +399 -0
  36. genai_otel/instrumentors/hyperbolic_instrumentor.py +236 -0
  37. genai_otel/instrumentors/instructor_instrumentor.py +425 -0
  38. genai_otel/instrumentors/langchain_instrumentor.py +340 -0
  39. genai_otel/instrumentors/langgraph_instrumentor.py +328 -0
  40. genai_otel/instrumentors/llamaindex_instrumentor.py +36 -0
  41. genai_otel/instrumentors/mistralai_instrumentor.py +315 -0
  42. genai_otel/instrumentors/ollama_instrumentor.py +197 -0
  43. genai_otel/instrumentors/ollama_server_metrics_poller.py +336 -0
  44. genai_otel/instrumentors/openai_agents_instrumentor.py +291 -0
  45. genai_otel/instrumentors/openai_instrumentor.py +260 -0
  46. genai_otel/instrumentors/pydantic_ai_instrumentor.py +362 -0
  47. genai_otel/instrumentors/replicate_instrumentor.py +87 -0
  48. genai_otel/instrumentors/sambanova_instrumentor.py +196 -0
  49. genai_otel/instrumentors/togetherai_instrumentor.py +146 -0
  50. genai_otel/instrumentors/vertexai_instrumentor.py +106 -0
  51. genai_otel/llm_pricing.json +1676 -0
  52. genai_otel/logging_config.py +45 -0
  53. genai_otel/mcp_instrumentors/__init__.py +14 -0
  54. genai_otel/mcp_instrumentors/api_instrumentor.py +144 -0
  55. genai_otel/mcp_instrumentors/base.py +105 -0
  56. genai_otel/mcp_instrumentors/database_instrumentor.py +336 -0
  57. genai_otel/mcp_instrumentors/kafka_instrumentor.py +31 -0
  58. genai_otel/mcp_instrumentors/manager.py +139 -0
  59. genai_otel/mcp_instrumentors/redis_instrumentor.py +31 -0
  60. genai_otel/mcp_instrumentors/vector_db_instrumentor.py +265 -0
  61. genai_otel/metrics.py +148 -0
  62. genai_otel/py.typed +2 -0
  63. genai_otel/server_metrics.py +197 -0
  64. genai_otel_instrument-0.1.24.dist-info/METADATA +1404 -0
  65. genai_otel_instrument-0.1.24.dist-info/RECORD +69 -0
  66. genai_otel_instrument-0.1.24.dist-info/WHEEL +5 -0
  67. genai_otel_instrument-0.1.24.dist-info/entry_points.txt +2 -0
  68. genai_otel_instrument-0.1.24.dist-info/licenses/LICENSE +680 -0
  69. genai_otel_instrument-0.1.24.dist-info/top_level.txt +1 -0
@@ -0,0 +1,525 @@
1
+ """Hallucination Detection for GenAI applications.
2
+
3
+ This module provides hallucination detection capabilities to identify potentially
4
+ false or unsupported claims in LLM responses.
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from .config import HallucinationConfig
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class HallucinationResult:
19
+ """Result of hallucination detection.
20
+
21
+ Attributes:
22
+ has_hallucination: Whether potential hallucination was detected
23
+ hallucination_score: Overall hallucination risk score (0.0-1.0)
24
+ hallucination_indicators: List of indicators found
25
+ factual_claim_count: Count of factual claims detected
26
+ unsupported_claims: Claims that appear unsupported
27
+ hedge_words_count: Count of hedge words (indicating uncertainty)
28
+ citation_count: Count of citations found
29
+ original_text: Original input text
30
+ context_text: Optional context text for fact-checking
31
+ """
32
+
33
+ has_hallucination: bool
34
+ hallucination_score: float = 0.0
35
+ hallucination_indicators: List[str] = field(default_factory=list)
36
+ factual_claim_count: int = 0
37
+ unsupported_claims: List[str] = field(default_factory=list)
38
+ hedge_words_count: int = 0
39
+ citation_count: int = 0
40
+ original_text: Optional[str] = None
41
+ context_text: Optional[str] = None
42
+
43
+
44
+ class HallucinationDetector:
45
+ """Hallucination detector using pattern-based heuristics.
46
+
47
+ This detector identifies potential hallucinations through:
48
+ - Specific factual claim patterns (dates, numbers, names)
49
+ - Hedge word detection (may, might, possibly)
50
+ - Citation presence
51
+ - Contradiction detection with provided context
52
+ - Confidence marker detection
53
+
54
+ Note: This is a heuristic-based detector. For production use cases requiring
55
+ high accuracy, consider integrating dedicated fact-checking services or models.
56
+ """
57
+
58
+ # Patterns for factual claims
59
+ FACTUAL_CLAIM_PATTERNS = {
60
+ "specific_dates": [
61
+ r"\b(?:in|on|during)\s+\d{4}\b", # "in 2020"
62
+ r"\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}\b",
63
+ ],
64
+ "specific_numbers": [
65
+ r"\$?\d+(?:,\d{3})*(?:\.\d+)?\s*(?:million|billion|trillion|thousand)\b", # $45.7 billion, 328 million
66
+ r"\b(?:exactly|precisely|approximately)\s+\d+(?:,\d{3})*(?:\.\d+)?\b",
67
+ r"\b\d{1,3}(?:,\d{3})+\b", # 1,234 employees
68
+ r"\b\d+(?:\.\d+)?%", # 99.9% satisfaction
69
+ ],
70
+ "specific_names": [
71
+ r"\b(?:according\s+to|says|stated|claimed)\s+[A-Z][a-z]+\s+[A-Z][a-z]+\b",
72
+ r"\b[A-Z][a-z]+\s+[A-Z][a-z]+\s+(?:said|wrote|published|discovered)\b",
73
+ ],
74
+ "statistics": [
75
+ r"\b\d+(?:\.\d+)?%\s+of\b",
76
+ r"\b(?:studies?|research|data)\s+(?:shows?|indicates?|suggests?)\s+that\b",
77
+ ],
78
+ }
79
+
80
+ # Hedge words indicating uncertainty
81
+ HEDGE_WORDS = [
82
+ "may",
83
+ "might",
84
+ "possibly",
85
+ "probably",
86
+ "perhaps",
87
+ "likely",
88
+ "could",
89
+ "would",
90
+ "should",
91
+ "seems",
92
+ "appears",
93
+ "suggests",
94
+ "indicates",
95
+ "potentially",
96
+ "allegedly",
97
+ "reportedly",
98
+ ]
99
+
100
+ # High-confidence markers (lack of these may indicate hallucination)
101
+ CONFIDENCE_MARKERS = {
102
+ "citations": [
103
+ r"\[(?:\d+|[a-zA-Z]+)\]", # [1] or [a]
104
+ r"\((?:Source|Ref|Citation):",
105
+ r"https?://\S+", # URLs
106
+ r"\([A-Z][a-z]+(?:\s+et\s+al\.)?,\s+\d{4}\)", # (Smith, 2020) or (Jones et al., 2021)
107
+ r"¹|²|³|⁴|⁵|⁶|⁷|⁸|⁹", # Superscript numbers for footnotes
108
+ ],
109
+ "attribution": [
110
+ r"according\s+to",
111
+ r"based\s+on",
112
+ r"as\s+(?:per|stated|mentioned)\s+(?:in|by)",
113
+ ],
114
+ }
115
+
116
+ # Absolute statement markers
117
+ ABSOLUTE_WORDS = [
118
+ "absolutely",
119
+ "certainly",
120
+ "definitely",
121
+ "always",
122
+ "never",
123
+ "all",
124
+ "none",
125
+ "every",
126
+ ]
127
+
128
+ def __init__(self, config: HallucinationConfig):
129
+ """Initialize hallucination detector.
130
+
131
+ Args:
132
+ config: Hallucination detection configuration
133
+ """
134
+ self.config = config
135
+
136
+ def is_available(self) -> bool:
137
+ """Check if hallucination detector is available.
138
+
139
+ Returns:
140
+ bool: Always True (pattern-based detection always available)
141
+ """
142
+ return True
143
+
144
+ def detect(self, text: str, context: Optional[str] = None) -> HallucinationResult:
145
+ """Detect potential hallucinations in text.
146
+
147
+ Args:
148
+ text: Text to analyze (typically a response)
149
+ context: Optional context text to check against
150
+
151
+ Returns:
152
+ HallucinationResult: Detection results
153
+ """
154
+ if not self.config.enabled:
155
+ return HallucinationResult(
156
+ has_hallucination=False, original_text=text, context_text=context
157
+ )
158
+
159
+ try:
160
+ indicators = []
161
+ unsupported_claims = []
162
+
163
+ # Count factual claims
164
+ factual_claims_count = self._count_factual_claims(text)
165
+
166
+ # Count hedge words (only if enabled in config)
167
+ hedge_count = self._count_hedge_words(text) if self.config.check_hedging else 0
168
+
169
+ # Count citations/attributions (only if enabled in config)
170
+ citation_count = self._count_citations(text) if self.config.check_citations else 0
171
+
172
+ # Calculate hallucination score based on heuristics
173
+ hallucination_score = self._calculate_hallucination_score(
174
+ text, factual_claims_count, hedge_count, citation_count, context
175
+ )
176
+
177
+ # Identify indicators
178
+ if factual_claims_count > 0 and citation_count == 0:
179
+ indicators.append("specific_claims_without_citations")
180
+ unsupported_claims.extend(self._extract_factual_claims(text)[:3])
181
+
182
+ if hedge_count > 3:
183
+ indicators.append("high_uncertainty_language")
184
+
185
+ # Check for absolute statements without citations
186
+ if self._has_absolute_statements(text) and citation_count == 0:
187
+ indicators.append("absolute_statements")
188
+
189
+ # Check for internal contradictions within the text
190
+ if self._check_internal_contradiction(text):
191
+ indicators.append("context_contradiction")
192
+ hallucination_score = min(hallucination_score + 0.3, 1.0)
193
+
194
+ # Check for contradictions with external context
195
+ if context and self._check_context_contradiction(text, context):
196
+ if "context_contradiction" not in indicators:
197
+ indicators.append("context_contradiction")
198
+ hallucination_score = min(hallucination_score + 0.3, 1.0)
199
+
200
+ has_hallucination = hallucination_score >= self.config.threshold
201
+
202
+ return HallucinationResult(
203
+ has_hallucination=has_hallucination,
204
+ hallucination_score=hallucination_score,
205
+ hallucination_indicators=indicators,
206
+ factual_claim_count=factual_claims_count,
207
+ unsupported_claims=unsupported_claims,
208
+ hedge_words_count=hedge_count,
209
+ citation_count=citation_count,
210
+ original_text=text,
211
+ context_text=context,
212
+ )
213
+
214
+ except Exception as e:
215
+ logger.error("Error detecting hallucinations: %s", e, exc_info=True)
216
+ return HallucinationResult(
217
+ has_hallucination=False, original_text=text, context_text=context
218
+ )
219
+
220
+ def _count_factual_claims(self, text: str) -> int:
221
+ """Count factual claims in text.
222
+
223
+ Args:
224
+ text: Text to analyze
225
+
226
+ Returns:
227
+ int: Number of factual claims found
228
+ """
229
+ count = 0
230
+ for claim_type, patterns in self.FACTUAL_CLAIM_PATTERNS.items():
231
+ for pattern in patterns:
232
+ matches = re.findall(pattern, text, re.IGNORECASE)
233
+ count += len(matches)
234
+ return count
235
+
236
+ def _extract_factual_claims(self, text: str) -> List[str]:
237
+ """Extract factual claims from text.
238
+
239
+ Args:
240
+ text: Text to analyze
241
+
242
+ Returns:
243
+ List[str]: List of factual claims found
244
+ """
245
+ claims = []
246
+ for claim_type, patterns in self.FACTUAL_CLAIM_PATTERNS.items():
247
+ for pattern in patterns:
248
+ matches = re.finditer(pattern, text, re.IGNORECASE)
249
+ for match in matches:
250
+ # Get sentence containing the match
251
+ sentence_pattern = r"[^.!?]*" + re.escape(match.group()) + r"[^.!?]*[.!?]"
252
+ sentence_match = re.search(sentence_pattern, text, re.IGNORECASE)
253
+ if sentence_match:
254
+ claims.append(sentence_match.group().strip())
255
+ return claims
256
+
257
+ def _count_hedge_words(self, text: str) -> int:
258
+ """Count hedge words indicating uncertainty.
259
+
260
+ Args:
261
+ text: Text to analyze
262
+
263
+ Returns:
264
+ int: Number of hedge words found
265
+ """
266
+ text_lower = text.lower()
267
+ count = 0
268
+ for hedge_word in self.HEDGE_WORDS:
269
+ # Use word boundaries to avoid partial matches
270
+ pattern = r"\b" + re.escape(hedge_word) + r"\b"
271
+ count += len(re.findall(pattern, text_lower))
272
+ return count
273
+
274
+ def _count_citations(self, text: str) -> int:
275
+ """Count citations and attributions in text.
276
+
277
+ Args:
278
+ text: Text to analyze
279
+
280
+ Returns:
281
+ int: Number of citations found
282
+ """
283
+ count = 0
284
+ for citation_type, patterns in self.CONFIDENCE_MARKERS.items():
285
+ for pattern in patterns:
286
+ matches = re.findall(pattern, text, re.IGNORECASE)
287
+ count += len(matches)
288
+ return count
289
+
290
+ def _calculate_hallucination_score(
291
+ self,
292
+ text: str,
293
+ factual_claims_count: int,
294
+ hedge_count: int,
295
+ citation_count: int,
296
+ context: Optional[str] = None,
297
+ ) -> float:
298
+ """Calculate overall hallucination risk score.
299
+
300
+ Args:
301
+ text: Text to analyze
302
+ factual_claims_count: Number of factual claims
303
+ hedge_count: Number of hedge words
304
+ citation_count: Number of citations
305
+ context: Optional context text
306
+
307
+ Returns:
308
+ float: Hallucination score (0.0-1.0)
309
+ """
310
+ score = 0.0
311
+
312
+ # High factual claims without citations increases score
313
+ if factual_claims_count > 0:
314
+ citation_ratio = citation_count / factual_claims_count
315
+ if citation_ratio == 0:
316
+ score += 0.4 # No citations for factual claims
317
+ elif citation_ratio < 0.3:
318
+ score += 0.2 # Few citations for factual claims
319
+
320
+ # High hedge word count increases score
321
+ if len(text.split()) > 0:
322
+ hedge_density = hedge_count / len(text.split())
323
+ if hedge_density > 0.1: # More than 10% hedge words
324
+ score += 0.3
325
+
326
+ # Very specific claims without attribution
327
+ specific_patterns = [
328
+ r"\b(?:exactly|precisely)\s+\d+",
329
+ r"\bthe\s+fact\s+(?:is|that)\b",
330
+ r"\b(?:definitely|certainly|absolutely)\b",
331
+ ]
332
+ for pattern in specific_patterns:
333
+ if re.search(pattern, text, re.IGNORECASE):
334
+ score += 0.1
335
+
336
+ return min(score, 1.0)
337
+
338
+ def _has_absolute_statements(self, text: str) -> bool:
339
+ """Check if text contains absolute statements.
340
+
341
+ Args:
342
+ text: Text to analyze
343
+
344
+ Returns:
345
+ bool: True if absolute statements found
346
+ """
347
+ text_lower = text.lower()
348
+ for absolute_word in self.ABSOLUTE_WORDS:
349
+ pattern = r"\b" + re.escape(absolute_word) + r"\b"
350
+ if re.search(pattern, text_lower):
351
+ return True
352
+ return False
353
+
354
+ def _check_internal_contradiction(self, text: str) -> bool:
355
+ """Check if text contradicts itself internally.
356
+
357
+ This detects cases where the same subject has different values mentioned.
358
+ For example: "The population is 300 million. However, the population is 400 million."
359
+
360
+ Args:
361
+ text: Text to check
362
+
363
+ Returns:
364
+ bool: True if potential internal contradiction detected
365
+ """
366
+ # Split into sentences
367
+ sentences = re.split(r"[.!?]+", text)
368
+ if len(sentences) < 2:
369
+ return False
370
+
371
+ # Look for contradictory numbers for the same subject
372
+ # Pattern: extract subject + number pairs
373
+ number_pattern = r"(\w+)\s+(?:is|are|was|were|reached?)\s+(\d+(?:,\d{3})*(?:\.\d+)?(?:\s*(?:million|billion|thousand))?)"
374
+
375
+ subject_values = {}
376
+ for sentence in sentences:
377
+ matches = re.findall(number_pattern, sentence.lower())
378
+ for subject, value in matches:
379
+ if subject in subject_values:
380
+ # Same subject mentioned with different value
381
+ if subject_values[subject] != value:
382
+ return True
383
+ else:
384
+ subject_values[subject] = value
385
+
386
+ # Look for contradictory statements with "however", "but", "although"
387
+ contradiction_markers = [r"\bhowever\b", r"\bbut\b", r"\balthough\b", r"\bactually\b"]
388
+ has_contradiction_marker = any(
389
+ re.search(marker, text.lower()) for marker in contradiction_markers
390
+ )
391
+
392
+ if has_contradiction_marker and len(sentences) >= 2:
393
+ # Check if similar topics discussed with different numbers
394
+ numbers_in_text = re.findall(
395
+ r"\d+(?:,\d{3})*(?:\.\d+)?(?:\s*(?:million|billion|thousand))?", text
396
+ )
397
+ if len(numbers_in_text) >= 2:
398
+ # Multiple different numbers with contradiction marker suggests contradiction
399
+ unique_numbers = set(numbers_in_text)
400
+ if len(unique_numbers) >= 2:
401
+ return True
402
+
403
+ return False
404
+
405
+ def _check_context_contradiction(self, text: str, context: str) -> bool:
406
+ """Check if response contradicts provided context.
407
+
408
+ This is a simple heuristic check. For production use, consider
409
+ using dedicated NLI (Natural Language Inference) models.
410
+
411
+ Args:
412
+ text: Text to check
413
+ context: Context to check against
414
+
415
+ Returns:
416
+ bool: True if potential contradiction detected
417
+ """
418
+ # Extract key terms from context
419
+ context_lower = context.lower()
420
+ text_lower = text.lower()
421
+
422
+ # Check for number contradictions with same subject
423
+ # Extract subject + number pairs from both texts
424
+ number_pattern = r"(\w+)\s+(?:is|are|was|were|reached?)\s+(?:exactly|approximately|about)?\s*(\d+(?:,\d{3})*(?:\.\d+)?(?:\s*(?:million|billion|thousand))?)"
425
+
426
+ context_numbers = {}
427
+ text_numbers = {}
428
+
429
+ for match in re.finditer(number_pattern, context_lower):
430
+ subject, number = match.groups()
431
+ context_numbers[subject] = number.strip()
432
+
433
+ for match in re.finditer(number_pattern, text_lower):
434
+ subject, number = match.groups()
435
+ text_numbers[subject] = number.strip()
436
+
437
+ # Check if same subject has different numbers
438
+ for subject in context_numbers:
439
+ if subject in text_numbers and context_numbers[subject] != text_numbers[subject]:
440
+ return True
441
+
442
+ # Simple negation detection
443
+ negation_patterns = [
444
+ r"\bnot\b",
445
+ r"\bno\b",
446
+ r"\bnever\b",
447
+ r"\bn't\b",
448
+ r"\bimpossible\b",
449
+ r"\bfalse\b",
450
+ ]
451
+
452
+ context_has_negation = any(
453
+ re.search(pattern, context_lower) for pattern in negation_patterns
454
+ )
455
+ text_has_negation = any(re.search(pattern, text_lower) for pattern in negation_patterns)
456
+
457
+ # Very simple contradiction check: if context says "not X" and text says "X"
458
+ # This is a naive approach - consider using NLI models for better accuracy
459
+ if context_has_negation != text_has_negation:
460
+ # Extract nouns/entities and check if they appear in both
461
+ context_words = set(re.findall(r"\b[a-z]{4,}\b", context_lower))
462
+ text_words = set(re.findall(r"\b[a-z]{4,}\b", text_lower))
463
+ common_words = context_words & text_words
464
+
465
+ if len(common_words) > 2: # Some overlap in content
466
+ return True
467
+
468
+ return False
469
+
470
+ def analyze_batch(
471
+ self, texts: List[str], contexts: Optional[List[str]] = None
472
+ ) -> List[HallucinationResult]:
473
+ """Analyze multiple texts for hallucinations.
474
+
475
+ Args:
476
+ texts: List of texts to analyze
477
+ contexts: Optional list of context texts (same length as texts)
478
+
479
+ Returns:
480
+ List[HallucinationResult]: Detection results for each text
481
+ """
482
+ results = []
483
+ for i, text in enumerate(texts):
484
+ context = contexts[i] if contexts and i < len(contexts) else None
485
+ results.append(self.detect(text, context))
486
+ return results
487
+
488
+ def get_statistics(self, results: List[HallucinationResult]) -> Dict[str, Any]:
489
+ """Get statistics from multiple detection results.
490
+
491
+ Args:
492
+ results: List of detection results
493
+
494
+ Returns:
495
+ Dict[str, Any]: Statistics including hallucination rates
496
+ """
497
+ total_hallucinations = sum(1 for r in results if r.has_hallucination)
498
+
499
+ # Aggregate indicator counts
500
+ indicator_counts: Dict[str, int] = {}
501
+ for result in results:
502
+ for indicator in result.hallucination_indicators:
503
+ indicator_counts[indicator] = indicator_counts.get(indicator, 0) + 1
504
+
505
+ # Calculate average scores
506
+ avg_score = sum(r.hallucination_score for r in results) / len(results) if results else 0.0
507
+ avg_hedge_count = (
508
+ sum(r.hedge_words_count for r in results) / len(results) if results else 0.0
509
+ )
510
+ avg_citation_count = (
511
+ sum(r.citation_count for r in results) / len(results) if results else 0.0
512
+ )
513
+
514
+ return {
515
+ "total_responses_analyzed": len(results),
516
+ "hallucination_count": total_hallucinations,
517
+ "hallucination_rate": total_hallucinations / len(results) if results else 0.0,
518
+ "indicator_counts": indicator_counts,
519
+ "average_hallucination_score": avg_score,
520
+ "average_hedge_words": avg_hedge_count,
521
+ "average_citations": avg_citation_count,
522
+ "most_common_indicator": (
523
+ max(indicator_counts.items(), key=lambda x: x[1])[0] if indicator_counts else None
524
+ ),
525
+ }