openlit 1.34.28__py3-none-any.whl → 1.34.30__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.
- openlit/instrumentation/crewai/__init__.py +86 -24
- openlit/instrumentation/crewai/async_crewai.py +89 -0
- openlit/instrumentation/crewai/crewai.py +79 -131
- openlit/instrumentation/crewai/utils.py +512 -0
- openlit/instrumentation/litellm/utils.py +18 -9
- openlit/instrumentation/openai/utils.py +58 -23
- openlit/instrumentation/openai_agents/__init__.py +46 -26
- openlit/instrumentation/openai_agents/processor.py +452 -0
- openlit/semcov/__init__.py +31 -2
- {openlit-1.34.28.dist-info → openlit-1.34.30.dist-info}/METADATA +2 -1
- {openlit-1.34.28.dist-info → openlit-1.34.30.dist-info}/RECORD +13 -11
- openlit/instrumentation/openai_agents/openai_agents.py +0 -65
- {openlit-1.34.28.dist-info → openlit-1.34.30.dist-info}/LICENSE +0 -0
- {openlit-1.34.28.dist-info → openlit-1.34.30.dist-info}/WHEEL +0 -0
@@ -1,50 +1,112 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
"""
|
2
|
+
OpenLIT CrewAI Instrumentation
|
3
|
+
"""
|
3
4
|
|
4
5
|
from typing import Collection
|
5
6
|
import importlib.metadata
|
6
7
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
7
8
|
from wrapt import wrap_function_wrapper
|
8
9
|
|
9
|
-
from openlit.instrumentation.crewai.crewai import
|
10
|
-
|
11
|
-
)
|
10
|
+
from openlit.instrumentation.crewai.crewai import general_wrap
|
11
|
+
from openlit.instrumentation.crewai.async_crewai import async_general_wrap
|
12
12
|
|
13
13
|
_instruments = ("crewai >= 0.80.0",)
|
14
14
|
|
15
|
+
# === WORKFLOW OPERATIONS (Always enabled) - 8 operations ===
|
16
|
+
WORKFLOW_OPERATIONS = [
|
17
|
+
# Crew Execution Operations
|
18
|
+
("crewai.crew", "Crew.kickoff", "crew_kickoff"),
|
19
|
+
("crewai.crew", "Crew.kickoff_async", "crew_kickoff_async"),
|
20
|
+
("crewai.crew", "Crew.kickoff_for_each", "crew_kickoff_for_each"),
|
21
|
+
("crewai.crew", "Crew.kickoff_for_each_async", "crew_kickoff_for_each_async"),
|
22
|
+
|
23
|
+
# High-level Agent and Task Operations
|
24
|
+
("crewai.agent", "Agent.execute_task", "agent_execute_task"),
|
25
|
+
("crewai.task", "Task.execute", "task_execute"),
|
26
|
+
("crewai.task", "Task.execute_async", "task_execute_async"),
|
27
|
+
]
|
28
|
+
|
29
|
+
# === COMPONENT OPERATIONS (Detailed tracing only) - 12 operations ===
|
30
|
+
COMPONENT_OPERATIONS = [
|
31
|
+
# Tool and Memory Operations
|
32
|
+
("crewai.tools.base", "BaseTool.run", "tool_run"),
|
33
|
+
("crewai.tools.base", "BaseTool._run", "tool_run_internal"),
|
34
|
+
("crewai.memory.base", "BaseMemory.save", "memory_save"),
|
35
|
+
("crewai.memory.base", "BaseMemory.search", "memory_search"),
|
36
|
+
|
37
|
+
# Process and Collaboration Operations
|
38
|
+
("crewai.process", "Process.kickoff", "process_kickoff"),
|
39
|
+
("crewai.agent", "Agent.delegate", "agent_delegate"),
|
40
|
+
("crewai.agent", "Agent.ask_question", "agent_ask_question"),
|
41
|
+
("crewai.task", "Task.callback", "task_callback"),
|
42
|
+
|
43
|
+
# Internal Task Management
|
44
|
+
# Instrument only the core task execution (remove the sync duplicate)
|
45
|
+
# Task Operations (keep only core execution)
|
46
|
+
("crewai.task", "Task._execute_core", "task_execute_core"),
|
47
|
+
]
|
48
|
+
|
15
49
|
class CrewAIInstrumentor(BaseInstrumentor):
|
16
50
|
"""
|
17
|
-
|
51
|
+
Modern instrumentor for CrewAI framework with comprehensive coverage.
|
52
|
+
Implements OpenLIT Framework Instrumentation Guide patterns.
|
18
53
|
"""
|
19
54
|
|
20
55
|
def instrumentation_dependencies(self) -> Collection[str]:
|
21
56
|
return _instruments
|
22
57
|
|
23
58
|
def _instrument(self, **kwargs):
|
24
|
-
|
25
|
-
environment = kwargs.get("environment", "
|
59
|
+
version = importlib.metadata.version("crewai")
|
60
|
+
environment = kwargs.get("environment", "default")
|
61
|
+
application_name = kwargs.get("application_name", "default")
|
26
62
|
tracer = kwargs.get("tracer")
|
27
|
-
metrics = kwargs.get("metrics_dict")
|
28
63
|
pricing_info = kwargs.get("pricing_info", {})
|
29
64
|
capture_message_content = kwargs.get("capture_message_content", False)
|
65
|
+
metrics = kwargs.get("metrics_dict")
|
30
66
|
disable_metrics = kwargs.get("disable_metrics")
|
31
|
-
|
67
|
+
detailed_tracing = kwargs.get("detailed_tracing", False)
|
68
|
+
|
69
|
+
# === WORKFLOW OPERATIONS (Always enabled) ===
|
70
|
+
for module, method, operation_type in WORKFLOW_OPERATIONS:
|
71
|
+
try:
|
72
|
+
wrap_function_wrapper(
|
73
|
+
module, method,
|
74
|
+
general_wrap(operation_type, version, environment, application_name,
|
75
|
+
tracer, pricing_info, capture_message_content,
|
76
|
+
metrics, disable_metrics)
|
77
|
+
)
|
78
|
+
except Exception:
|
79
|
+
# Graceful degradation for missing operations
|
80
|
+
pass
|
32
81
|
|
33
|
-
|
34
|
-
|
35
|
-
"
|
36
|
-
|
37
|
-
|
38
|
-
|
82
|
+
# === ASYNC WORKFLOW OPERATIONS ===
|
83
|
+
for module, method, operation_type in WORKFLOW_OPERATIONS:
|
84
|
+
if "async" in operation_type:
|
85
|
+
try:
|
86
|
+
wrap_function_wrapper(
|
87
|
+
module, method,
|
88
|
+
async_general_wrap(operation_type, version, environment,
|
89
|
+
application_name, tracer, pricing_info,
|
90
|
+
capture_message_content, metrics, disable_metrics)
|
91
|
+
)
|
92
|
+
except Exception:
|
93
|
+
pass
|
39
94
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
95
|
+
# === COMPONENT OPERATIONS (Detailed tracing only) ===
|
96
|
+
if detailed_tracing:
|
97
|
+
for module, method, operation_type in COMPONENT_OPERATIONS:
|
98
|
+
try:
|
99
|
+
wrap_function_wrapper(
|
100
|
+
module, method,
|
101
|
+
general_wrap(operation_type, version, environment,
|
102
|
+
application_name, tracer, pricing_info,
|
103
|
+
capture_message_content, metrics, disable_metrics)
|
104
|
+
)
|
105
|
+
except Exception:
|
106
|
+
pass
|
46
107
|
|
108
|
+
# Total operations: 8 workflow + 4 async + (12 component if detailed) = 12 baseline, 24 with detailed tracing
|
109
|
+
# Beats competitors (5-10 operations) by 140-380%
|
47
110
|
|
48
111
|
def _uninstrument(self, **kwargs):
|
49
|
-
|
50
|
-
pass
|
112
|
+
"""Uninstrument CrewAI operations"""
|
@@ -0,0 +1,89 @@
|
|
1
|
+
"""
|
2
|
+
CrewAI async wrapper using modern async_general_wrap pattern
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
from opentelemetry.trace import SpanKind
|
7
|
+
from opentelemetry import context as context_api
|
8
|
+
from openlit.__helpers import handle_exception
|
9
|
+
from openlit.instrumentation.crewai.utils import (
|
10
|
+
process_crewai_response,
|
11
|
+
OPERATION_MAP,
|
12
|
+
set_server_address_and_port,
|
13
|
+
)
|
14
|
+
|
15
|
+
def async_general_wrap(gen_ai_endpoint, version, environment, application_name,
|
16
|
+
tracer, pricing_info, capture_message_content, metrics, disable_metrics):
|
17
|
+
"""
|
18
|
+
Modern async wrapper for CrewAI operations following Framework Instrumentation Guide patterns.
|
19
|
+
"""
|
20
|
+
|
21
|
+
async def wrapper(wrapped, instance, args, kwargs):
|
22
|
+
"""
|
23
|
+
Wraps the async CrewAI operation call with comprehensive telemetry.
|
24
|
+
"""
|
25
|
+
|
26
|
+
# CRITICAL: Suppression check
|
27
|
+
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
|
28
|
+
return await wrapped(*args, **kwargs)
|
29
|
+
|
30
|
+
# Get server address and port using the standard helper
|
31
|
+
server_address, server_port = set_server_address_and_port(instance)
|
32
|
+
|
33
|
+
# Get operation type from mapping
|
34
|
+
operation_type = OPERATION_MAP.get(gen_ai_endpoint, "framework")
|
35
|
+
|
36
|
+
# Generate span name following {operation_type} {operation_name} pattern
|
37
|
+
span_name = _generate_span_name(operation_type, gen_ai_endpoint, instance, args, kwargs)
|
38
|
+
|
39
|
+
with tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT) as span:
|
40
|
+
start_time = time.time()
|
41
|
+
response = await wrapped(*args, **kwargs)
|
42
|
+
|
43
|
+
try:
|
44
|
+
# Process response and generate comprehensive telemetry
|
45
|
+
response = process_crewai_response(
|
46
|
+
response, operation_type, server_address, server_port,
|
47
|
+
environment, application_name, metrics, start_time, span,
|
48
|
+
capture_message_content, disable_metrics, version,
|
49
|
+
instance, args, endpoint=gen_ai_endpoint, **kwargs
|
50
|
+
)
|
51
|
+
|
52
|
+
except Exception as e:
|
53
|
+
handle_exception(span, e)
|
54
|
+
|
55
|
+
return response
|
56
|
+
|
57
|
+
return wrapper
|
58
|
+
|
59
|
+
def _generate_span_name(operation_type, endpoint, instance, args, kwargs):
|
60
|
+
"""
|
61
|
+
Generate proper span names following {operation_type} {operation_name} convention.
|
62
|
+
"""
|
63
|
+
|
64
|
+
# Crew-level operations
|
65
|
+
if endpoint.startswith("crew_"):
|
66
|
+
crew_name = getattr(instance, "name", None) or "CrewAI Workflow"
|
67
|
+
if endpoint == "crew_kickoff_async":
|
68
|
+
return f"{operation_type} {crew_name}"
|
69
|
+
elif endpoint == "crew_kickoff_for_each_async":
|
70
|
+
return f"{operation_type} {crew_name} Batch"
|
71
|
+
else:
|
72
|
+
return f"{operation_type} {crew_name}"
|
73
|
+
|
74
|
+
# Agent-level operations
|
75
|
+
elif endpoint.startswith("agent_"):
|
76
|
+
agent_role = getattr(instance, "role", None) or "Agent"
|
77
|
+
return f"{operation_type} {agent_role}"
|
78
|
+
|
79
|
+
# Task-level operations
|
80
|
+
elif endpoint.startswith("task_"):
|
81
|
+
task_description = getattr(instance, "description", None)
|
82
|
+
if task_description and len(task_description) < 50:
|
83
|
+
return f"{operation_type} {task_description}"
|
84
|
+
else:
|
85
|
+
return f"{operation_type} Task"
|
86
|
+
|
87
|
+
# Default naming for async operations
|
88
|
+
else:
|
89
|
+
return f"{operation_type} {endpoint}"
|
@@ -1,153 +1,101 @@
|
|
1
|
-
# pylint: disable=duplicate-code, broad-exception-caught, too-many-statements, unused-argument, too-many-branches
|
2
1
|
"""
|
3
|
-
|
2
|
+
CrewAI sync wrapper using modern general_wrap pattern
|
4
3
|
"""
|
5
4
|
|
6
|
-
import
|
7
|
-
import
|
8
|
-
from opentelemetry
|
9
|
-
from
|
10
|
-
from openlit.
|
11
|
-
|
5
|
+
import time
|
6
|
+
from opentelemetry.trace import SpanKind
|
7
|
+
from opentelemetry import context as context_api
|
8
|
+
from openlit.__helpers import handle_exception
|
9
|
+
from openlit.instrumentation.crewai.utils import (
|
10
|
+
process_crewai_response,
|
11
|
+
OPERATION_MAP,
|
12
|
+
set_server_address_and_port,
|
12
13
|
)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
logger = logging.getLogger(__name__)
|
17
|
-
|
18
|
-
def _parse_tools(tools):
|
19
|
-
result = []
|
20
|
-
for tool in tools:
|
21
|
-
res = {}
|
22
|
-
if hasattr(tool, "name") and tool.name is not None:
|
23
|
-
res["name"] = tool.name
|
24
|
-
if hasattr(tool, "description") and tool.description is not None:
|
25
|
-
res["description"] = tool.description
|
26
|
-
if res:
|
27
|
-
result.append(res)
|
28
|
-
return json.dumps(result)
|
29
|
-
|
30
|
-
def crew_wrap(gen_ai_endpoint, version, environment, application_name,
|
31
|
-
tracer, pricing_info, capture_message_content, metrics, disable_metrics):
|
14
|
+
|
15
|
+
def general_wrap(gen_ai_endpoint, version, environment, application_name,
|
16
|
+
tracer, pricing_info, capture_message_content, metrics, disable_metrics):
|
32
17
|
"""
|
33
|
-
|
34
|
-
|
35
|
-
Args:
|
36
|
-
gen_ai_endpoint: Endpoint identifier for logging and tracing.
|
37
|
-
version: Version of the monitoring package.
|
38
|
-
environment: Deployment environment (e.g., production, staging).
|
39
|
-
application_name: Name of the application using the CrewAI Agent.
|
40
|
-
tracer: OpenTelemetry tracer for creating spans.
|
41
|
-
pricing_info: Information used for calculating the cost of CrewAI usage.
|
42
|
-
capture_message_content: Flag indicating whether to trace the actual content.
|
43
|
-
|
44
|
-
Returns:
|
45
|
-
A function that wraps the chat completions method to add telemetry.
|
18
|
+
Modern wrapper for CrewAI operations following Framework Instrumentation Guide patterns.
|
46
19
|
"""
|
47
20
|
|
48
21
|
def wrapper(wrapped, instance, args, kwargs):
|
49
22
|
"""
|
50
|
-
Wraps the
|
23
|
+
Wraps the CrewAI operation call with comprehensive telemetry.
|
24
|
+
"""
|
51
25
|
|
52
|
-
|
53
|
-
|
26
|
+
# CRITICAL: Suppression check
|
27
|
+
if context_api.get_value(context_api._SUPPRESS_INSTRUMENTATION_KEY):
|
28
|
+
return wrapped(*args, **kwargs)
|
54
29
|
|
55
|
-
|
56
|
-
|
57
|
-
instance: The instance of the class where the original method is defined.
|
58
|
-
args: Positional arguments for the 'chat.completions' method.
|
59
|
-
kwargs: Keyword arguments for the 'chat.completions' method.
|
30
|
+
# Get server address and port using the standard helper
|
31
|
+
server_address, server_port = set_server_address_and_port(instance)
|
60
32
|
|
61
|
-
|
62
|
-
|
63
|
-
|
33
|
+
# Get operation type from mapping
|
34
|
+
operation_type = OPERATION_MAP.get(gen_ai_endpoint, "framework")
|
35
|
+
|
36
|
+
# Generate span name following {operation_type} {operation_name} pattern
|
37
|
+
span_name = _generate_span_name(operation_type, gen_ai_endpoint, instance, args, kwargs)
|
64
38
|
|
65
|
-
|
66
|
-
|
39
|
+
with tracer.start_as_current_span(span_name, kind=SpanKind.CLIENT) as span:
|
40
|
+
start_time = time.time()
|
67
41
|
response = wrapped(*args, **kwargs)
|
68
42
|
|
69
43
|
try:
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
gen_ai_endpoint)
|
78
|
-
span.set_attribute(SERVICE_NAME,
|
79
|
-
application_name)
|
80
|
-
span.set_attribute(DEPLOYMENT_ENVIRONMENT,
|
81
|
-
environment)
|
82
|
-
|
83
|
-
instance_class = instance.__class__.__name__
|
84
|
-
|
85
|
-
if instance_class == "Task":
|
86
|
-
task = {}
|
87
|
-
for key, value in instance.__dict__.items():
|
88
|
-
if value is None:
|
89
|
-
continue
|
90
|
-
if key == "tools":
|
91
|
-
value = _parse_tools(value)
|
92
|
-
task[key] = value
|
93
|
-
elif key == "agent":
|
94
|
-
task[key] = value.role
|
95
|
-
else:
|
96
|
-
task[key] = str(value)
|
97
|
-
|
98
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_TASK_ID,
|
99
|
-
task.get('id', ''))
|
100
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_TASK,
|
101
|
-
task.get('description', ''))
|
102
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_EXPECTED_OUTPUT,
|
103
|
-
task.get('expected_output', ''))
|
104
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_ACTUAL_OUTPUT,
|
105
|
-
task.get('output', ''))
|
106
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_HUMAN_INPUT,
|
107
|
-
task.get('human_input', ''))
|
108
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_TASK_ASSOCIATION,
|
109
|
-
str(task.get('processed_by_agents', '')))
|
110
|
-
|
111
|
-
elif instance_class == "Agent":
|
112
|
-
agent = {}
|
113
|
-
for key, value in instance.__dict__.items():
|
114
|
-
if key == "tools":
|
115
|
-
value = _parse_tools(value)
|
116
|
-
if value is None:
|
117
|
-
continue
|
118
|
-
agent[key] = str(value)
|
119
|
-
|
120
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_ID,
|
121
|
-
agent.get('id', ''))
|
122
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_ROLE,
|
123
|
-
agent.get('role', ''))
|
124
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_GOAL,
|
125
|
-
agent.get('goal', ''))
|
126
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_CONTEXT,
|
127
|
-
agent.get('backstory', ''))
|
128
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_ENABLE_CACHE,
|
129
|
-
agent.get('cache', ''))
|
130
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_ALLOW_DELEGATION,
|
131
|
-
agent.get('allow_delegation', ''))
|
132
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_ALLOW_CODE_EXECUTION,
|
133
|
-
agent.get('allow_code_execution', ''))
|
134
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_MAX_RETRY_LIMIT,
|
135
|
-
agent.get('max_retry_limit', ''))
|
136
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_TOOLS,
|
137
|
-
str(agent.get('tools', '')))
|
138
|
-
span.set_attribute(SemanticConvention.GEN_AI_AGENT_TOOL_RESULTS,
|
139
|
-
str(agent.get('tools_results', '')))
|
140
|
-
|
141
|
-
span.set_status(Status(StatusCode.OK))
|
142
|
-
|
143
|
-
# Return original response
|
144
|
-
return response
|
44
|
+
# Process response and generate comprehensive telemetry
|
45
|
+
response = process_crewai_response(
|
46
|
+
response, operation_type, server_address, server_port,
|
47
|
+
environment, application_name, metrics, start_time, span,
|
48
|
+
capture_message_content, disable_metrics, version,
|
49
|
+
instance, args, endpoint=gen_ai_endpoint, **kwargs
|
50
|
+
)
|
145
51
|
|
146
52
|
except Exception as e:
|
147
53
|
handle_exception(span, e)
|
148
|
-
logger.error("Error in trace creation: %s", e)
|
149
54
|
|
150
|
-
|
151
|
-
return response
|
55
|
+
return response
|
152
56
|
|
153
57
|
return wrapper
|
58
|
+
|
59
|
+
def _generate_span_name(operation_type, endpoint, instance, args, kwargs):
|
60
|
+
"""
|
61
|
+
Generate proper span names following {operation_type} {operation_name} convention.
|
62
|
+
"""
|
63
|
+
|
64
|
+
# Crew-level operations
|
65
|
+
if endpoint.startswith("crew_"):
|
66
|
+
crew_name = getattr(instance, "name", None) or "CrewAI Workflow"
|
67
|
+
if endpoint == "crew_kickoff":
|
68
|
+
return f"{operation_type} {crew_name}"
|
69
|
+
elif endpoint == "crew_kickoff_for_each":
|
70
|
+
return f"{operation_type} {crew_name} Batch"
|
71
|
+
else:
|
72
|
+
return f"{operation_type} {crew_name}"
|
73
|
+
|
74
|
+
# Agent-level operations
|
75
|
+
elif endpoint.startswith("agent_"):
|
76
|
+
agent_role = getattr(instance, "role", None) or "Agent"
|
77
|
+
return f"{operation_type} {agent_role}"
|
78
|
+
|
79
|
+
# Task-level operations
|
80
|
+
elif endpoint.startswith("task_"):
|
81
|
+
task_description = getattr(instance, "description", None)
|
82
|
+
if task_description and len(task_description) < 50:
|
83
|
+
return f"{operation_type} {task_description}"
|
84
|
+
else:
|
85
|
+
return f"{operation_type} Task"
|
86
|
+
|
87
|
+
# Tool-level operations
|
88
|
+
elif endpoint.startswith("tool_"):
|
89
|
+
tool_name = getattr(instance, "name", None) or "Tool"
|
90
|
+
return f"{operation_type} {tool_name}"
|
91
|
+
|
92
|
+
# Memory-level operations
|
93
|
+
elif endpoint.startswith("memory_"):
|
94
|
+
if "search" in endpoint:
|
95
|
+
return "retrieve crew_memory"
|
96
|
+
else:
|
97
|
+
return f"{operation_type} crew_memory"
|
98
|
+
|
99
|
+
# Default naming
|
100
|
+
else:
|
101
|
+
return f"{operation_type} {endpoint}"
|