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,661 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentor for DSPy framework.
|
|
2
|
+
|
|
3
|
+
This instrumentor automatically traces DSPy programs, modules, predictions,
|
|
4
|
+
chain-of-thought reasoning, and optimizer operations.
|
|
5
|
+
|
|
6
|
+
DSPy is a Stanford NLP framework for programming language models declaratively
|
|
7
|
+
using modular components that can be optimized automatically.
|
|
8
|
+
|
|
9
|
+
Requirements:
|
|
10
|
+
pip install dspy-ai
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
from ..config import OTelConfig
|
|
17
|
+
from .base import BaseInstrumentor
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DSPyInstrumentor(BaseInstrumentor):
|
|
23
|
+
"""Instrumentor for DSPy framework"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""Initialize the instrumentor."""
|
|
27
|
+
super().__init__()
|
|
28
|
+
self._dspy_available = False
|
|
29
|
+
self._check_availability()
|
|
30
|
+
|
|
31
|
+
def _check_availability(self):
|
|
32
|
+
"""Check if DSPy is available."""
|
|
33
|
+
try:
|
|
34
|
+
import dspy
|
|
35
|
+
|
|
36
|
+
self._dspy_available = True
|
|
37
|
+
logger.debug("DSPy framework detected and available for instrumentation")
|
|
38
|
+
except ImportError:
|
|
39
|
+
logger.debug("DSPy not installed, instrumentation will be skipped")
|
|
40
|
+
self._dspy_available = False
|
|
41
|
+
|
|
42
|
+
def instrument(self, config: OTelConfig):
|
|
43
|
+
"""Instrument DSPy if available.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config (OTelConfig): The OpenTelemetry configuration object.
|
|
47
|
+
"""
|
|
48
|
+
if not self._dspy_available:
|
|
49
|
+
logger.debug("Skipping DSPy instrumentation - library not available")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
self.config = config
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
import dspy
|
|
56
|
+
import wrapt
|
|
57
|
+
from dspy.primitives.module import BaseModule
|
|
58
|
+
|
|
59
|
+
# Wrap Module.__call__ to trace all module executions
|
|
60
|
+
wrapt.wrap_function_wrapper(
|
|
61
|
+
"dspy.primitives.module",
|
|
62
|
+
"BaseModule.__call__",
|
|
63
|
+
self._wrap_module_call,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Wrap Predict.forward for prediction operations
|
|
67
|
+
wrapt.wrap_function_wrapper(
|
|
68
|
+
"dspy.predict.predict",
|
|
69
|
+
"Predict.forward",
|
|
70
|
+
self._wrap_predict_forward,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Wrap ChainOfThought.forward if available
|
|
74
|
+
try:
|
|
75
|
+
wrapt.wrap_function_wrapper(
|
|
76
|
+
"dspy.predict.chain_of_thought",
|
|
77
|
+
"ChainOfThought.forward",
|
|
78
|
+
self._wrap_chain_of_thought_forward,
|
|
79
|
+
)
|
|
80
|
+
except (ImportError, AttributeError):
|
|
81
|
+
logger.debug("ChainOfThought not available for instrumentation")
|
|
82
|
+
|
|
83
|
+
# Wrap ReAct.forward if available
|
|
84
|
+
try:
|
|
85
|
+
wrapt.wrap_function_wrapper(
|
|
86
|
+
"dspy.predict.react",
|
|
87
|
+
"ReAct.forward",
|
|
88
|
+
self._wrap_react_forward,
|
|
89
|
+
)
|
|
90
|
+
except (ImportError, AttributeError):
|
|
91
|
+
logger.debug("ReAct not available for instrumentation")
|
|
92
|
+
|
|
93
|
+
# Wrap optimizer compile methods
|
|
94
|
+
self._wrap_optimizers()
|
|
95
|
+
|
|
96
|
+
self._instrumented = True
|
|
97
|
+
logger.info("DSPy instrumentation enabled")
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error("Failed to instrument DSPy: %s", e, exc_info=True)
|
|
101
|
+
if config.fail_on_error:
|
|
102
|
+
raise
|
|
103
|
+
|
|
104
|
+
def _wrap_module_call(self, wrapped, instance, args, kwargs):
|
|
105
|
+
"""Wrap Module.__call__ to trace module execution.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
wrapped: The original method
|
|
109
|
+
instance: The Module instance
|
|
110
|
+
args: Positional arguments
|
|
111
|
+
kwargs: Keyword arguments
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
The result of the wrapped method
|
|
115
|
+
"""
|
|
116
|
+
# Get module class name
|
|
117
|
+
module_name = instance.__class__.__name__
|
|
118
|
+
|
|
119
|
+
# Create span name based on module type
|
|
120
|
+
if module_name == "BaseModule":
|
|
121
|
+
span_name = "dspy.module.call"
|
|
122
|
+
else:
|
|
123
|
+
span_name = f"dspy.module.{module_name.lower()}"
|
|
124
|
+
|
|
125
|
+
return self.create_span_wrapper(
|
|
126
|
+
span_name=span_name,
|
|
127
|
+
extract_attributes=lambda inst, args, kwargs: self._extract_module_attributes(
|
|
128
|
+
instance, args, kwargs
|
|
129
|
+
),
|
|
130
|
+
extract_response_attributes=self._extract_module_response_attributes,
|
|
131
|
+
)(wrapped)(*args, **kwargs)
|
|
132
|
+
|
|
133
|
+
def _wrap_predict_forward(self, wrapped, instance, args, kwargs):
|
|
134
|
+
"""Wrap Predict.forward to trace predictions.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
wrapped: The original method
|
|
138
|
+
instance: The Predict instance
|
|
139
|
+
args: Positional arguments
|
|
140
|
+
kwargs: Keyword arguments
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The result of the wrapped method
|
|
144
|
+
"""
|
|
145
|
+
return self.create_span_wrapper(
|
|
146
|
+
span_name="dspy.predict",
|
|
147
|
+
extract_attributes=lambda inst, args, kwargs: self._extract_predict_attributes(
|
|
148
|
+
instance, kwargs
|
|
149
|
+
),
|
|
150
|
+
extract_response_attributes=self._extract_predict_response_attributes,
|
|
151
|
+
)(wrapped)(*args, **kwargs)
|
|
152
|
+
|
|
153
|
+
def _wrap_chain_of_thought_forward(self, wrapped, instance, args, kwargs):
|
|
154
|
+
"""Wrap ChainOfThought.forward to trace chain-of-thought reasoning.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
wrapped: The original method
|
|
158
|
+
instance: The ChainOfThought instance
|
|
159
|
+
args: Positional arguments
|
|
160
|
+
kwargs: Keyword arguments
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The result of the wrapped method
|
|
164
|
+
"""
|
|
165
|
+
return self.create_span_wrapper(
|
|
166
|
+
span_name="dspy.chain_of_thought",
|
|
167
|
+
extract_attributes=lambda inst, args, kwargs: self._extract_cot_attributes(
|
|
168
|
+
instance, kwargs
|
|
169
|
+
),
|
|
170
|
+
extract_response_attributes=self._extract_cot_response_attributes,
|
|
171
|
+
)(wrapped)(*args, **kwargs)
|
|
172
|
+
|
|
173
|
+
def _wrap_react_forward(self, wrapped, instance, args, kwargs):
|
|
174
|
+
"""Wrap ReAct.forward to trace ReAct (reasoning + acting).
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
wrapped: The original method
|
|
178
|
+
instance: The ReAct instance
|
|
179
|
+
args: Positional arguments
|
|
180
|
+
kwargs: Keyword arguments
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The result of the wrapped method
|
|
184
|
+
"""
|
|
185
|
+
return self.create_span_wrapper(
|
|
186
|
+
span_name="dspy.react",
|
|
187
|
+
extract_attributes=lambda inst, args, kwargs: self._extract_react_attributes(
|
|
188
|
+
instance, kwargs
|
|
189
|
+
),
|
|
190
|
+
extract_response_attributes=self._extract_react_response_attributes,
|
|
191
|
+
)(wrapped)(*args, **kwargs)
|
|
192
|
+
|
|
193
|
+
def _wrap_optimizers(self):
|
|
194
|
+
"""Wrap optimizer compile methods."""
|
|
195
|
+
try:
|
|
196
|
+
import wrapt
|
|
197
|
+
|
|
198
|
+
# Wrap COPRO optimizer
|
|
199
|
+
try:
|
|
200
|
+
wrapt.wrap_function_wrapper(
|
|
201
|
+
"dspy.teleprompt.copro_optimizer",
|
|
202
|
+
"COPRO.compile",
|
|
203
|
+
self._wrap_optimizer_compile,
|
|
204
|
+
)
|
|
205
|
+
except (ImportError, AttributeError):
|
|
206
|
+
logger.debug("COPRO optimizer not available for instrumentation")
|
|
207
|
+
|
|
208
|
+
# Wrap MIPROv2 optimizer if available
|
|
209
|
+
try:
|
|
210
|
+
wrapt.wrap_function_wrapper(
|
|
211
|
+
"dspy.teleprompt.mipro_optimizer_v2",
|
|
212
|
+
"MIPROv2.compile",
|
|
213
|
+
self._wrap_optimizer_compile,
|
|
214
|
+
)
|
|
215
|
+
except (ImportError, AttributeError):
|
|
216
|
+
logger.debug("MIPROv2 optimizer not available for instrumentation")
|
|
217
|
+
|
|
218
|
+
# Wrap BootstrapFewShot teleprompter
|
|
219
|
+
try:
|
|
220
|
+
wrapt.wrap_function_wrapper(
|
|
221
|
+
"dspy.teleprompt.bootstrap",
|
|
222
|
+
"BootstrapFewShot.compile",
|
|
223
|
+
self._wrap_optimizer_compile,
|
|
224
|
+
)
|
|
225
|
+
except (ImportError, AttributeError):
|
|
226
|
+
logger.debug("BootstrapFewShot not available for instrumentation")
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.debug("Failed to wrap optimizers: %s", e)
|
|
230
|
+
|
|
231
|
+
def _wrap_optimizer_compile(self, wrapped, instance, args, kwargs):
|
|
232
|
+
"""Wrap optimizer compile method.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
wrapped: The original method
|
|
236
|
+
instance: The optimizer instance
|
|
237
|
+
args: Positional arguments
|
|
238
|
+
kwargs: Keyword arguments
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
The result of the wrapped method
|
|
242
|
+
"""
|
|
243
|
+
optimizer_name = instance.__class__.__name__
|
|
244
|
+
|
|
245
|
+
return self.create_span_wrapper(
|
|
246
|
+
span_name=f"dspy.optimizer.{optimizer_name.lower()}",
|
|
247
|
+
extract_attributes=lambda inst, args, kwargs: self._extract_optimizer_attributes(
|
|
248
|
+
instance, args, kwargs
|
|
249
|
+
),
|
|
250
|
+
extract_response_attributes=self._extract_optimizer_response_attributes,
|
|
251
|
+
)(wrapped)(*args, **kwargs)
|
|
252
|
+
|
|
253
|
+
def _extract_module_attributes(self, instance: Any, args: Any, kwargs: Any) -> Dict[str, Any]:
|
|
254
|
+
"""Extract attributes from Module execution.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
instance: The Module instance
|
|
258
|
+
args: Positional arguments
|
|
259
|
+
kwargs: Keyword arguments
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
263
|
+
"""
|
|
264
|
+
attrs = {}
|
|
265
|
+
|
|
266
|
+
# Core attributes
|
|
267
|
+
attrs["gen_ai.system"] = "dspy"
|
|
268
|
+
attrs["gen_ai.operation.name"] = "module.call"
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
# Module information
|
|
272
|
+
module_name = instance.__class__.__name__
|
|
273
|
+
attrs["dspy.module.name"] = module_name
|
|
274
|
+
|
|
275
|
+
# Check if module has a name attribute
|
|
276
|
+
if hasattr(instance, "name"):
|
|
277
|
+
attrs["dspy.module.instance_name"] = str(instance.name)
|
|
278
|
+
|
|
279
|
+
# Extract input kwargs
|
|
280
|
+
if kwargs:
|
|
281
|
+
# Limit to first few keys
|
|
282
|
+
input_keys = list(kwargs.keys())[:10]
|
|
283
|
+
attrs["dspy.module.input_keys"] = input_keys
|
|
284
|
+
attrs["dspy.module.input_count"] = len(kwargs)
|
|
285
|
+
|
|
286
|
+
# Extract signature if available
|
|
287
|
+
if hasattr(instance, "signature"):
|
|
288
|
+
sig = instance.signature
|
|
289
|
+
if hasattr(sig, "__name__"):
|
|
290
|
+
attrs["dspy.module.signature"] = sig.__name__
|
|
291
|
+
elif hasattr(sig, "__class__"):
|
|
292
|
+
attrs["dspy.module.signature"] = sig.__class__.__name__
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.debug("Failed to extract module attributes: %s", e)
|
|
296
|
+
|
|
297
|
+
return attrs
|
|
298
|
+
|
|
299
|
+
def _extract_predict_attributes(self, instance: Any, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
300
|
+
"""Extract attributes from Predict execution.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
instance: The Predict instance
|
|
304
|
+
kwargs: Keyword arguments
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
308
|
+
"""
|
|
309
|
+
attrs = {}
|
|
310
|
+
|
|
311
|
+
# Core attributes
|
|
312
|
+
attrs["gen_ai.system"] = "dspy"
|
|
313
|
+
attrs["gen_ai.operation.name"] = "predict"
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
# Extract signature
|
|
317
|
+
if hasattr(instance, "signature"):
|
|
318
|
+
sig = instance.signature
|
|
319
|
+
if hasattr(sig, "__name__"):
|
|
320
|
+
attrs["dspy.predict.signature"] = sig.__name__
|
|
321
|
+
if hasattr(sig, "instructions") and sig.instructions:
|
|
322
|
+
attrs["dspy.predict.instructions"] = str(sig.instructions)[:500]
|
|
323
|
+
|
|
324
|
+
# Extract input and output fields
|
|
325
|
+
if hasattr(sig, "input_fields"):
|
|
326
|
+
input_fields = [f.input_variable for f in sig.input_fields]
|
|
327
|
+
attrs["dspy.predict.input_fields"] = input_fields[:10]
|
|
328
|
+
|
|
329
|
+
if hasattr(sig, "output_fields"):
|
|
330
|
+
output_fields = [f.output_variable for f in sig.output_fields]
|
|
331
|
+
attrs["dspy.predict.output_fields"] = output_fields[:10]
|
|
332
|
+
|
|
333
|
+
# Extract input values
|
|
334
|
+
if kwargs:
|
|
335
|
+
# Get first input value for tracing
|
|
336
|
+
for key, value in list(kwargs.items())[:3]:
|
|
337
|
+
if isinstance(value, str):
|
|
338
|
+
attrs[f"dspy.predict.input.{key}"] = value[:500]
|
|
339
|
+
else:
|
|
340
|
+
attrs[f"dspy.predict.input.{key}"] = str(value)[:200]
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.debug("Failed to extract predict attributes: %s", e)
|
|
344
|
+
|
|
345
|
+
return attrs
|
|
346
|
+
|
|
347
|
+
def _extract_cot_attributes(self, instance: Any, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
348
|
+
"""Extract attributes from ChainOfThought execution.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
instance: The ChainOfThought instance
|
|
352
|
+
kwargs: Keyword arguments
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
356
|
+
"""
|
|
357
|
+
attrs = {}
|
|
358
|
+
|
|
359
|
+
# Core attributes
|
|
360
|
+
attrs["gen_ai.system"] = "dspy"
|
|
361
|
+
attrs["gen_ai.operation.name"] = "chain_of_thought"
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
# Extract signature
|
|
365
|
+
if hasattr(instance, "signature"):
|
|
366
|
+
sig = instance.signature
|
|
367
|
+
if hasattr(sig, "__name__"):
|
|
368
|
+
attrs["dspy.cot.signature"] = sig.__name__
|
|
369
|
+
|
|
370
|
+
# Extract reasoning fields
|
|
371
|
+
if hasattr(instance, "extended_signature"):
|
|
372
|
+
ext_sig = instance.extended_signature
|
|
373
|
+
if hasattr(ext_sig, "output_fields"):
|
|
374
|
+
output_fields = [f.output_variable for f in ext_sig.output_fields]
|
|
375
|
+
attrs["dspy.cot.output_fields"] = output_fields[:10]
|
|
376
|
+
|
|
377
|
+
# Input values
|
|
378
|
+
if kwargs:
|
|
379
|
+
for key, value in list(kwargs.items())[:3]:
|
|
380
|
+
if isinstance(value, str):
|
|
381
|
+
attrs[f"dspy.cot.input.{key}"] = value[:500]
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
logger.debug("Failed to extract chain_of_thought attributes: %s", e)
|
|
385
|
+
|
|
386
|
+
return attrs
|
|
387
|
+
|
|
388
|
+
def _extract_react_attributes(self, instance: Any, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
389
|
+
"""Extract attributes from ReAct execution.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
instance: The ReAct instance
|
|
393
|
+
kwargs: Keyword arguments
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
397
|
+
"""
|
|
398
|
+
attrs = {}
|
|
399
|
+
|
|
400
|
+
# Core attributes
|
|
401
|
+
attrs["gen_ai.system"] = "dspy"
|
|
402
|
+
attrs["gen_ai.operation.name"] = "react"
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
# Extract signature
|
|
406
|
+
if hasattr(instance, "signature"):
|
|
407
|
+
sig = instance.signature
|
|
408
|
+
if hasattr(sig, "__name__"):
|
|
409
|
+
attrs["dspy.react.signature"] = sig.__name__
|
|
410
|
+
|
|
411
|
+
# Extract tools if available
|
|
412
|
+
if hasattr(instance, "tools"):
|
|
413
|
+
tools = instance.tools
|
|
414
|
+
if tools:
|
|
415
|
+
tool_names = [t.__name__ if hasattr(t, "__name__") else str(t) for t in tools]
|
|
416
|
+
attrs["dspy.react.tools"] = tool_names[:10]
|
|
417
|
+
attrs["dspy.react.tools_count"] = len(tools)
|
|
418
|
+
|
|
419
|
+
# Input values
|
|
420
|
+
if kwargs:
|
|
421
|
+
for key, value in list(kwargs.items())[:3]:
|
|
422
|
+
if isinstance(value, str):
|
|
423
|
+
attrs[f"dspy.react.input.{key}"] = value[:500]
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
logger.debug("Failed to extract react attributes: %s", e)
|
|
427
|
+
|
|
428
|
+
return attrs
|
|
429
|
+
|
|
430
|
+
def _extract_optimizer_attributes(
|
|
431
|
+
self, instance: Any, args: Any, kwargs: Any
|
|
432
|
+
) -> Dict[str, Any]:
|
|
433
|
+
"""Extract attributes from optimizer compile.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
instance: The optimizer instance
|
|
437
|
+
args: Positional arguments
|
|
438
|
+
kwargs: Keyword arguments
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
442
|
+
"""
|
|
443
|
+
attrs = {}
|
|
444
|
+
|
|
445
|
+
# Core attributes
|
|
446
|
+
attrs["gen_ai.system"] = "dspy"
|
|
447
|
+
attrs["gen_ai.operation.name"] = "optimizer.compile"
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
# Optimizer information
|
|
451
|
+
optimizer_name = instance.__class__.__name__
|
|
452
|
+
attrs["dspy.optimizer.name"] = optimizer_name
|
|
453
|
+
|
|
454
|
+
# Extract optimizer parameters
|
|
455
|
+
if hasattr(instance, "metric"):
|
|
456
|
+
attrs["dspy.optimizer.has_metric"] = True
|
|
457
|
+
|
|
458
|
+
# Training set size
|
|
459
|
+
if "trainset" in kwargs:
|
|
460
|
+
trainset = kwargs["trainset"]
|
|
461
|
+
if hasattr(trainset, "__len__"):
|
|
462
|
+
attrs["dspy.optimizer.trainset_size"] = len(trainset)
|
|
463
|
+
|
|
464
|
+
# Validation set size
|
|
465
|
+
if "valset" in kwargs:
|
|
466
|
+
valset = kwargs["valset"]
|
|
467
|
+
if hasattr(valset, "__len__"):
|
|
468
|
+
attrs["dspy.optimizer.valset_size"] = len(valset)
|
|
469
|
+
|
|
470
|
+
# COPRO specific
|
|
471
|
+
if optimizer_name == "COPRO":
|
|
472
|
+
if hasattr(instance, "breadth"):
|
|
473
|
+
attrs["dspy.optimizer.copro.breadth"] = instance.breadth
|
|
474
|
+
if hasattr(instance, "depth"):
|
|
475
|
+
attrs["dspy.optimizer.copro.depth"] = instance.depth
|
|
476
|
+
|
|
477
|
+
except Exception as e:
|
|
478
|
+
logger.debug("Failed to extract optimizer attributes: %s", e)
|
|
479
|
+
|
|
480
|
+
return attrs
|
|
481
|
+
|
|
482
|
+
def _extract_module_response_attributes(self, result: Any) -> Dict[str, Any]:
|
|
483
|
+
"""Extract response attributes from Module execution.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
result: The Module execution result
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Dict[str, Any]: Dictionary of response attributes.
|
|
490
|
+
"""
|
|
491
|
+
attrs = {}
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
# Check if result is a Prediction object
|
|
495
|
+
if hasattr(result, "__class__") and result.__class__.__name__ == "Prediction":
|
|
496
|
+
# Extract output keys
|
|
497
|
+
if hasattr(result, "_store"):
|
|
498
|
+
output_keys = list(result._store.keys())
|
|
499
|
+
attrs["dspy.module.output_keys"] = output_keys[:10]
|
|
500
|
+
attrs["dspy.module.output_count"] = len(output_keys)
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
logger.debug("Failed to extract module response attributes: %s", e)
|
|
504
|
+
|
|
505
|
+
return attrs
|
|
506
|
+
|
|
507
|
+
def _extract_predict_response_attributes(self, result: Any) -> Dict[str, Any]:
|
|
508
|
+
"""Extract response attributes from Predict execution.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
result: The Predict execution result (Prediction object)
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Dict[str, Any]: Dictionary of response attributes.
|
|
515
|
+
"""
|
|
516
|
+
attrs = {}
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
# Extract prediction outputs
|
|
520
|
+
if hasattr(result, "_store"):
|
|
521
|
+
store = result._store
|
|
522
|
+
for key, value in list(store.items())[:5]:
|
|
523
|
+
if isinstance(value, str):
|
|
524
|
+
attrs[f"dspy.predict.output.{key}"] = value[:500]
|
|
525
|
+
else:
|
|
526
|
+
attrs[f"dspy.predict.output.{key}"] = str(value)[:200]
|
|
527
|
+
|
|
528
|
+
except Exception as e:
|
|
529
|
+
logger.debug("Failed to extract predict response attributes: %s", e)
|
|
530
|
+
|
|
531
|
+
return attrs
|
|
532
|
+
|
|
533
|
+
def _extract_cot_response_attributes(self, result: Any) -> Dict[str, Any]:
|
|
534
|
+
"""Extract response attributes from ChainOfThought execution.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
result: The ChainOfThought execution result
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Dict[str, Any]: Dictionary of response attributes.
|
|
541
|
+
"""
|
|
542
|
+
attrs = {}
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
# Extract reasoning and answer
|
|
546
|
+
if hasattr(result, "_store"):
|
|
547
|
+
store = result._store
|
|
548
|
+
|
|
549
|
+
# Look for common reasoning field names
|
|
550
|
+
reasoning_fields = ["rationale", "reasoning", "thought", "chain_of_thought"]
|
|
551
|
+
for field in reasoning_fields:
|
|
552
|
+
if field in store:
|
|
553
|
+
attrs["dspy.cot.reasoning"] = str(store[field])[:1000]
|
|
554
|
+
break
|
|
555
|
+
|
|
556
|
+
# Extract final answer/output
|
|
557
|
+
for key, value in list(store.items())[:5]:
|
|
558
|
+
if key not in reasoning_fields:
|
|
559
|
+
if isinstance(value, str):
|
|
560
|
+
attrs[f"dspy.cot.output.{key}"] = value[:500]
|
|
561
|
+
|
|
562
|
+
except Exception as e:
|
|
563
|
+
logger.debug("Failed to extract chain_of_thought response attributes: %s", e)
|
|
564
|
+
|
|
565
|
+
return attrs
|
|
566
|
+
|
|
567
|
+
def _extract_react_response_attributes(self, result: Any) -> Dict[str, Any]:
|
|
568
|
+
"""Extract response attributes from ReAct execution.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
result: The ReAct execution result
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
Dict[str, Any]: Dictionary of response attributes.
|
|
575
|
+
"""
|
|
576
|
+
attrs = {}
|
|
577
|
+
|
|
578
|
+
try:
|
|
579
|
+
# Extract actions and observations
|
|
580
|
+
if hasattr(result, "_store"):
|
|
581
|
+
store = result._store
|
|
582
|
+
|
|
583
|
+
# Look for action/observation traces
|
|
584
|
+
if "trajectory" in store:
|
|
585
|
+
attrs["dspy.react.has_trajectory"] = True
|
|
586
|
+
|
|
587
|
+
# Extract final answer
|
|
588
|
+
for key, value in list(store.items())[:5]:
|
|
589
|
+
if isinstance(value, str):
|
|
590
|
+
attrs[f"dspy.react.output.{key}"] = value[:500]
|
|
591
|
+
|
|
592
|
+
except Exception as e:
|
|
593
|
+
logger.debug("Failed to extract react response attributes: %s", e)
|
|
594
|
+
|
|
595
|
+
return attrs
|
|
596
|
+
|
|
597
|
+
def _extract_optimizer_response_attributes(self, result: Any) -> Dict[str, Any]:
|
|
598
|
+
"""Extract response attributes from optimizer compile.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
result: The compiled program
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
Dict[str, Any]: Dictionary of response attributes.
|
|
605
|
+
"""
|
|
606
|
+
attrs = {}
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
# The result is the compiled/optimized program
|
|
610
|
+
if hasattr(result, "__class__"):
|
|
611
|
+
attrs["dspy.optimizer.result_type"] = result.__class__.__name__
|
|
612
|
+
|
|
613
|
+
# Check if program has demos/examples after optimization
|
|
614
|
+
if hasattr(result, "demos"):
|
|
615
|
+
demos = result.demos
|
|
616
|
+
if demos:
|
|
617
|
+
attrs["dspy.optimizer.demos_count"] = len(demos)
|
|
618
|
+
|
|
619
|
+
except Exception as e:
|
|
620
|
+
logger.debug("Failed to extract optimizer response attributes: %s", e)
|
|
621
|
+
|
|
622
|
+
return attrs
|
|
623
|
+
|
|
624
|
+
def _extract_usage(self, result) -> Optional[Dict[str, int]]:
|
|
625
|
+
"""Extract token usage from DSPy result.
|
|
626
|
+
|
|
627
|
+
Note: DSPy tracks usage via internal usage_tracker.
|
|
628
|
+
Token usage is captured by underlying LM provider instrumentors.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
result: The DSPy operation result.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Optional[Dict[str, int]]: Dictionary with token counts or None.
|
|
635
|
+
"""
|
|
636
|
+
# DSPy's usage tracking is internal and aggregated
|
|
637
|
+
# Token usage is tracked by underlying LM instrumentors (OpenAI, Anthropic, etc.)
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
def _extract_finish_reason(self, result) -> Optional[str]:
|
|
641
|
+
"""Extract finish reason from DSPy result.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
result: The DSPy operation result.
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
Optional[str]: The finish reason string or None if not available.
|
|
648
|
+
"""
|
|
649
|
+
try:
|
|
650
|
+
# Check if result has finish reason
|
|
651
|
+
if hasattr(result, "_store") and "finish_reason" in result._store:
|
|
652
|
+
return result._store["finish_reason"]
|
|
653
|
+
|
|
654
|
+
# For successful predictions, assume completion
|
|
655
|
+
if hasattr(result, "_store") and result._store:
|
|
656
|
+
return "completed"
|
|
657
|
+
|
|
658
|
+
except Exception as e:
|
|
659
|
+
logger.debug("Failed to extract finish reason: %s", e)
|
|
660
|
+
|
|
661
|
+
return None
|