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.
- genai_otel/__init__.py +132 -0
- genai_otel/__version__.py +34 -0
- genai_otel/auto_instrument.py +602 -0
- genai_otel/cli.py +92 -0
- genai_otel/config.py +333 -0
- genai_otel/cost_calculator.py +467 -0
- genai_otel/cost_enriching_exporter.py +207 -0
- genai_otel/cost_enrichment_processor.py +174 -0
- genai_otel/evaluation/__init__.py +76 -0
- genai_otel/evaluation/bias_detector.py +364 -0
- genai_otel/evaluation/config.py +261 -0
- genai_otel/evaluation/hallucination_detector.py +525 -0
- genai_otel/evaluation/pii_detector.py +356 -0
- genai_otel/evaluation/prompt_injection_detector.py +262 -0
- genai_otel/evaluation/restricted_topics_detector.py +316 -0
- genai_otel/evaluation/span_processor.py +962 -0
- genai_otel/evaluation/toxicity_detector.py +406 -0
- genai_otel/exceptions.py +17 -0
- genai_otel/gpu_metrics.py +516 -0
- genai_otel/instrumentors/__init__.py +71 -0
- genai_otel/instrumentors/anthropic_instrumentor.py +134 -0
- genai_otel/instrumentors/anyscale_instrumentor.py +27 -0
- genai_otel/instrumentors/autogen_instrumentor.py +394 -0
- genai_otel/instrumentors/aws_bedrock_instrumentor.py +94 -0
- genai_otel/instrumentors/azure_openai_instrumentor.py +69 -0
- genai_otel/instrumentors/base.py +919 -0
- genai_otel/instrumentors/bedrock_agents_instrumentor.py +398 -0
- genai_otel/instrumentors/cohere_instrumentor.py +140 -0
- genai_otel/instrumentors/crewai_instrumentor.py +311 -0
- genai_otel/instrumentors/dspy_instrumentor.py +661 -0
- genai_otel/instrumentors/google_ai_instrumentor.py +310 -0
- genai_otel/instrumentors/groq_instrumentor.py +106 -0
- genai_otel/instrumentors/guardrails_ai_instrumentor.py +510 -0
- genai_otel/instrumentors/haystack_instrumentor.py +503 -0
- genai_otel/instrumentors/huggingface_instrumentor.py +399 -0
- genai_otel/instrumentors/hyperbolic_instrumentor.py +236 -0
- genai_otel/instrumentors/instructor_instrumentor.py +425 -0
- genai_otel/instrumentors/langchain_instrumentor.py +340 -0
- genai_otel/instrumentors/langgraph_instrumentor.py +328 -0
- genai_otel/instrumentors/llamaindex_instrumentor.py +36 -0
- genai_otel/instrumentors/mistralai_instrumentor.py +315 -0
- genai_otel/instrumentors/ollama_instrumentor.py +197 -0
- genai_otel/instrumentors/ollama_server_metrics_poller.py +336 -0
- genai_otel/instrumentors/openai_agents_instrumentor.py +291 -0
- genai_otel/instrumentors/openai_instrumentor.py +260 -0
- genai_otel/instrumentors/pydantic_ai_instrumentor.py +362 -0
- genai_otel/instrumentors/replicate_instrumentor.py +87 -0
- genai_otel/instrumentors/sambanova_instrumentor.py +196 -0
- genai_otel/instrumentors/togetherai_instrumentor.py +146 -0
- genai_otel/instrumentors/vertexai_instrumentor.py +106 -0
- genai_otel/llm_pricing.json +1676 -0
- genai_otel/logging_config.py +45 -0
- genai_otel/mcp_instrumentors/__init__.py +14 -0
- genai_otel/mcp_instrumentors/api_instrumentor.py +144 -0
- genai_otel/mcp_instrumentors/base.py +105 -0
- genai_otel/mcp_instrumentors/database_instrumentor.py +336 -0
- genai_otel/mcp_instrumentors/kafka_instrumentor.py +31 -0
- genai_otel/mcp_instrumentors/manager.py +139 -0
- genai_otel/mcp_instrumentors/redis_instrumentor.py +31 -0
- genai_otel/mcp_instrumentors/vector_db_instrumentor.py +265 -0
- genai_otel/metrics.py +148 -0
- genai_otel/py.typed +2 -0
- genai_otel/server_metrics.py +197 -0
- genai_otel_instrument-0.1.24.dist-info/METADATA +1404 -0
- genai_otel_instrument-0.1.24.dist-info/RECORD +69 -0
- genai_otel_instrument-0.1.24.dist-info/WHEEL +5 -0
- genai_otel_instrument-0.1.24.dist-info/entry_points.txt +2 -0
- genai_otel_instrument-0.1.24.dist-info/licenses/LICENSE +680 -0
- 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
|
+
}
|