Skip to content

Commit a032bbb

Browse files
committed
Refactored CrewAISpanAttributes to improve handling of agent, crew, and task attributes.
1 parent 44839f0 commit a032bbb

3 files changed

Lines changed: 496 additions & 68 deletions

File tree

agentops/semconv/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class AgentAttributes:
88
AGENT_ID = "agent.id" # Unique identifier for the agent
99
AGENT_NAME = "agent.name" # Name of the agent
1010
AGENT_ROLE = "agent.role" # Role of the agent
11+
AGENT = "agent" # Root prefix for agent attributes
1112

1213
# Capabilities
1314
AGENT_TOOLS = "agent.tools" # Tools available to the agent

third_party/opentelemetry/instrumentation/crewai/crewai_span_attributes.py

Lines changed: 177 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from agentops.semconv.span_attributes import SpanAttributes
99
from agentops.semconv.agent import AgentAttributes
10-
from agentops.semconv.workflow import WorkflowAttributes
10+
from agentops.semconv.tool import ToolAttributes
11+
from agentops.semconv.message import MessageAttributes
1112

1213
# Initialize logger for logging potential issues and operations
1314
logger = logging.getLogger(__name__)
@@ -23,25 +24,31 @@ def _parse_tools(tools):
2324
res["description"] = tool.description
2425
if res:
2526
result.append(res)
26-
return json.dumps(result)
27+
return result
2728

2829
def set_span_attribute(span: Span, key: str, value: Any) -> None:
2930
"""Set a single attribute on a span."""
3031
if value is not None and value != "":
32+
if hasattr(value, "__str__"):
33+
value = str(value)
3134
span.set_attribute(key, value)
3235

3336

3437
class CrewAISpanAttributes:
3538
"""Manages span attributes for CrewAI instrumentation."""
3639

37-
def __init__(self, span: Span, instance) -> None:
40+
def __init__(self, span: Span, instance, skip_agent_processing=False) -> None:
3841
self.span = span
3942
self.instance = instance
43+
self.skip_agent_processing = skip_agent_processing
4044
self.process_instance()
4145

