77
88from agentops .semconv .span_attributes import SpanAttributes
99from 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
1314logger = 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
2829def 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
3437class 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