Skip to content

Commit e203d13

Browse files
authored
Refactor CrewAI instrumentation to enhance span attribute management (#926)
* Refactor CrewAI instrumentation to enhance span attribute management * Refactored CrewAISpanAttributes to improve handling of agent, crew, and task attributes. * transfer crew ai from third party * Refactor CrewAI instrumentation to enhance attribute management and update version to 0.36.0. Removed third-party dependencies and improved error handling in agent and task processing.
1 parent 0f83678 commit e203d13

File tree

17 files changed

+934
-387
lines changed

17 files changed

+934
-387
lines changed

agentops/instrumentation/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def get_instance(self) -> BaseInstrumentor:
6363
provider_import_name="anthropic",
6464
),
6565
InstrumentorLoader(
66-
module_name="opentelemetry.instrumentation.crewai",
66+
module_name="agentops.instrumentation.crewai",
6767
class_name="CrewAIInstrumentor",
6868
provider_import_name="crewai",
6969
),

third_party/opentelemetry/instrumentation/crewai/LICENSE renamed to agentops/instrumentation/crewai/LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,4 @@
198198
distributed under the License is distributed on an "AS IS" BASIS,
199199
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200200
See the License for the specific language governing permissions and
201-
limitations under the License.
201+
limitations under the License.

third_party/opentelemetry/instrumentation/crewai/NOTICE.md renamed to agentops/instrumentation/crewai/NOTICE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ Copyright notice from the original project:
66
Copyright (c) Traceloop (https://traceloop.com)
77

88
The Apache 2.0 license can be found in the LICENSE file in this directory.
9+
10+
This code has been modified and adapted for use in the AgentOps project.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""OpenTelemetry CrewAI instrumentation"""
2+
3+
from agentops.instrumentation.crewai.version import __version__
4+
from agentops.instrumentation.crewai.instrumentation import CrewAIInstrumentor
5+
6+
__all__ = ["CrewAIInstrumentor", "__version__"]
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
"""OpenTelemetry instrumentation for CrewAI."""
2+
3+
import json
4+
import logging
5+
from typing import Any
6+
from opentelemetry.trace import Span
7+
8+
from agentops.semconv.span_attributes import SpanAttributes
9+
from agentops.semconv.agent import AgentAttributes
10+
from agentops.semconv.tool import ToolAttributes
11+
from agentops.semconv.message import MessageAttributes
12+
13+
# Initialize logger for logging potential issues and operations
14+
logger = logging.getLogger(__name__)
15+
16+
def _parse_tools(tools):
17+
"""Parse tools into a JSON string with name and description."""
18+
result = []
19+
for tool in tools:
20+
res = {}
21+
if hasattr(tool, "name") and tool.name is not None:
22+
res["name"] = tool.name
23+
if hasattr(tool, "description") and tool.description is not None:
24+
res["description"] = tool.description
25+
if res:
26+
result.append(res)
27+
return result
28+
29+
def set_span_attribute(span: Span, key: str, value: Any) -> None:
30+
"""Set a single attribute on a span."""
31+
if value is not None and value != "":
32+
if hasattr(value, "__str__"):
33+
value = str(value)
34+
span.set_attribute(key, value)
35+
36+
37+
class CrewAISpanAttributes:
38+
"""Manages span attributes for CrewAI instrumentation."""
39+
40+
def __init__(self, span: Span, instance, skip_agent_processing=False) -> None:
41+
self.span = span
42+
self.instance = instance
43+
self.skip_agent_processing = skip_agent_processing
44+
self.process_instance()
45+
46+
def process_instance(self):
47+
"""Process the instance based on its type."""
48+
instance_type = self.instance.__class__.__name__
49+
self._set_attribute(SpanAttributes.LLM_SYSTEM, "crewai")
50+
self._set_attribute(SpanAttributes.AGENTOPS_ENTITY_NAME, instance_type)
51+
52+
method_mapping = {
53+
"Crew": self._process_crew,
54+
"Agent": self._process_agent,
55+
"Task": self._process_task,
56+
"LLM": self._process_llm,
57+
}
58+
method = method_mapping.get(instance_type)
59+
if method:
60+
method()
61+
62+
def _process_crew(self):
63+
"""Process a Crew instance."""
64+
crew_id = getattr(self.instance, "id", "")
65+
self._set_attribute("crewai.crew.id", str(crew_id))
66+
self._set_attribute("crewai.crew.type", "crewai.crew")
67+
self._set_attribute(SpanAttributes.AGENTOPS_SPAN_KIND, "workflow")
68+
69+
logger.debug(f"CrewAI: Processing crew with id {crew_id}")
70+
71+
for key, value in self.instance.__dict__.items():
72+
if value is None:
73+
continue
74+
75+
if key == "tasks":
76+
if isinstance(value, list):
77+
self._set_attribute("crewai.crew.max_turns", str(len(value)))
78+
logger.debug(f"CrewAI: Found {len(value)} tasks")
79+
elif key == "agents":
80+
if isinstance(value, list):
81+
logger.debug(f"CrewAI: Found {len(value)} agents in crew")
82+
83+
if not self.skip_agent_processing:
84+
self._parse_agents(value)
85+
elif key == "llms":
86+
self._parse_llms(value)
87+
elif key == "result":
88+
self._set_attribute("crewai.crew.final_output", str(value))
89+
self._set_attribute("crewai.crew.output", str(value))
90+
self._set_attribute(SpanAttributes.AGENTOPS_ENTITY_OUTPUT, str(value))
91+
else:
92+
self._set_attribute(f"crewai.crew.{key}", str(value))
93+
94+
def _process_agent(self):
95+
"""Process an Agent instance."""
96+
agent = {}
97+
self._set_attribute(SpanAttributes.AGENTOPS_SPAN_KIND, "agent")
98+
99+
for key, value in self.instance.__dict__.items():
100+
if key == "tools":
101+
parsed_tools = _parse_tools(value)
102+
for i, tool in enumerate(parsed_tools):
103+
tool_prefix = f"crewai.agent.tool.{i}."
104+
for tool_key, tool_value in tool.items():
105+
self._set_attribute(f"{tool_prefix}{tool_key}", str(tool_value))
106+
107+
agent[key] = json.dumps(parsed_tools)
108+
109+
if value is None:
110+
continue
111+
112+
if key != "tools":
113+
agent[key] = str(value)
114+
115+
self._set_attribute(AgentAttributes.AGENT_ID, agent.get('id', ''))
116+
self._set_attribute(AgentAttributes.AGENT_ROLE, agent.get('role', ''))
117+
self._set_attribute(AgentAttributes.AGENT_NAME, agent.get('name', ''))
118+
self._set_attribute(AgentAttributes.AGENT_TOOLS, agent.get('tools', ''))
119+
120+
if 'reasoning' in agent:
121+
self._set_attribute(AgentAttributes.AGENT_REASONING, agent.get('reasoning', ''))
122+
123+
if 'goal' in agent:
124+
self._set_attribute(SpanAttributes.AGENTOPS_ENTITY_INPUT, agent.get('goal', ''))
125+
126+
self._set_attribute("crewai.agent.goal", agent.get('goal', ''))
127+
self._set_attribute("crewai.agent.backstory", agent.get('backstory', ''))
128+
self._set_attribute("crewai.agent.cache", agent.get('cache', ''))
129+
self._set_attribute("crewai.agent.allow_delegation", agent.get('allow_delegation', ''))
130+
self._set_attribute("crewai.agent.allow_code_execution", agent.get('allow_code_execution', ''))
131+
self._set_attribute("crewai.agent.max_retry_limit", agent.get('max_retry_limit', ''))
132+
133+
if hasattr(self.instance, "llm") and self.instance.llm is not None:
134+
model_name = getattr(self.instance.llm, "model", None) or getattr(self.instance.llm, "model_name", None) or ""
135+
temp = getattr(self.instance.llm, "temperature", None)
136+
max_tokens = getattr(self.instance.llm, "max_tokens", None)
137+
top_p = getattr(self.instance.llm, "top_p", None)
138+
139+
self._set_attribute(SpanAttributes.LLM_REQUEST_MODEL, model_name)
140+
if temp is not None:
141+
self._set_attribute(SpanAttributes.LLM_REQUEST_TEMPERATURE, str(temp))
142+
if max_tokens is not None:
143+
self._set_attribute(SpanAttributes.LLM_REQUEST_MAX_TOKENS, str(max_tokens))
144+
if top_p is not None:
145+
self._set_attribute(SpanAttributes.LLM_REQUEST_TOP_P, str(top_p))
146+
147+
self._set_attribute("crewai.agent.llm", str(model_name))
148+
self._set_attribute(AgentAttributes.AGENT_MODELS, str(model_name))
149+
150+
def _process_task(self):
151+
"""Process a Task instance."""
152+
task = {}
153+
self._set_attribute(SpanAttributes.AGENTOPS_SPAN_KIND, "workflow.step")
154+
155+
for key, value in self.instance.__dict__.items():
156+
if value is None:
157+
continue
158+
if key == "tools":
159+
parsed_tools = _parse_tools(value)
160+
for i, tool in enumerate(parsed_tools):
161+
tool_prefix = f"crewai.task.tool.{i}."
162+
for tool_key, tool_value in tool.items():
163+
self._set_attribute(f"{tool_prefix}{tool_key}", str(tool_value))
164+
165+
task[key] = json.dumps(parsed_tools)
166+
167+
elif key == "agent":
168+
task[key] = value.role if value else None
169+
if value:
170+
agent_id = getattr(value, "id", "")
171+
self._set_attribute(AgentAttributes.FROM_AGENT, str(agent_id))
172+
else:
173+
task[key] = str(value)
174+
175+
self._set_attribute("crewai.task.name", task.get('description', ''))
176+
self._set_attribute("crewai.task.type", "task")
177+
self._set_attribute("crewai.task.input", task.get('context', ''))
178+
self._set_attribute("crewai.task.expected_output", task.get('expected_output', ''))
179+
180+
if 'description' in task:
181+
self._set_attribute(SpanAttributes.AGENTOPS_ENTITY_INPUT, task.get('description', ''))
182+
if 'output' in task:
183+
self._set_attribute(SpanAttributes.AGENTOPS_ENTITY_OUTPUT, task.get('output', ''))
184+
self._set_attribute("crewai.task.output", task.get('output', ''))
185+
186+
if 'id' in task:
187+
self._set_attribute("crewai.task.id", str(task.get('id', '')))
188+
189+
if 'status' in task:
190+
self._set_attribute("crewai.task.status", task.get('status', ''))
191+
192+
self._set_attribute("crewai.task.agent", task.get('agent', ''))
193+
self._set_attribute("crewai.task.human_input", task.get('human_input', ''))
194+
self._set_attribute("crewai.task.processed_by_agents", str(task.get('processed_by_agents', '')))
195+
196+
if 'tools' in task and task['tools']:
197+
try:
198+
tools = json.loads(task['tools'])
199+
for i, tool in enumerate(tools):
200+
self._set_attribute(MessageAttributes.TOOL_CALL_NAME.format(i=i), tool.get("name", ""))
201+
self._set_attribute(MessageAttributes.TOOL_CALL_DESCRIPTION.format(i=i), tool.get("description", ""))
202+
except (json.JSONDecodeError, TypeError):
203+
logger.warning(f"Failed to parse tools for task: {task.get('id', 'unknown')}")
204+
205+
def _process_llm(self):
206+
"""Process an LLM instance."""
207+
llm = {}
208+
self._set_attribute(SpanAttributes.AGENTOPS_SPAN_KIND, "llm")
209+
210+
for key, value in self.instance.__dict__.items():
211+
if value is None:
212+
continue
213+
llm[key] = str(value)
214+
215+
model_name = llm.get('model_name', '') or llm.get('model', '')
216+
self._set_attribute(SpanAttributes.LLM_REQUEST_MODEL, model_name)
217+
self._set_attribute(SpanAttributes.LLM_REQUEST_TEMPERATURE, llm.get('temperature', ''))
218+
self._set_attribute(SpanAttributes.LLM_REQUEST_MAX_TOKENS, llm.get('max_tokens', ''))
219+
self._set_attribute(SpanAttributes.LLM_REQUEST_TOP_P, llm.get('top_p', ''))
220+
221+
if 'frequency_penalty' in llm:
222+
self._set_attribute(SpanAttributes.LLM_REQUEST_FREQUENCY_PENALTY, llm.get('frequency_penalty', ''))
223+
if 'presence_penalty' in llm:
224+
self._set_attribute(SpanAttributes.LLM_REQUEST_PRESENCE_PENALTY, llm.get('presence_penalty', ''))
225+
if 'streaming' in llm:
226+
self._set_attribute(SpanAttributes.LLM_REQUEST_STREAMING, llm.get('streaming', ''))
227+
228+
if 'api_key' in llm:
229+
self._set_attribute("gen_ai.request.api_key_present", "true")
230+
231+
if 'base_url' in llm:
232+
self._set_attribute(SpanAttributes.LLM_OPENAI_API_BASE, llm.get('base_url', ''))
233+
234+
if 'api_version' in llm:
235+
self._set_attribute(SpanAttributes.LLM_OPENAI_API_VERSION, llm.get('api_version', ''))
236+
237+
def _parse_agents(self, agents):
238+
"""Parse agents into a list of dictionaries."""
239+
if not agents:
240+
logger.debug("CrewAI: No agents to parse")
241+
return
242+
243+
agent_count = len(agents)
244+
logger.debug(f"CrewAI: Parsing {agent_count} agents")
245+
246+
# Pre-process all agents to collect their data first
247+
agent_data_list = []
248+
249+
for idx, agent in enumerate(agents):
250+
if agent is None:
251+
logger.debug(f"CrewAI: Agent at index {idx} is None, skipping")
252+
agent_data_list.append(None)
253+
continue
254+
255+
logger.debug(f"CrewAI: Processing agent at index {idx}")
256+
try:
257+
agent_data = self._extract_agent_data(agent)
258+
agent_data_list.append(agent_data)
259+
except Exception as e:
260+
logger.error(f"CrewAI: Error extracting data for agent at index {idx}: {str(e)}")
261+
agent_data_list.append(None)
262+
263+
# Now set all attributes at once for each agent
264+
for idx, agent_data in enumerate(agent_data_list):
265+
if agent_data is None:
266+
continue
267+
268+
for key, value in agent_data.items():
269+
if key == "tools" and isinstance(value, list):
270+
for tool_idx, tool in enumerate(value):
271+
for tool_key, tool_value in tool.items():
272+
self._set_attribute(f"crewai.agents.{idx}.tools.{tool_idx}.{tool_key}", str(tool_value))
273+
else:
274+
self._set_attribute(f"crewai.agents.{idx}.{key}", value)
275+
276+
def _parse_llms(self, llms):
277+
"""Parse LLMs into a list of dictionaries."""
278+
for idx, llm in enumerate(llms):
279+
if llm is not None:
280+
model_name = getattr(llm, "model", None) or getattr(llm, "model_name", None) or ""
281+
llm_data = {
282+
"model": model_name,
283+
"temperature": llm.temperature,
284+
"max_tokens": llm.max_tokens,
285+
"max_completion_tokens": llm.max_completion_tokens,
286+
"top_p": llm.top_p,
287+
"n": llm.n,
288+
"seed": llm.seed,
289+
"base_url": llm.base_url,
290+
"api_version": llm.api_version,
291+
}
292+
293+
self._set_attribute(f"{SpanAttributes.LLM_REQUEST_MODEL}.{idx}", model_name)
294+
if hasattr(llm, "temperature"):
295+
self._set_attribute(f"{SpanAttributes.LLM_REQUEST_TEMPERATURE}.{idx}", str(llm.temperature))
296+
if hasattr(llm, "max_tokens"):
297+
self._set_attribute(f"{SpanAttributes.LLM_REQUEST_MAX_TOKENS}.{idx}", str(llm.max_tokens))
298+
if hasattr(llm, "top_p"):
299+
self._set_attribute(f"{SpanAttributes.LLM_REQUEST_TOP_P}.{idx}", str(llm.top_p))
300+
301+
for key, value in llm_data.items():
302+
if value is not None:
303+
self._set_attribute(f"crewai.llms.{idx}.{key}", str(value))
304+
305+
def _extract_agent_data(self, agent):
306+
"""Extract data from an agent."""
307+
model = getattr(agent.llm, "model", None) or getattr(agent.llm, "model_name", None) or ""
308+
309+
tools_list = []
310+
if hasattr(agent, "tools") and agent.tools:
311+
tools_list = _parse_tools(agent.tools)
312+
313+
return {
314+
"id": str(agent.id),
315+
"role": agent.role,
316+
"goal": agent.goal,
317+
"backstory": agent.backstory,
318+
"cache": agent.cache,
319+
"config": agent.config,
320+
"verbose": agent.verbose,
321+
"allow_delegation": agent.allow_delegation,
322+
"tools": tools_list,
323+
"max_iter": agent.max_iter,
324+
"llm": str(model),
325+
}
326+
327+
def _set_attribute(self, key, value):
328+
"""Set an attribute on the span."""
329+
if value is not None and value != "":
330+
set_span_attribute(self.span, key, value)

0 commit comments

Comments
 (0)