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,311 @@
1
+ """OpenTelemetry instrumentor for the CrewAI framework.
2
+
3
+ This instrumentor automatically traces crew execution, agents, tasks, and
4
+ collaborative workflows using the CrewAI multi-agent framework.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from typing import Any, Dict, Optional
10
+
11
+ from ..config import OTelConfig
12
+ from .base import BaseInstrumentor
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CrewAIInstrumentor(BaseInstrumentor):
18
+ """Instrumentor for CrewAI multi-agent framework"""
19
+
20
+ def __init__(self):
21
+ """Initialize the instrumentor."""
22
+ super().__init__()
23
+ self._crewai_available = False
24
+ self._check_availability()
25
+
26
+ def _check_availability(self):
27
+ """Check if CrewAI library is available."""
28
+ try:
29
+ import crewai
30
+
31
+ self._crewai_available = True
32
+ logger.debug("CrewAI library detected and available for instrumentation")
33
+ except ImportError:
34
+ logger.debug("CrewAI library not installed, instrumentation will be skipped")
35
+ self._crewai_available = False
36
+
37
+ def instrument(self, config: OTelConfig):
38
+ """Instrument CrewAI framework if available.
39
+
40
+ Args:
41
+ config (OTelConfig): The OpenTelemetry configuration object.
42
+ """
43
+ if not self._crewai_available:
44
+ logger.debug("Skipping CrewAI instrumentation - library not available")
45
+ return
46
+
47
+ self.config = config
48
+
49
+ try:
50
+ import crewai
51
+ import wrapt
52
+
53
+ # Instrument Crew.kickoff() method (main execution entry point)
54
+ if hasattr(crewai, "Crew"):
55
+ if hasattr(crewai.Crew, "kickoff"):
56
+ original_kickoff = crewai.Crew.kickoff
57
+ crewai.Crew.kickoff = wrapt.FunctionWrapper(
58
+ original_kickoff, self._wrap_crew_kickoff
59
+ )
60
+
61
+ self._instrumented = True
62
+ logger.info("CrewAI instrumentation enabled")
63
+
64
+ except Exception as e:
65
+ logger.error("Failed to instrument CrewAI: %s", e, exc_info=True)
66
+ if config.fail_on_error:
67
+ raise
68
+
69
+ def _wrap_crew_kickoff(self, wrapped, instance, args, kwargs):
70
+ """Wrap Crew.kickoff() method with span.
71
+
72
+ Args:
73
+ wrapped: The original method.
74
+ instance: The Crew instance.
75
+ args: Positional arguments.
76
+ kwargs: Keyword arguments.
77
+ """
78
+ return self.create_span_wrapper(
79
+ span_name="crewai.crew.execution",
80
+ extract_attributes=self._extract_crew_attributes,
81
+ )(wrapped)(instance, *args, **kwargs)
82
+
83
+ def _extract_crew_attributes(self, instance: Any, args: Any, kwargs: Any) -> Dict[str, Any]:
84
+ """Extract attributes from Crew.kickoff() call.
85
+
86
+ Args:
87
+ instance: The Crew instance.
88
+ args: Positional arguments.
89
+ kwargs: Keyword arguments.
90
+
91
+ Returns:
92
+ Dict[str, Any]: Dictionary of attributes to set on the span.
93
+ """
94
+ attrs = {}
95
+
96
+ # Core attributes
97
+ attrs["gen_ai.system"] = "crewai"
98
+ attrs["gen_ai.operation.name"] = "crew.execution"
99
+
100
+ # Extract crew ID if available
101
+ if hasattr(instance, "id"):
102
+ attrs["crewai.crew.id"] = str(instance.id)
103
+
104
+ # Extract crew name if available
105
+ if hasattr(instance, "name"):
106
+ attrs["crewai.crew.name"] = instance.name
107
+
108
+ # Extract process type (sequential, hierarchical, etc.)
109
+ if hasattr(instance, "process"):
110
+ process = instance.process
111
+ # Process might be an enum or string
112
+ process_type = str(process).split(".")[-1] if hasattr(process, "name") else str(process)
113
+ attrs["crewai.process.type"] = process_type
114
+
115
+ # Extract agents
116
+ if hasattr(instance, "agents") and instance.agents:
117
+ try:
118
+ agent_count = len(instance.agents)
119
+ attrs["crewai.agent_count"] = agent_count
120
+
121
+ # Extract agent roles
122
+ agent_roles = []
123
+ agent_goals = []
124
+ for agent in instance.agents:
125
+ if hasattr(agent, "role"):
126
+ agent_roles.append(str(agent.role)[:100]) # Truncate long roles
127
+ if hasattr(agent, "goal"):
128
+ agent_goals.append(str(agent.goal)[:100]) # Truncate long goals
129
+
130
+ if agent_roles:
131
+ attrs["crewai.agent.roles"] = agent_roles
132
+ if agent_goals:
133
+ attrs["crewai.agent.goals"] = agent_goals
134
+
135
+ # Extract tools from agents
136
+ all_tools = []
137
+ for agent in instance.agents:
138
+ if hasattr(agent, "tools") and agent.tools:
139
+ for tool in agent.tools:
140
+ tool_name = str(getattr(tool, "name", type(tool).__name__))
141
+ if tool_name and tool_name not in all_tools:
142
+ all_tools.append(tool_name)
143
+
144
+ if all_tools:
145
+ attrs["crewai.tools"] = all_tools[:10] # Limit to 10 tools
146
+ attrs["crewai.tool_count"] = len(all_tools)
147
+
148
+ except Exception as e:
149
+ logger.debug("Failed to extract agent information: %s", e)
150
+
151
+ # Extract tasks
152
+ if hasattr(instance, "tasks") and instance.tasks:
153
+ try:
154
+ task_count = len(instance.tasks)
155
+ attrs["crewai.task_count"] = task_count
156
+
157
+ # Extract task descriptions (truncated)
158
+ task_descriptions = []
159
+ for task in instance.tasks:
160
+ if hasattr(task, "description"):
161
+ desc = str(task.description)[:100] # Truncate
162
+ task_descriptions.append(desc)
163
+
164
+ if task_descriptions:
165
+ attrs["crewai.task.descriptions"] = task_descriptions
166
+
167
+ except Exception as e:
168
+ logger.debug("Failed to extract task information: %s", e)
169
+
170
+ # Extract verbose setting
171
+ if hasattr(instance, "verbose"):
172
+ attrs["crewai.verbose"] = instance.verbose
173
+
174
+ # Extract inputs passed to kickoff (first positional arg or 'inputs' kwarg)
175
+ inputs = None
176
+ if len(args) > 0:
177
+ inputs = args[0]
178
+ elif "inputs" in kwargs:
179
+ inputs = kwargs["inputs"]
180
+
181
+ if inputs:
182
+ try:
183
+ if isinstance(inputs, dict):
184
+ # Store input keys and truncated values
185
+ input_keys = list(inputs.keys())
186
+ attrs["crewai.inputs.keys"] = input_keys
187
+
188
+ # Store input values (truncated)
189
+ for key, value in list(inputs.items())[:5]: # Limit to 5 inputs
190
+ value_str = str(value)[:200] # Truncate long values
191
+ attrs[f"crewai.inputs.{key}"] = value_str
192
+ else:
193
+ # Non-dict inputs
194
+ attrs["crewai.inputs"] = str(inputs)[:200]
195
+ except Exception as e:
196
+ logger.debug("Failed to extract inputs: %s", e)
197
+
198
+ # Extract manager agent for hierarchical process
199
+ if hasattr(instance, "manager_agent") and instance.manager_agent:
200
+ try:
201
+ if hasattr(instance.manager_agent, "role"):
202
+ attrs["crewai.manager.role"] = str(instance.manager_agent.role)[:100]
203
+ except Exception as e:
204
+ logger.debug("Failed to extract manager agent: %s", e)
205
+
206
+ return attrs
207
+
208
+ def _extract_usage(self, result) -> Optional[Dict[str, int]]:
209
+ """Extract token usage from crew execution result.
210
+
211
+ Note: CrewAI doesn't directly expose token usage in the result.
212
+ Token usage is captured by underlying LLM provider instrumentors.
213
+
214
+ Args:
215
+ result: The crew execution result.
216
+
217
+ Returns:
218
+ Optional[Dict[str, int]]: Dictionary with token counts or None.
219
+ """
220
+ # CrewAI doesn't directly expose usage in the result
221
+ # Token usage is captured by LLM provider instrumentors (OpenAI, Anthropic, etc.)
222
+ # We could try to aggregate if CrewAI provides usage info in the future
223
+ if hasattr(result, "token_usage"):
224
+ try:
225
+ usage = result.token_usage
226
+ return {
227
+ "prompt_tokens": getattr(usage, "prompt_tokens", 0),
228
+ "completion_tokens": getattr(usage, "completion_tokens", 0),
229
+ "total_tokens": getattr(usage, "total_tokens", 0),
230
+ }
231
+ except Exception as e:
232
+ logger.debug("Failed to extract token usage: %s", e)
233
+ return None
234
+
235
+ def _extract_response_attributes(self, result) -> Dict[str, Any]:
236
+ """Extract response attributes from crew execution result.
237
+
238
+ Args:
239
+ result: The crew execution result.
240
+
241
+ Returns:
242
+ Dict[str, Any]: Dictionary of response attributes.
243
+ """
244
+ attrs = {}
245
+
246
+ # CrewAI result can be a string or a CrewOutput object
247
+ try:
248
+ # If result is a string (common case)
249
+ if isinstance(result, str):
250
+ output = result[:500] # Truncate to avoid span size issues
251
+ attrs["crewai.output"] = output
252
+ attrs["crewai.output_length"] = len(result)
253
+
254
+ # If result is a CrewOutput object
255
+ elif hasattr(result, "raw"):
256
+ output = str(result.raw)[:500]
257
+ attrs["crewai.output"] = output
258
+ attrs["crewai.output_length"] = len(str(result.raw))
259
+
260
+ # If result has tasks_output attribute (list of task results)
261
+ if hasattr(result, "tasks_output"):
262
+ try:
263
+ tasks_output = result.tasks_output
264
+ if tasks_output:
265
+ attrs["crewai.tasks_completed"] = len(tasks_output)
266
+
267
+ # Extract output from each task
268
+ task_outputs = []
269
+ for idx, task_output in enumerate(tasks_output[:5]): # Limit to 5 tasks
270
+ if hasattr(task_output, "raw"):
271
+ task_result = str(task_output.raw)[:200] # Truncate
272
+ task_outputs.append(task_result)
273
+
274
+ if task_outputs:
275
+ attrs["crewai.task_outputs"] = task_outputs
276
+ except Exception as e:
277
+ logger.debug("Failed to extract tasks_output: %s", e)
278
+
279
+ # If result has json attribute
280
+ if hasattr(result, "json"):
281
+ try:
282
+ attrs["crewai.output.json"] = str(result.json)[:500]
283
+ except Exception as e:
284
+ logger.debug("Failed to extract JSON output: %s", e)
285
+
286
+ # If result has pydantic attribute
287
+ if hasattr(result, "pydantic"):
288
+ try:
289
+ attrs["crewai.output.pydantic"] = str(result.pydantic)[:500]
290
+ except Exception as e:
291
+ logger.debug("Failed to extract Pydantic output: %s", e)
292
+
293
+ except Exception as e:
294
+ logger.debug("Failed to extract response attributes: %s", e)
295
+
296
+ return attrs
297
+
298
+ def _extract_finish_reason(self, result) -> Optional[str]:
299
+ """Extract finish reason from crew execution result.
300
+
301
+ Args:
302
+ result: The crew execution result.
303
+
304
+ Returns:
305
+ Optional[str]: The finish reason string or None if not available.
306
+ """
307
+ # CrewAI doesn't typically provide a finish_reason
308
+ # We could infer completion status
309
+ if result:
310
+ return "completed"
311
+ return None