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,962 @@
|
|
|
1
|
+
"""OpenTelemetry Span Processor for evaluation and safety features.
|
|
2
|
+
|
|
3
|
+
This module provides a span processor that adds evaluation metrics and safety
|
|
4
|
+
checks to GenAI spans, including PII detection, toxicity detection, bias detection,
|
|
5
|
+
prompt injection detection, restricted topics, and hallucination detection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from opentelemetry import metrics
|
|
12
|
+
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
|
|
13
|
+
from opentelemetry.trace import Status, StatusCode
|
|
14
|
+
|
|
15
|
+
from .bias_detector import BiasDetector
|
|
16
|
+
from .config import (
|
|
17
|
+
BiasConfig,
|
|
18
|
+
HallucinationConfig,
|
|
19
|
+
PIIConfig,
|
|
20
|
+
PromptInjectionConfig,
|
|
21
|
+
RestrictedTopicsConfig,
|
|
22
|
+
ToxicityConfig,
|
|
23
|
+
)
|
|
24
|
+
from .hallucination_detector import HallucinationDetector
|
|
25
|
+
from .pii_detector import PIIDetector
|
|
26
|
+
from .prompt_injection_detector import PromptInjectionDetector
|
|
27
|
+
from .restricted_topics_detector import RestrictedTopicsDetector
|
|
28
|
+
from .toxicity_detector import ToxicityDetector
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EvaluationSpanProcessor(SpanProcessor):
|
|
34
|
+
"""Span processor for evaluation and safety features.
|
|
35
|
+
|
|
36
|
+
This processor analyzes GenAI spans and adds evaluation metrics and safety
|
|
37
|
+
attributes. It runs checks on prompts and responses based on enabled features.
|
|
38
|
+
|
|
39
|
+
Features:
|
|
40
|
+
- PII Detection: Detect and redact personally identifiable information
|
|
41
|
+
- Toxicity Detection: Monitor toxic or harmful content
|
|
42
|
+
- Bias Detection: Detect demographic and other biases
|
|
43
|
+
- Prompt Injection Detection: Protect against prompt injection attacks
|
|
44
|
+
- Restricted Topics: Block sensitive or inappropriate topics
|
|
45
|
+
- Hallucination Detection: Track factual accuracy and groundedness
|
|
46
|
+
|
|
47
|
+
All features are opt-in and configured independently.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
pii_config: Optional[PIIConfig] = None,
|
|
53
|
+
toxicity_config: Optional[ToxicityConfig] = None,
|
|
54
|
+
bias_config: Optional[BiasConfig] = None,
|
|
55
|
+
prompt_injection_config: Optional[PromptInjectionConfig] = None,
|
|
56
|
+
restricted_topics_config: Optional[RestrictedTopicsConfig] = None,
|
|
57
|
+
hallucination_config: Optional[HallucinationConfig] = None,
|
|
58
|
+
):
|
|
59
|
+
"""Initialize evaluation span processor.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
pii_config: PII detection configuration
|
|
63
|
+
toxicity_config: Toxicity detection configuration
|
|
64
|
+
bias_config: Bias detection configuration
|
|
65
|
+
prompt_injection_config: Prompt injection detection configuration
|
|
66
|
+
restricted_topics_config: Restricted topics configuration
|
|
67
|
+
hallucination_config: Hallucination detection configuration
|
|
68
|
+
"""
|
|
69
|
+
super().__init__()
|
|
70
|
+
|
|
71
|
+
# Store configurations
|
|
72
|
+
self.pii_config = pii_config or PIIConfig()
|
|
73
|
+
self.toxicity_config = toxicity_config or ToxicityConfig()
|
|
74
|
+
self.bias_config = bias_config or BiasConfig()
|
|
75
|
+
self.prompt_injection_config = prompt_injection_config or PromptInjectionConfig()
|
|
76
|
+
self.restricted_topics_config = restricted_topics_config or RestrictedTopicsConfig()
|
|
77
|
+
self.hallucination_config = hallucination_config or HallucinationConfig()
|
|
78
|
+
|
|
79
|
+
# Initialize detectors
|
|
80
|
+
self.pii_detector = None
|
|
81
|
+
if self.pii_config.enabled:
|
|
82
|
+
self.pii_detector = PIIDetector(self.pii_config)
|
|
83
|
+
if not self.pii_detector.is_available():
|
|
84
|
+
logger.warning(
|
|
85
|
+
"PII detector not available, PII detection will use fallback patterns"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.toxicity_detector = None
|
|
89
|
+
if self.toxicity_config.enabled:
|
|
90
|
+
self.toxicity_detector = ToxicityDetector(self.toxicity_config)
|
|
91
|
+
if not self.toxicity_detector.is_available():
|
|
92
|
+
logger.warning(
|
|
93
|
+
"Toxicity detector not available, please install either:\n"
|
|
94
|
+
" - Perspective API: pip install google-api-python-client\n"
|
|
95
|
+
" - Detoxify: pip install detoxify"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.bias_detector = None
|
|
99
|
+
if self.bias_config.enabled:
|
|
100
|
+
self.bias_detector = BiasDetector(self.bias_config)
|
|
101
|
+
if not self.bias_detector.is_available():
|
|
102
|
+
logger.warning(
|
|
103
|
+
"Bias detector not available (pattern-based detection always available)"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.prompt_injection_detector = None
|
|
107
|
+
if self.prompt_injection_config.enabled:
|
|
108
|
+
self.prompt_injection_detector = PromptInjectionDetector(self.prompt_injection_config)
|
|
109
|
+
|
|
110
|
+
self.restricted_topics_detector = None
|
|
111
|
+
if self.restricted_topics_config.enabled:
|
|
112
|
+
self.restricted_topics_detector = RestrictedTopicsDetector(
|
|
113
|
+
self.restricted_topics_config
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self.hallucination_detector = None
|
|
117
|
+
if self.hallucination_config.enabled:
|
|
118
|
+
self.hallucination_detector = HallucinationDetector(self.hallucination_config)
|
|
119
|
+
|
|
120
|
+
# Initialize metrics
|
|
121
|
+
meter = metrics.get_meter(__name__)
|
|
122
|
+
|
|
123
|
+
# PII Detection Metrics
|
|
124
|
+
self.pii_detection_counter = meter.create_counter(
|
|
125
|
+
name="genai.evaluation.pii.detections",
|
|
126
|
+
description="Number of PII detections in prompts and responses",
|
|
127
|
+
unit="1",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
self.pii_entity_counter = meter.create_counter(
|
|
131
|
+
name="genai.evaluation.pii.entities",
|
|
132
|
+
description="Number of PII entities detected by type",
|
|
133
|
+
unit="1",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.pii_blocked_counter = meter.create_counter(
|
|
137
|
+
name="genai.evaluation.pii.blocked",
|
|
138
|
+
description="Number of requests/responses blocked due to PII",
|
|
139
|
+
unit="1",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Toxicity Detection Metrics
|
|
143
|
+
self.toxicity_detection_counter = meter.create_counter(
|
|
144
|
+
name="genai.evaluation.toxicity.detections",
|
|
145
|
+
description="Number of toxicity detections in prompts and responses",
|
|
146
|
+
unit="1",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self.toxicity_category_counter = meter.create_counter(
|
|
150
|
+
name="genai.evaluation.toxicity.categories",
|
|
151
|
+
description="Toxicity detections by category",
|
|
152
|
+
unit="1",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
self.toxicity_blocked_counter = meter.create_counter(
|
|
156
|
+
name="genai.evaluation.toxicity.blocked",
|
|
157
|
+
description="Number of requests/responses blocked due to toxicity",
|
|
158
|
+
unit="1",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
self.toxicity_score_histogram = meter.create_histogram(
|
|
162
|
+
name="genai.evaluation.toxicity.score",
|
|
163
|
+
description="Toxicity score distribution",
|
|
164
|
+
unit="1",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Bias Detection Metrics
|
|
168
|
+
self.bias_detection_counter = meter.create_counter(
|
|
169
|
+
name="genai.evaluation.bias.detections",
|
|
170
|
+
description="Number of bias detections in prompts and responses",
|
|
171
|
+
unit="1",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self.bias_type_counter = meter.create_counter(
|
|
175
|
+
name="genai.evaluation.bias.types",
|
|
176
|
+
description="Bias detections by type",
|
|
177
|
+
unit="1",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
self.bias_blocked_counter = meter.create_counter(
|
|
181
|
+
name="genai.evaluation.bias.blocked",
|
|
182
|
+
description="Number of requests/responses blocked due to bias",
|
|
183
|
+
unit="1",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
self.bias_score_histogram = meter.create_histogram(
|
|
187
|
+
name="genai.evaluation.bias.score",
|
|
188
|
+
description="Bias score distribution",
|
|
189
|
+
unit="1",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Prompt Injection Detection Metrics
|
|
193
|
+
self.prompt_injection_counter = meter.create_counter(
|
|
194
|
+
name="genai.evaluation.prompt_injection.detections",
|
|
195
|
+
description="Number of prompt injection attempts detected",
|
|
196
|
+
unit="1",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
self.prompt_injection_type_counter = meter.create_counter(
|
|
200
|
+
name="genai.evaluation.prompt_injection.types",
|
|
201
|
+
description="Prompt injection detections by type",
|
|
202
|
+
unit="1",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.prompt_injection_blocked_counter = meter.create_counter(
|
|
206
|
+
name="genai.evaluation.prompt_injection.blocked",
|
|
207
|
+
description="Number of requests blocked due to prompt injection",
|
|
208
|
+
unit="1",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
self.prompt_injection_score_histogram = meter.create_histogram(
|
|
212
|
+
name="genai.evaluation.prompt_injection.score",
|
|
213
|
+
description="Prompt injection score distribution",
|
|
214
|
+
unit="1",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Restricted Topics Metrics
|
|
218
|
+
self.restricted_topics_counter = meter.create_counter(
|
|
219
|
+
name="genai.evaluation.restricted_topics.detections",
|
|
220
|
+
description="Number of restricted topics detected",
|
|
221
|
+
unit="1",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self.restricted_topics_type_counter = meter.create_counter(
|
|
225
|
+
name="genai.evaluation.restricted_topics.types",
|
|
226
|
+
description="Restricted topics by type",
|
|
227
|
+
unit="1",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
self.restricted_topics_blocked_counter = meter.create_counter(
|
|
231
|
+
name="genai.evaluation.restricted_topics.blocked",
|
|
232
|
+
description="Number of requests/responses blocked due to restricted topics",
|
|
233
|
+
unit="1",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self.restricted_topics_score_histogram = meter.create_histogram(
|
|
237
|
+
name="genai.evaluation.restricted_topics.score",
|
|
238
|
+
description="Restricted topics score distribution",
|
|
239
|
+
unit="1",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Hallucination Detection Metrics
|
|
243
|
+
self.hallucination_counter = meter.create_counter(
|
|
244
|
+
name="genai.evaluation.hallucination.detections",
|
|
245
|
+
description="Number of potential hallucinations detected",
|
|
246
|
+
unit="1",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.hallucination_indicator_counter = meter.create_counter(
|
|
250
|
+
name="genai.evaluation.hallucination.indicators",
|
|
251
|
+
description="Hallucination detections by indicator type",
|
|
252
|
+
unit="1",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
self.hallucination_score_histogram = meter.create_histogram(
|
|
256
|
+
name="genai.evaluation.hallucination.score",
|
|
257
|
+
description="Hallucination score distribution",
|
|
258
|
+
unit="1",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
logger.info("EvaluationSpanProcessor initialized with features:")
|
|
262
|
+
logger.info(" - PII Detection: %s", self.pii_config.enabled)
|
|
263
|
+
logger.info(" - Toxicity Detection: %s", self.toxicity_config.enabled)
|
|
264
|
+
logger.info(" - Bias Detection: %s", self.bias_config.enabled)
|
|
265
|
+
logger.info(" - Prompt Injection Detection: %s", self.prompt_injection_config.enabled)
|
|
266
|
+
logger.info(" - Restricted Topics: %s", self.restricted_topics_config.enabled)
|
|
267
|
+
logger.info(" - Hallucination Detection: %s", self.hallucination_config.enabled)
|
|
268
|
+
|
|
269
|
+
def on_start(self, span: Span, parent_context=None) -> None:
|
|
270
|
+
"""Called when a span is started.
|
|
271
|
+
|
|
272
|
+
For evaluation features, we primarily process on_end when we have the full
|
|
273
|
+
prompt and response data.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
span: The span that was started
|
|
277
|
+
parent_context: Parent context (optional)
|
|
278
|
+
"""
|
|
279
|
+
# Most evaluation happens on_end, but we can do prompt analysis here if needed
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
283
|
+
"""Called when a span is ended.
|
|
284
|
+
|
|
285
|
+
This is where we perform evaluation and safety checks on the span's
|
|
286
|
+
prompt and response data.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
span: The span that ended
|
|
290
|
+
"""
|
|
291
|
+
if not isinstance(span, Span):
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Extract prompt and response from span attributes
|
|
296
|
+
attributes = dict(span.attributes) if span.attributes else {}
|
|
297
|
+
|
|
298
|
+
prompt = self._extract_prompt(attributes)
|
|
299
|
+
response = self._extract_response(attributes)
|
|
300
|
+
|
|
301
|
+
# Run PII detection
|
|
302
|
+
if self.pii_config.enabled and self.pii_detector:
|
|
303
|
+
self._check_pii(span, prompt, response)
|
|
304
|
+
|
|
305
|
+
# Run toxicity detection
|
|
306
|
+
if self.toxicity_config.enabled and self.toxicity_detector:
|
|
307
|
+
self._check_toxicity(span, prompt, response)
|
|
308
|
+
|
|
309
|
+
# Run bias detection
|
|
310
|
+
if self.bias_config.enabled and self.bias_detector:
|
|
311
|
+
self._check_bias(span, prompt, response)
|
|
312
|
+
|
|
313
|
+
# Run prompt injection detection (prompts only)
|
|
314
|
+
if self.prompt_injection_config.enabled and self.prompt_injection_detector:
|
|
315
|
+
self._check_prompt_injection(span, prompt)
|
|
316
|
+
|
|
317
|
+
# Run restricted topics detection
|
|
318
|
+
if self.restricted_topics_config.enabled and self.restricted_topics_detector:
|
|
319
|
+
self._check_restricted_topics(span, prompt, response)
|
|
320
|
+
|
|
321
|
+
# Run hallucination detection (responses only)
|
|
322
|
+
if self.hallucination_config.enabled and self.hallucination_detector:
|
|
323
|
+
self._check_hallucination(span, prompt, response, attributes)
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error("Error in evaluation span processor: %s", e, exc_info=True)
|
|
327
|
+
|
|
328
|
+
def _extract_prompt(self, attributes: dict) -> Optional[str]:
|
|
329
|
+
"""Extract prompt text from span attributes.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
attributes: Span attributes
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Optional[str]: Prompt text if found
|
|
336
|
+
"""
|
|
337
|
+
# Try different attribute names used by various instrumentors
|
|
338
|
+
prompt_keys = [
|
|
339
|
+
"gen_ai.prompt",
|
|
340
|
+
"gen_ai.prompt.0.content",
|
|
341
|
+
"gen_ai.request.prompt",
|
|
342
|
+
"llm.prompts",
|
|
343
|
+
"gen_ai.content.prompt",
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
for key in prompt_keys:
|
|
347
|
+
if key in attributes:
|
|
348
|
+
value = attributes[key]
|
|
349
|
+
if isinstance(value, str):
|
|
350
|
+
return value
|
|
351
|
+
elif isinstance(value, list) and value:
|
|
352
|
+
# Handle list of messages
|
|
353
|
+
if isinstance(value[0], dict) and "content" in value[0]:
|
|
354
|
+
return value[0]["content"]
|
|
355
|
+
elif isinstance(value[0], str):
|
|
356
|
+
return value[0]
|
|
357
|
+
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
def _extract_response(self, attributes: dict) -> Optional[str]:
|
|
361
|
+
"""Extract response text from span attributes.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
attributes: Span attributes
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Optional[str]: Response text if found
|
|
368
|
+
"""
|
|
369
|
+
# Try different attribute names used by various instrumentors
|
|
370
|
+
response_keys = [
|
|
371
|
+
"gen_ai.response",
|
|
372
|
+
"gen_ai.completion",
|
|
373
|
+
"gen_ai.response.0.content",
|
|
374
|
+
"llm.responses",
|
|
375
|
+
"gen_ai.content.completion",
|
|
376
|
+
"gen_ai.response.message.content",
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
for key in response_keys:
|
|
380
|
+
if key in attributes:
|
|
381
|
+
value = attributes[key]
|
|
382
|
+
if isinstance(value, str):
|
|
383
|
+
return value
|
|
384
|
+
elif isinstance(value, list) and value:
|
|
385
|
+
# Handle list of messages
|
|
386
|
+
if isinstance(value[0], dict) and "content" in value[0]:
|
|
387
|
+
return value[0]["content"]
|
|
388
|
+
elif isinstance(value[0], str):
|
|
389
|
+
return value[0]
|
|
390
|
+
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
def _check_pii(self, span: Span, prompt: Optional[str], response: Optional[str]) -> None:
|
|
394
|
+
"""Check for PII in prompt and response.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
span: The span to add PII attributes to
|
|
398
|
+
prompt: Prompt text (optional)
|
|
399
|
+
response: Response text (optional)
|
|
400
|
+
"""
|
|
401
|
+
if not self.pii_detector:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
# Check prompt for PII
|
|
406
|
+
if prompt:
|
|
407
|
+
result = self.pii_detector.detect(prompt)
|
|
408
|
+
if result.has_pii:
|
|
409
|
+
span.set_attribute("evaluation.pii.prompt.detected", True)
|
|
410
|
+
span.set_attribute("evaluation.pii.prompt.entity_count", len(result.entities))
|
|
411
|
+
span.set_attribute(
|
|
412
|
+
"evaluation.pii.prompt.entity_types",
|
|
413
|
+
list(result.entity_counts.keys()),
|
|
414
|
+
)
|
|
415
|
+
span.set_attribute("evaluation.pii.prompt.score", result.score)
|
|
416
|
+
|
|
417
|
+
# Record metrics
|
|
418
|
+
self.pii_detection_counter.add(
|
|
419
|
+
1, {"location": "prompt", "mode": self.pii_config.mode.value}
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Add entity counts by type
|
|
423
|
+
for entity_type, count in result.entity_counts.items():
|
|
424
|
+
span.set_attribute(
|
|
425
|
+
f"evaluation.pii.prompt.{entity_type.lower()}_count", count
|
|
426
|
+
)
|
|
427
|
+
# Record entity metrics
|
|
428
|
+
self.pii_entity_counter.add(
|
|
429
|
+
count,
|
|
430
|
+
{
|
|
431
|
+
"entity_type": entity_type,
|
|
432
|
+
"location": "prompt",
|
|
433
|
+
},
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# If blocking, set error status
|
|
437
|
+
if result.blocked:
|
|
438
|
+
span.set_status(
|
|
439
|
+
Status(
|
|
440
|
+
StatusCode.ERROR,
|
|
441
|
+
"Request blocked due to PII detection",
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
span.set_attribute("evaluation.pii.prompt.blocked", True)
|
|
445
|
+
# Record blocked metric
|
|
446
|
+
self.pii_blocked_counter.add(1, {"location": "prompt"})
|
|
447
|
+
|
|
448
|
+
# Add redacted text if available
|
|
449
|
+
if result.redacted_text:
|
|
450
|
+
span.set_attribute("evaluation.pii.prompt.redacted", result.redacted_text)
|
|
451
|
+
else:
|
|
452
|
+
span.set_attribute("evaluation.pii.prompt.detected", False)
|
|
453
|
+
|
|
454
|
+
# Check response for PII
|
|
455
|
+
if response:
|
|
456
|
+
result = self.pii_detector.detect(response)
|
|
457
|
+
if result.has_pii:
|
|
458
|
+
span.set_attribute("evaluation.pii.response.detected", True)
|
|
459
|
+
span.set_attribute("evaluation.pii.response.entity_count", len(result.entities))
|
|
460
|
+
span.set_attribute(
|
|
461
|
+
"evaluation.pii.response.entity_types",
|
|
462
|
+
list(result.entity_counts.keys()),
|
|
463
|
+
)
|
|
464
|
+
span.set_attribute("evaluation.pii.response.score", result.score)
|
|
465
|
+
|
|
466
|
+
# Record metrics
|
|
467
|
+
self.pii_detection_counter.add(
|
|
468
|
+
1, {"location": "response", "mode": self.pii_config.mode.value}
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Add entity counts by type
|
|
472
|
+
for entity_type, count in result.entity_counts.items():
|
|
473
|
+
span.set_attribute(
|
|
474
|
+
f"evaluation.pii.response.{entity_type.lower()}_count",
|
|
475
|
+
count,
|
|
476
|
+
)
|
|
477
|
+
# Record entity metrics
|
|
478
|
+
self.pii_entity_counter.add(
|
|
479
|
+
count,
|
|
480
|
+
{
|
|
481
|
+
"entity_type": entity_type,
|
|
482
|
+
"location": "response",
|
|
483
|
+
},
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# If blocking, set error status
|
|
487
|
+
if result.blocked:
|
|
488
|
+
span.set_status(
|
|
489
|
+
Status(
|
|
490
|
+
StatusCode.ERROR,
|
|
491
|
+
"Response blocked due to PII detection",
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
span.set_attribute("evaluation.pii.response.blocked", True)
|
|
495
|
+
# Record blocked metric
|
|
496
|
+
self.pii_blocked_counter.add(1, {"location": "response"})
|
|
497
|
+
|
|
498
|
+
# Add redacted text if available
|
|
499
|
+
if result.redacted_text:
|
|
500
|
+
span.set_attribute("evaluation.pii.response.redacted", result.redacted_text)
|
|
501
|
+
else:
|
|
502
|
+
span.set_attribute("evaluation.pii.response.detected", False)
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
logger.error("Error checking PII: %s", e, exc_info=True)
|
|
506
|
+
span.set_attribute("evaluation.pii.error", str(e))
|
|
507
|
+
|
|
508
|
+
def _check_toxicity(self, span: Span, prompt: Optional[str], response: Optional[str]) -> None:
|
|
509
|
+
"""Check for toxicity in prompt and response.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
span: The span to add toxicity attributes to
|
|
513
|
+
prompt: Prompt text (optional)
|
|
514
|
+
response: Response text (optional)
|
|
515
|
+
"""
|
|
516
|
+
if not self.toxicity_detector:
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
# Check prompt for toxicity
|
|
521
|
+
if prompt:
|
|
522
|
+
result = self.toxicity_detector.detect(prompt)
|
|
523
|
+
if result.is_toxic:
|
|
524
|
+
span.set_attribute("evaluation.toxicity.prompt.detected", True)
|
|
525
|
+
span.set_attribute("evaluation.toxicity.prompt.max_score", result.max_score)
|
|
526
|
+
span.set_attribute(
|
|
527
|
+
"evaluation.toxicity.prompt.categories",
|
|
528
|
+
result.toxic_categories,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Add individual category scores
|
|
532
|
+
for category, score in result.scores.items():
|
|
533
|
+
span.set_attribute(f"evaluation.toxicity.prompt.{category}_score", score)
|
|
534
|
+
|
|
535
|
+
# Record metrics
|
|
536
|
+
self.toxicity_detection_counter.add(1, {"location": "prompt"})
|
|
537
|
+
self.toxicity_score_histogram.record(result.max_score, {"location": "prompt"})
|
|
538
|
+
|
|
539
|
+
# Record category metrics
|
|
540
|
+
for category in result.toxic_categories:
|
|
541
|
+
self.toxicity_category_counter.add(
|
|
542
|
+
1, {"category": category, "location": "prompt"}
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
# If blocking, set error status
|
|
546
|
+
if result.blocked:
|
|
547
|
+
span.set_status(
|
|
548
|
+
Status(
|
|
549
|
+
StatusCode.ERROR,
|
|
550
|
+
"Request blocked due to toxic content",
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
span.set_attribute("evaluation.toxicity.prompt.blocked", True)
|
|
554
|
+
self.toxicity_blocked_counter.add(1, {"location": "prompt"})
|
|
555
|
+
else:
|
|
556
|
+
span.set_attribute("evaluation.toxicity.prompt.detected", False)
|
|
557
|
+
|
|
558
|
+
# Check response for toxicity
|
|
559
|
+
if response:
|
|
560
|
+
result = self.toxicity_detector.detect(response)
|
|
561
|
+
if result.is_toxic:
|
|
562
|
+
span.set_attribute("evaluation.toxicity.response.detected", True)
|
|
563
|
+
span.set_attribute("evaluation.toxicity.response.max_score", result.max_score)
|
|
564
|
+
span.set_attribute(
|
|
565
|
+
"evaluation.toxicity.response.categories",
|
|
566
|
+
result.toxic_categories,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Add individual category scores
|
|
570
|
+
for category, score in result.scores.items():
|
|
571
|
+
span.set_attribute(f"evaluation.toxicity.response.{category}_score", score)
|
|
572
|
+
|
|
573
|
+
# Record metrics
|
|
574
|
+
self.toxicity_detection_counter.add(1, {"location": "response"})
|
|
575
|
+
self.toxicity_score_histogram.record(result.max_score, {"location": "response"})
|
|
576
|
+
|
|
577
|
+
# Record category metrics
|
|
578
|
+
for category in result.toxic_categories:
|
|
579
|
+
self.toxicity_category_counter.add(
|
|
580
|
+
1, {"category": category, "location": "response"}
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# If blocking, set error status
|
|
584
|
+
if result.blocked:
|
|
585
|
+
span.set_status(
|
|
586
|
+
Status(
|
|
587
|
+
StatusCode.ERROR,
|
|
588
|
+
"Response blocked due to toxic content",
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
span.set_attribute("evaluation.toxicity.response.blocked", True)
|
|
592
|
+
self.toxicity_blocked_counter.add(1, {"location": "response"})
|
|
593
|
+
else:
|
|
594
|
+
span.set_attribute("evaluation.toxicity.response.detected", False)
|
|
595
|
+
|
|
596
|
+
except Exception as e:
|
|
597
|
+
logger.error("Error checking toxicity: %s", e, exc_info=True)
|
|
598
|
+
span.set_attribute("evaluation.toxicity.error", str(e))
|
|
599
|
+
|
|
600
|
+
def _check_bias(self, span: Span, prompt: Optional[str], response: Optional[str]) -> None:
|
|
601
|
+
"""Check for bias in prompt and response.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
span: The span to add bias attributes to
|
|
605
|
+
prompt: Prompt text (optional)
|
|
606
|
+
response: Response text (optional)
|
|
607
|
+
"""
|
|
608
|
+
if not self.bias_detector:
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
try:
|
|
612
|
+
# Check prompt for bias
|
|
613
|
+
if prompt:
|
|
614
|
+
result = self.bias_detector.detect(prompt)
|
|
615
|
+
if result.has_bias:
|
|
616
|
+
span.set_attribute("evaluation.bias.prompt.detected", True)
|
|
617
|
+
span.set_attribute("evaluation.bias.prompt.max_score", result.max_score)
|
|
618
|
+
span.set_attribute(
|
|
619
|
+
"evaluation.bias.prompt.detected_biases",
|
|
620
|
+
result.detected_biases,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Add individual bias type scores
|
|
624
|
+
for bias_type, score in result.bias_scores.items():
|
|
625
|
+
if score > 0:
|
|
626
|
+
span.set_attribute(f"evaluation.bias.prompt.{bias_type}_score", score)
|
|
627
|
+
|
|
628
|
+
# Add patterns matched
|
|
629
|
+
for bias_type, patterns in result.patterns_matched.items():
|
|
630
|
+
span.set_attribute(
|
|
631
|
+
f"evaluation.bias.prompt.{bias_type}_patterns",
|
|
632
|
+
patterns[:5], # Limit to first 5 patterns
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# Record metrics
|
|
636
|
+
self.bias_detection_counter.add(1, {"location": "prompt"})
|
|
637
|
+
self.bias_score_histogram.record(result.max_score, {"location": "prompt"})
|
|
638
|
+
|
|
639
|
+
# Record bias type metrics
|
|
640
|
+
for bias_type in result.detected_biases:
|
|
641
|
+
self.bias_type_counter.add(
|
|
642
|
+
1, {"bias_type": bias_type, "location": "prompt"}
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# If blocking mode and threshold exceeded, set error status
|
|
646
|
+
if self.bias_config.block_on_detection and result.has_bias:
|
|
647
|
+
span.set_status(
|
|
648
|
+
Status(
|
|
649
|
+
StatusCode.ERROR,
|
|
650
|
+
"Request blocked due to bias detection",
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
span.set_attribute("evaluation.bias.prompt.blocked", True)
|
|
654
|
+
self.bias_blocked_counter.add(1, {"location": "prompt"})
|
|
655
|
+
else:
|
|
656
|
+
span.set_attribute("evaluation.bias.prompt.detected", False)
|
|
657
|
+
|
|
658
|
+
# Check response for bias
|
|
659
|
+
if response:
|
|
660
|
+
result = self.bias_detector.detect(response)
|
|
661
|
+
if result.has_bias:
|
|
662
|
+
span.set_attribute("evaluation.bias.response.detected", True)
|
|
663
|
+
span.set_attribute("evaluation.bias.response.max_score", result.max_score)
|
|
664
|
+
span.set_attribute(
|
|
665
|
+
"evaluation.bias.response.detected_biases",
|
|
666
|
+
result.detected_biases,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
# Add individual bias type scores
|
|
670
|
+
for bias_type, score in result.bias_scores.items():
|
|
671
|
+
if score > 0:
|
|
672
|
+
span.set_attribute(f"evaluation.bias.response.{bias_type}_score", score)
|
|
673
|
+
|
|
674
|
+
# Add patterns matched
|
|
675
|
+
for bias_type, patterns in result.patterns_matched.items():
|
|
676
|
+
span.set_attribute(
|
|
677
|
+
f"evaluation.bias.response.{bias_type}_patterns",
|
|
678
|
+
patterns[:5], # Limit to first 5 patterns
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Record metrics
|
|
682
|
+
self.bias_detection_counter.add(1, {"location": "response"})
|
|
683
|
+
self.bias_score_histogram.record(result.max_score, {"location": "response"})
|
|
684
|
+
|
|
685
|
+
# Record bias type metrics
|
|
686
|
+
for bias_type in result.detected_biases:
|
|
687
|
+
self.bias_type_counter.add(
|
|
688
|
+
1, {"bias_type": bias_type, "location": "response"}
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
# If blocking mode and threshold exceeded, set error status
|
|
692
|
+
if self.bias_config.block_on_detection and result.has_bias:
|
|
693
|
+
span.set_status(
|
|
694
|
+
Status(
|
|
695
|
+
StatusCode.ERROR,
|
|
696
|
+
"Response blocked due to bias detection",
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
span.set_attribute("evaluation.bias.response.blocked", True)
|
|
700
|
+
self.bias_blocked_counter.add(1, {"location": "response"})
|
|
701
|
+
else:
|
|
702
|
+
span.set_attribute("evaluation.bias.response.detected", False)
|
|
703
|
+
|
|
704
|
+
except Exception as e:
|
|
705
|
+
logger.error("Error checking bias: %s", e, exc_info=True)
|
|
706
|
+
span.set_attribute("evaluation.bias.error", str(e))
|
|
707
|
+
|
|
708
|
+
def _check_prompt_injection(self, span: Span, prompt: Optional[str]) -> None:
|
|
709
|
+
"""Check for prompt injection attempts in prompt.
|
|
710
|
+
|
|
711
|
+
Args:
|
|
712
|
+
span: The span to add prompt injection attributes to
|
|
713
|
+
prompt: Prompt text (optional)
|
|
714
|
+
"""
|
|
715
|
+
if not self.prompt_injection_detector or not prompt:
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
result = self.prompt_injection_detector.detect(prompt)
|
|
720
|
+
if result.is_injection:
|
|
721
|
+
span.set_attribute("evaluation.prompt_injection.detected", True)
|
|
722
|
+
span.set_attribute("evaluation.prompt_injection.score", result.injection_score)
|
|
723
|
+
span.set_attribute("evaluation.prompt_injection.types", result.injection_types)
|
|
724
|
+
|
|
725
|
+
# Add patterns matched
|
|
726
|
+
for inj_type, patterns in result.patterns_matched.items():
|
|
727
|
+
span.set_attribute(
|
|
728
|
+
f"evaluation.prompt_injection.{inj_type}_patterns",
|
|
729
|
+
patterns[:5], # Limit to first 5 patterns
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# Record metrics
|
|
733
|
+
self.prompt_injection_counter.add(1, {"location": "prompt"})
|
|
734
|
+
self.prompt_injection_score_histogram.record(
|
|
735
|
+
result.injection_score, {"location": "prompt"}
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# Record injection type metrics
|
|
739
|
+
for inj_type in result.injection_types:
|
|
740
|
+
self.prompt_injection_type_counter.add(1, {"injection_type": inj_type})
|
|
741
|
+
|
|
742
|
+
# If blocking, set error status
|
|
743
|
+
if result.blocked:
|
|
744
|
+
span.set_status(
|
|
745
|
+
Status(
|
|
746
|
+
StatusCode.ERROR,
|
|
747
|
+
"Request blocked due to prompt injection attempt",
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
span.set_attribute("evaluation.prompt_injection.blocked", True)
|
|
751
|
+
self.prompt_injection_blocked_counter.add(1, {})
|
|
752
|
+
else:
|
|
753
|
+
span.set_attribute("evaluation.prompt_injection.detected", False)
|
|
754
|
+
|
|
755
|
+
except Exception as e:
|
|
756
|
+
logger.error("Error checking prompt injection: %s", e, exc_info=True)
|
|
757
|
+
span.set_attribute("evaluation.prompt_injection.error", str(e))
|
|
758
|
+
|
|
759
|
+
def _check_restricted_topics(
|
|
760
|
+
self, span: Span, prompt: Optional[str], response: Optional[str]
|
|
761
|
+
) -> None:
|
|
762
|
+
"""Check for restricted topics in prompt and response.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
span: The span to add restricted topics attributes to
|
|
766
|
+
prompt: Prompt text (optional)
|
|
767
|
+
response: Response text (optional)
|
|
768
|
+
"""
|
|
769
|
+
if not self.restricted_topics_detector:
|
|
770
|
+
return
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
# Check prompt for restricted topics
|
|
774
|
+
if prompt:
|
|
775
|
+
result = self.restricted_topics_detector.detect(prompt)
|
|
776
|
+
if result.has_restricted_topic:
|
|
777
|
+
span.set_attribute("evaluation.restricted_topics.prompt.detected", True)
|
|
778
|
+
span.set_attribute(
|
|
779
|
+
"evaluation.restricted_topics.prompt.max_score", result.max_score
|
|
780
|
+
)
|
|
781
|
+
span.set_attribute(
|
|
782
|
+
"evaluation.restricted_topics.prompt.topics",
|
|
783
|
+
result.detected_topics,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
# Add individual topic scores
|
|
787
|
+
for topic, score in result.topic_scores.items():
|
|
788
|
+
if score > 0:
|
|
789
|
+
span.set_attribute(
|
|
790
|
+
f"evaluation.restricted_topics.prompt.{topic}_score", score
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# Record metrics
|
|
794
|
+
self.restricted_topics_counter.add(1, {"location": "prompt"})
|
|
795
|
+
self.restricted_topics_score_histogram.record(
|
|
796
|
+
result.max_score, {"location": "prompt"}
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
# Record topic metrics
|
|
800
|
+
for topic in result.detected_topics:
|
|
801
|
+
self.restricted_topics_type_counter.add(
|
|
802
|
+
1, {"topic": topic, "location": "prompt"}
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# If blocking, set error status
|
|
806
|
+
if result.blocked:
|
|
807
|
+
span.set_status(
|
|
808
|
+
Status(
|
|
809
|
+
StatusCode.ERROR,
|
|
810
|
+
"Request blocked due to restricted topic",
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
span.set_attribute("evaluation.restricted_topics.prompt.blocked", True)
|
|
814
|
+
self.restricted_topics_blocked_counter.add(1, {"location": "prompt"})
|
|
815
|
+
else:
|
|
816
|
+
span.set_attribute("evaluation.restricted_topics.prompt.detected", False)
|
|
817
|
+
|
|
818
|
+
# Check response for restricted topics
|
|
819
|
+
if response:
|
|
820
|
+
result = self.restricted_topics_detector.detect(response)
|
|
821
|
+
if result.has_restricted_topic:
|
|
822
|
+
span.set_attribute("evaluation.restricted_topics.response.detected", True)
|
|
823
|
+
span.set_attribute(
|
|
824
|
+
"evaluation.restricted_topics.response.max_score", result.max_score
|
|
825
|
+
)
|
|
826
|
+
span.set_attribute(
|
|
827
|
+
"evaluation.restricted_topics.response.topics",
|
|
828
|
+
result.detected_topics,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# Add individual topic scores
|
|
832
|
+
for topic, score in result.topic_scores.items():
|
|
833
|
+
if score > 0:
|
|
834
|
+
span.set_attribute(
|
|
835
|
+
f"evaluation.restricted_topics.response.{topic}_score", score
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
# Record metrics
|
|
839
|
+
self.restricted_topics_counter.add(1, {"location": "response"})
|
|
840
|
+
self.restricted_topics_score_histogram.record(
|
|
841
|
+
result.max_score, {"location": "response"}
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
# Record topic metrics
|
|
845
|
+
for topic in result.detected_topics:
|
|
846
|
+
self.restricted_topics_type_counter.add(
|
|
847
|
+
1, {"topic": topic, "location": "response"}
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
# If blocking, set error status
|
|
851
|
+
if result.blocked:
|
|
852
|
+
span.set_status(
|
|
853
|
+
Status(
|
|
854
|
+
StatusCode.ERROR,
|
|
855
|
+
"Response blocked due to restricted topic",
|
|
856
|
+
)
|
|
857
|
+
)
|
|
858
|
+
span.set_attribute("evaluation.restricted_topics.response.blocked", True)
|
|
859
|
+
self.restricted_topics_blocked_counter.add(1, {"location": "response"})
|
|
860
|
+
else:
|
|
861
|
+
span.set_attribute("evaluation.restricted_topics.response.detected", False)
|
|
862
|
+
|
|
863
|
+
except Exception as e:
|
|
864
|
+
logger.error("Error checking restricted topics: %s", e, exc_info=True)
|
|
865
|
+
span.set_attribute("evaluation.restricted_topics.error", str(e))
|
|
866
|
+
|
|
867
|
+
def _check_hallucination(
|
|
868
|
+
self,
|
|
869
|
+
span: Span,
|
|
870
|
+
prompt: Optional[str],
|
|
871
|
+
response: Optional[str],
|
|
872
|
+
attributes: dict,
|
|
873
|
+
) -> None:
|
|
874
|
+
"""Check for potential hallucinations in response.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
span: The span to add hallucination attributes to
|
|
878
|
+
prompt: Prompt text (optional, used as context)
|
|
879
|
+
response: Response text (optional)
|
|
880
|
+
attributes: Span attributes (for extracting context)
|
|
881
|
+
"""
|
|
882
|
+
if not self.hallucination_detector or not response:
|
|
883
|
+
return
|
|
884
|
+
|
|
885
|
+
try:
|
|
886
|
+
# Use prompt as context if available
|
|
887
|
+
context = prompt
|
|
888
|
+
|
|
889
|
+
# Try to extract additional context from attributes
|
|
890
|
+
context_keys = [
|
|
891
|
+
"gen_ai.context",
|
|
892
|
+
"gen_ai.retrieval.documents",
|
|
893
|
+
"gen_ai.rag.context",
|
|
894
|
+
]
|
|
895
|
+
for key in context_keys:
|
|
896
|
+
if key in attributes:
|
|
897
|
+
value = attributes[key]
|
|
898
|
+
if isinstance(value, str):
|
|
899
|
+
context = f"{context}\n{value}" if context else value
|
|
900
|
+
break
|
|
901
|
+
|
|
902
|
+
result = self.hallucination_detector.detect(response, context)
|
|
903
|
+
|
|
904
|
+
# Always set basic attributes
|
|
905
|
+
span.set_attribute(
|
|
906
|
+
"evaluation.hallucination.response.detected", result.has_hallucination
|
|
907
|
+
)
|
|
908
|
+
span.set_attribute(
|
|
909
|
+
"evaluation.hallucination.response.score", result.hallucination_score
|
|
910
|
+
)
|
|
911
|
+
span.set_attribute("evaluation.hallucination.response.citations", result.citation_count)
|
|
912
|
+
span.set_attribute(
|
|
913
|
+
"evaluation.hallucination.response.hedge_words", result.hedge_words_count
|
|
914
|
+
)
|
|
915
|
+
span.set_attribute(
|
|
916
|
+
"evaluation.hallucination.response.claims", result.factual_claim_count
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
if result.has_hallucination:
|
|
920
|
+
span.set_attribute(
|
|
921
|
+
"evaluation.hallucination.response.indicators", result.hallucination_indicators
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
# Add unsupported claims
|
|
925
|
+
if result.unsupported_claims:
|
|
926
|
+
span.set_attribute(
|
|
927
|
+
"evaluation.hallucination.response.unsupported_claims",
|
|
928
|
+
result.unsupported_claims[:3], # Limit to first 3
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
# Record metrics
|
|
932
|
+
self.hallucination_counter.add(1, {"location": "response"})
|
|
933
|
+
self.hallucination_score_histogram.record(
|
|
934
|
+
result.hallucination_score, {"location": "response"}
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
# Record indicator metrics
|
|
938
|
+
for indicator in result.hallucination_indicators:
|
|
939
|
+
self.hallucination_indicator_counter.add(1, {"indicator": indicator})
|
|
940
|
+
|
|
941
|
+
except Exception as e:
|
|
942
|
+
logger.error("Error checking hallucination: %s", e, exc_info=True)
|
|
943
|
+
span.set_attribute("evaluation.hallucination.error", str(e))
|
|
944
|
+
|
|
945
|
+
def shutdown(self) -> None:
|
|
946
|
+
"""Shutdown the span processor.
|
|
947
|
+
|
|
948
|
+
Called when the tracer provider is shut down.
|
|
949
|
+
"""
|
|
950
|
+
logger.info("EvaluationSpanProcessor shutting down")
|
|
951
|
+
|
|
952
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
953
|
+
"""Force flush any buffered spans.
|
|
954
|
+
|
|
955
|
+
Args:
|
|
956
|
+
timeout_millis: Timeout in milliseconds
|
|
957
|
+
|
|
958
|
+
Returns:
|
|
959
|
+
bool: True if successful
|
|
960
|
+
"""
|
|
961
|
+
# No buffering in this processor
|
|
962
|
+
return True
|