4246
def process_instance(self):
4347
"""Process the instance based on its type."""
4448
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+
4552
method_mapping = {
4653
"Crew": self._process_crew,
4754
"Agent": self._process_agent,
@@ -54,112 +61,225 @@ def process_instance(self):
5461

5562
def _process_crew(self):
5663
"""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+
5771
for key, value in self.instance.__dict__.items():
5872
if value is None:
5973
continue
74+
6075
if key == "tasks":
61-
self._parse_tasks(value)
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")
6279
elif key == "agents":
63-
self._parse_agents(value)
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)
6485
elif key == "llms":
6586
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))
6691
else:
6792
self._set_attribute(f"crewai.crew.{key}", str(value))
6893

6994
def _process_agent(self):
7095
"""Process an Agent instance."""
7196
agent = {}
97+
self._set_attribute(SpanAttributes.AGENTOPS_SPAN_KIND, "agent")
98+
7299
for key, value in self.instance.__dict__.items():
73100
if key == "tools":
74-
value = _parse_tools(value)
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+
75109
if value is None:
76110
continue
77-
agent[key] = str(value)
111+
112+
if key != "tools":
113+
agent[key] = str(value)
78114

79-
# Set agent attributes using our semantic conventions
80115
self._set_attribute(AgentAttributes.AGENT_ID, agent.get('id', ''))
81116
self._set_attribute(AgentAttributes.AGENT_ROLE, agent.get('role', ''))
82117
self._set_attribute(AgentAttributes.AGENT_NAME, agent.get('name', ''))
83118
self._set_attribute(AgentAttributes.AGENT_TOOLS, agent.get('tools', ''))
84119

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+
85126
self._set_attribute("crewai.agent.goal", agent.get('goal', ''))
86127
self._set_attribute("crewai.agent.backstory", agent.get('backstory', ''))
87128
self._set_attribute("crewai.agent.cache", agent.get('cache', ''))
88129
self._set_attribute("crewai.agent.allow_delegation", agent.get('allow_delegation', ''))
89130
self._set_attribute("crewai.agent.allow_code_execution", agent.get('allow_code_execution', ''))
90131
self._set_attribute("crewai.agent.max_retry_limit", agent.get('max_retry_limit', ''))
91-
self._set_attribute("crewai.agent.tools_results", agent.get('tools_results', ''))
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))
92149

93150
def _process_task(self):
94151
"""Process a Task instance."""
95152
task = {}
153+
self._set_attribute(SpanAttributes.AGENTOPS_SPAN_KIND, "workflow.step")
154+
96155
for key, value in self.instance.__dict__.items():
97156
if value is None:
98157
continue
99158
if key == "tools":
100-
value = _parse_tools(value)
101-
task[key] = value
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+
102167
elif key == "agent":
103168
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))
104172
else:
105173
task[key] = str(value)
106174

107-
# Set task attributes using our semantic conventions
108-
self._set_attribute(WorkflowAttributes.WORKFLOW_STEP_NAME, task.get('description', ''))
109-
self._set_attribute(WorkflowAttributes.WORKFLOW_STEP_TYPE, "task")
110-
self._set_attribute(WorkflowAttributes.WORKFLOW_STEP_INPUT, task.get('context', ''))
111-
self._set_attribute(WorkflowAttributes.WORKFLOW_STEP_OUTPUT, task.get('expected_output', ''))
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', ''))
112191

113-
self._set_attribute("crewai.task.id", task.get('id', ''))
114192
self._set_attribute("crewai.task.agent", task.get('agent', ''))
115193
self._set_attribute("crewai.task.human_input", task.get('human_input', ''))
116-
self._set_attribute("crewai.task.output", task.get('output', ''))
117194
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')}")
118204

119205
def _process_llm(self):
120206
"""Process an LLM instance."""
121207
llm = {}
208+
self._set_attribute(SpanAttributes.AGENTOPS_SPAN_KIND, "llm")
209+
122210
for key, value in self.instance.__dict__.items():
123211
if value is None:
124212
continue
125213
llm[key] = str(value)
126214

127-
# Set LLM attributes using our semantic conventions
128-
self._set_attribute(SpanAttributes.LLM_REQUEST_MODEL, llm.get('model_name', ''))
215+
model_name = llm.get('model_name', '') or llm.get('model', '')
216+
self._set_attribute(SpanAttributes.LLM_REQUEST_MODEL, model_name)
129217
self._set_attribute(SpanAttributes.LLM_REQUEST_TEMPERATURE, llm.get('temperature', ''))
130218
self._set_attribute(SpanAttributes.LLM_REQUEST_MAX_TOKENS, llm.get('max_tokens', ''))
131219
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', ''))
132236

133237
def _parse_agents(self, agents):
134238
"""Parse agents into a list of dictionaries."""
135-
for agent in agents:
136-
if agent is not None:
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:
137257
agent_data = self._extract_agent_data(agent)
138-
for key, value in agent_data.items():
139-
self._set_attribute(f"crewai.agent.{key}", value)
140-
141-
def _parse_tasks(self, tasks):
142-
"""Parse tasks into a list of dictionaries."""
143-
for task in tasks:
144-
if task is not None:
145-
task_data = {
146-
"agent": task.agent.role if task.agent else None,
147-
"description": task.description,
148-
"async_execution": task.async_execution,
149-
"expected_output": task.expected_output,
150-
"human_input": task.human_input,
151-
"tools": task.tools,
152-
"output_file": task.output_file,
153-
}
154-
for key, value in task_data.items():
155-
if value is not None:
156-
self._set_attribute(f"crewai.task.{key}", str(value))
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)
157275

158276
def _parse_llms(self, llms):
159277
"""Parse LLMs into a list of dictionaries."""
160-
for llm in llms:
278+
for idx, llm in enumerate(llms):
161279
if llm is not None:
280+
model_name = getattr(llm, "model", None) or getattr(llm, "model_name", None) or ""
162281
llm_data = {
282+
"model": model_name,
163283
"temperature": llm.temperature,
164284
"max_tokens": llm.max_tokens,
165285
"max_completion_tokens": llm.max_completion_tokens,
@@ -169,14 +289,27 @@ def _parse_llms(self, llms):
169289
"base_url": llm.base_url,
170290
"api_version": llm.api_version,
171291
}
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+
172301
for key, value in llm_data.items():
173302
if value is not None:
174-
self._set_attribute(f"crewai.llm.{key}", str(value))
303+
self._set_attribute(f"crewai.llms.{idx}.{key}", str(value))
175304

176305
def _extract_agent_data(self, agent):
177306
"""Extract data from an agent."""
178307
model = getattr(agent.llm, "model", None) or getattr(agent.llm, "model_name", None) or ""
179308

309+
tools_list = []
310+
if hasattr(agent, "tools") and agent.tools:
311+
tools_list = _parse_tools(agent.tools)
312+
180313
return {
181314
"id": str(agent.id),
182315
"role": agent.role,
@@ -186,7 +319,7 @@ def _extract_agent_data(self, agent):
186319
"config": agent.config,
187320
"verbose": agent.verbose,
188321
"allow_delegation": agent.allow_delegation,
189-
"tools": agent.tools,
322+
"tools": tools_list,
190323
"max_iter": agent.max_iter,
191324
"llm": str(model),
192325
}

0 commit comments

Comments
 (0)