genai-otel-instrument 0.1.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. genai_otel/__init__.py +132 -0
  2. genai_otel/__version__.py +34 -0
  3. genai_otel/auto_instrument.py +602 -0
  4. genai_otel/cli.py +92 -0
  5. genai_otel/config.py +333 -0
  6. genai_otel/cost_calculator.py +467 -0
  7. genai_otel/cost_enriching_exporter.py +207 -0
  8. genai_otel/cost_enrichment_processor.py +174 -0
  9. genai_otel/evaluation/__init__.py +76 -0
  10. genai_otel/evaluation/bias_detector.py +364 -0
  11. genai_otel/evaluation/config.py +261 -0
  12. genai_otel/evaluation/hallucination_detector.py +525 -0
  13. genai_otel/evaluation/pii_detector.py +356 -0
  14. genai_otel/evaluation/prompt_injection_detector.py +262 -0
  15. genai_otel/evaluation/restricted_topics_detector.py +316 -0
  16. genai_otel/evaluation/span_processor.py +962 -0
  17. genai_otel/evaluation/toxicity_detector.py +406 -0
  18. genai_otel/exceptions.py +17 -0
  19. genai_otel/gpu_metrics.py +516 -0
  20. genai_otel/instrumentors/__init__.py +71 -0
  21. genai_otel/instrumentors/anthropic_instrumentor.py +134 -0
  22. genai_otel/instrumentors/anyscale_instrumentor.py +27 -0
  23. genai_otel/instrumentors/autogen_instrumentor.py +394 -0
  24. genai_otel/instrumentors/aws_bedrock_instrumentor.py +94 -0
  25. genai_otel/instrumentors/azure_openai_instrumentor.py +69 -0
  26. genai_otel/instrumentors/base.py +919 -0
  27. genai_otel/instrumentors/bedrock_agents_instrumentor.py +398 -0
  28. genai_otel/instrumentors/cohere_instrumentor.py +140 -0
  29. genai_otel/instrumentors/crewai_instrumentor.py +311 -0
  30. genai_otel/instrumentors/dspy_instrumentor.py +661 -0
  31. genai_otel/instrumentors/google_ai_instrumentor.py +310 -0
  32. genai_otel/instrumentors/groq_instrumentor.py +106 -0
  33. genai_otel/instrumentors/guardrails_ai_instrumentor.py +510 -0
  34. genai_otel/instrumentors/haystack_instrumentor.py +503 -0
  35. genai_otel/instrumentors/huggingface_instrumentor.py +399 -0
  36. genai_otel/instrumentors/hyperbolic_instrumentor.py +236 -0
  37. genai_otel/instrumentors/instructor_instrumentor.py +425 -0
  38. genai_otel/instrumentors/langchain_instrumentor.py +340 -0
  39. genai_otel/instrumentors/langgraph_instrumentor.py +328 -0
  40. genai_otel/instrumentors/llamaindex_instrumentor.py +36 -0
  41. genai_otel/instrumentors/mistralai_instrumentor.py +315 -0
  42. genai_otel/instrumentors/ollama_instrumentor.py +197 -0
  43. genai_otel/instrumentors/ollama_server_metrics_poller.py +336 -0
  44. genai_otel/instrumentors/openai_agents_instrumentor.py +291 -0
  45. genai_otel/instrumentors/openai_instrumentor.py +260 -0
  46. genai_otel/instrumentors/pydantic_ai_instrumentor.py +362 -0
  47. genai_otel/instrumentors/replicate_instrumentor.py +87 -0
  48. genai_otel/instrumentors/sambanova_instrumentor.py +196 -0
  49. genai_otel/instrumentors/togetherai_instrumentor.py +146 -0
  50. genai_otel/instrumentors/vertexai_instrumentor.py +106 -0
  51. genai_otel/llm_pricing.json +1676 -0
  52. genai_otel/logging_config.py +45 -0
  53. genai_otel/mcp_instrumentors/__init__.py +14 -0
  54. genai_otel/mcp_instrumentors/api_instrumentor.py +144 -0
  55. genai_otel/mcp_instrumentors/base.py +105 -0
  56. genai_otel/mcp_instrumentors/database_instrumentor.py +336 -0
  57. genai_otel/mcp_instrumentors/kafka_instrumentor.py +31 -0
  58. genai_otel/mcp_instrumentors/manager.py +139 -0
  59. genai_otel/mcp_instrumentors/redis_instrumentor.py +31 -0
  60. genai_otel/mcp_instrumentors/vector_db_instrumentor.py +265 -0
  61. genai_otel/metrics.py +148 -0
  62. genai_otel/py.typed +2 -0
  63. genai_otel/server_metrics.py +197 -0
  64. genai_otel_instrument-0.1.24.dist-info/METADATA +1404 -0
  65. genai_otel_instrument-0.1.24.dist-info/RECORD +69 -0
  66. genai_otel_instrument-0.1.24.dist-info/WHEEL +5 -0
  67. genai_otel_instrument-0.1.24.dist-info/entry_points.txt +2 -0
  68. genai_otel_instrument-0.1.24.dist-info/licenses/LICENSE +680 -0
  69. genai_otel_instrument-0.1.24.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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