55from abc import ABC , abstractmethod
66from typing import TYPE_CHECKING , Any , Callable , Dict , Mapping , Optional , Tuple
77
8- from opentelemetry import context
9-
10- if TYPE_CHECKING :
11- from crewai .agent import Agent
12- from crewai .crew import Crew
13- from crewai .llm import LLM
14- from crewai .task import Task
15- from crewai .tools .structured_tool import CrewStructuredTool
16- from crewai .tools .tool_calling import ToolCalling
17- from crewai .tools .tool_usage import ToolUsage
18- from pydantic import BaseModel
19-
208from amazon .opentelemetry .distro .semconv ._incubating .attributes .gen_ai_attributes import (
219 GEN_AI_AGENT_DESCRIPTION ,
2210 GEN_AI_AGENT_ID ,
2816 GEN_AI_REQUEST_TEMPERATURE ,
2917 GEN_AI_SYSTEM_INSTRUCTIONS ,
3018 GEN_AI_TOOL_CALL_ARGUMENTS ,
31- GEN_AI_TOOL_CALL_ID ,
19+ GEN_AI_TOOL_CALL_RESULT ,
3220 GEN_AI_TOOL_DEFINITIONS ,
3321 GEN_AI_TOOL_DESCRIPTION ,
3422 GEN_AI_TOOL_NAME ,
23+ GEN_AI_TOOL_TYPE ,
3524)
36- from opentelemetry import trace
25+ from opentelemetry import context , trace
26+ from opentelemetry .semconv ._incubating .attributes .error_attributes import ERROR_TYPE
3727from opentelemetry .trace import SpanKind , Status , StatusCode
3828
29+ if TYPE_CHECKING :
30+ from crewai .agent import Agent
31+ from crewai .crew import Crew
32+ from crewai .llm import LLM
33+ from crewai .task import Task
34+ from crewai .tools .structured_tool import CrewStructuredTool
35+ from crewai .tools .tool_calling import ToolCalling
36+ from crewai .tools .tool_usage import ToolUsage
37+ from pydantic import BaseModel
38+
3939_OPERATION_INVOKE_AGENT = "invoke_agent"
4040_OPERATION_EXECUTE_TOOL = "execute_tool"
41- # default value for gen_ai.provider.name, a required attribute per OpenTelemetry semantic conventions.
41+ # default value for gen_ai.provider.name, a required attribute per OpenTelemetry
42+ # semantic conventions.
4243# "crewai" is not a standard provider name in semconv v1.39, but serves as a fallback when the
4344# underlying LLM provider cannot be determined.
4445_PROVIDER_CREWAI = "crewai"
45- _ERROR_TYPE = "error.type"
4646
4747
4848class _BaseWrapper (ABC ):
@@ -70,6 +70,32 @@ class _BaseWrapper(ABC):
7070 def __init__ (self , tracer : Optional [trace .Tracer ] = None ) -> None :
7171 self ._tracer = tracer or trace .get_tracer (__name__ )
7272
73+ def __call__ (
74+ self ,
75+ wrapped : Callable [..., Any ],
76+ instance : Any ,
77+ args : Tuple [Any , ...],
78+ kwargs : Mapping [str , Any ],
79+ ) -> Any :
80+ if context .get_value (context ._SUPPRESS_INSTRUMENTATION_KEY ):
81+ return wrapped (* args , ** kwargs )
82+
83+ with self ._tracer .start_as_current_span (
84+ self ._get_span_name (instance , args , kwargs ),
85+ kind = SpanKind .INTERNAL ,
86+ attributes = self ._get_attributes (instance , args , kwargs ),
87+ ) as span :
88+ try :
89+ result = wrapped (* args , ** kwargs )
90+ self ._on_success (span , result )
91+ span .set_status (Status (StatusCode .OK ))
92+ return result
93+ except Exception as exc : # pylint: disable=broad-exception-caught
94+ span .set_status (Status (StatusCode .ERROR , str (exc )))
95+ span .set_attribute (ERROR_TYPE , type (exc ).__name__ )
96+ span .record_exception (exc )
97+ raise
98+
7399 def _extract_provider_and_model (self , llm : Optional ["LLM" ]) -> Tuple [Optional [str ], Optional [str ]]:
74100 # extracts provider name and model from CrewAI LLM object
75101 if not llm :
@@ -93,7 +119,8 @@ def _extract_provider_and_model(self, llm: Optional["LLM"]) -> Tuple[Optional[st
93119
94120 return None , model
95121
96- def _serialize_to_json (self , value : Any , max_depth : int = 10 ) -> str :
122+ @staticmethod
123+ def _serialize_to_json (value : Any , max_depth : int = 10 ) -> str :
97124 def _truncate (obj : Any , depth : int ) -> Any :
98125 if depth <= 0 :
99126 return "..."
@@ -116,49 +143,24 @@ def _get_span_name(self, instance: Any, args: Tuple[Any, ...], kwargs: Mapping[s
116143 def _get_attributes (self , instance : Any , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]) -> Dict [str , Any ]:
117144 pass
118145
119- def __call__ (
120- self ,
121- wrapped : Callable [..., Any ],
122- instance : Any ,
123- args : Tuple [Any , ...],
124- kwargs : Mapping [str , Any ],
125- ) -> Any :
126- if context .get_value (context ._SUPPRESS_INSTRUMENTATION_KEY ):
127- return wrapped (* args , ** kwargs )
128-
129- with self ._tracer .start_as_current_span (
130- self ._get_span_name (instance , args , kwargs ),
131- kind = SpanKind .INTERNAL ,
132- attributes = self ._get_attributes (instance , args , kwargs ),
133- ) as span :
134- try :
135- result = wrapped (* args , ** kwargs )
136- span .set_status (Status (StatusCode .OK ))
137- return result
138- except Exception as e :
139- span .set_status (Status (StatusCode .ERROR , str (e )))
140- span .set_attribute (_ERROR_TYPE , type (e ).__name__ )
141- span .record_exception (e )
142- raise
146+ def _on_success (self , span : trace .Span , result : Any ) -> None :
147+ """Hook called on successful execution."""
143148
144149
145150class _CrewKickoffWrapper (_BaseWrapper ):
146- # wraps Crew.kickoff which is responsible for starting the entire agentic workflow.
147- # see: https://github.com/crewAIInc/crewAI/blob/main/src/crewai/crew.py
151+ # wraps Crew.kickoff which is responsible for starting the agentic workflow.
152+ # see:
153+ # https://github.com/crewAIInc/crewAI/blob/06d953bf46c636ff9f2d64f45574493d05fb7771/lib/crewai/src/crewai/crew.py#L676-L679
148154 # Note: The span name "crew_kickoff {crew_name}" does not conform to any current OTel semantic
149155 # conventions. This is because CrewAI's orchestration workflow where a Crew can contain multiple
150156 # agents but there currently does not exist any semantic convention naming schema to capture
151157 # this architecture.
152158
153- def _get_span_name (
154- self , instance : "Crew" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]
155- ) -> str :
159+ def _get_span_name (self , instance : "Crew" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]) -> str :
156160 crew_name = getattr (instance , "name" , None )
157161 return f"crew_kickoff { crew_name } " if crew_name else "crew_kickoff"
158162
159- def _get_attributes (
160- self , instance : "Crew" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]
161- ) -> Dict [str , Any ]:
163+ def _get_attributes (self , instance : "Crew" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]) -> Dict [str , Any ]:
162164 attributes : Dict [str , Any ] = {
163165 GEN_AI_OPERATION_NAME : _OPERATION_INVOKE_AGENT ,
164166 GEN_AI_PROVIDER_NAME : _PROVIDER_CREWAI ,
@@ -190,7 +192,8 @@ def _get_attributes(
190192
191193 return attributes
192194
193- def _extract_tool_definitions (self , tools : Any ) -> list :
195+ @staticmethod
196+ def _extract_tool_definitions (tools : Any ) -> list :
194197 defs = []
195198 for tool in tools :
196199 tool_def : Dict [str , Any ] = {"type" : "function" }
@@ -202,26 +205,24 @@ def _extract_tool_definitions(self, tools: Any) -> list:
202205 if args_schema is not None :
203206 try :
204207 tool_def ["parameters" ] = args_schema .model_json_schema ()
205- except Exception :
208+ except Exception : # pylint: disable=broad-exception-caught
206209 pass
207210 defs .append (tool_def )
208211 return defs
209212
210213
211214class _TaskExecuteCoreWrapper (_BaseWrapper ):
212- # wraps Task._execute_core which is responsible for running a single task with its assigned agent.
213- # see: https://github.com/crewAIInc/crewAI/blob/main/src/crewai/task.py
215+ # wraps Task._execute_core which is responsible for running a single task
216+ # with its assigned agent.
217+ # see:
218+ # https://github.com/crewAIInc/crewAI/blob/06d953bf46c636ff9f2d64f45574493d05fb7771/lib/crewai/src/crewai/task.py#L604-L608
214219
215- def _get_span_name (
216- self , instance : "Task" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]
217- ) -> str :
220+ def _get_span_name (self , instance : "Task" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]) -> str :
218221 agent : Optional [Agent ] = args [0 ] if args else kwargs .get ("agent" )
219222 agent_role = getattr (agent , "role" , None ) if agent else None
220223 return f"{ _OPERATION_INVOKE_AGENT } { agent_role } " if agent_role else _OPERATION_INVOKE_AGENT
221224
222- def _get_attributes (
223- self , instance : "Task" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]
224- ) -> Dict [str , Any ]:
225+ def _get_attributes (self , instance : "Task" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]) -> Dict [str , Any ]:
225226 agent : Optional [Agent ] = args [0 ] if args else kwargs .get ("agent" )
226227 attributes : Dict [str , Any ] = {
227228 GEN_AI_OPERATION_NAME : _OPERATION_INVOKE_AGENT ,
@@ -263,11 +264,10 @@ def _get_attributes(
263264
264265class _ToolUseWrapper (_BaseWrapper ):
265266 # Wraps ToolUsage._use which executes a tool call during agent task execution.
266- # see: https://github.com/crewAIInc/crewAI/blob/main/src/crewai/tools/tool_usage.py
267+ # see:
268+ # https://github.com/crewAIInc/crewAI/blob/06d953bf46c636ff9f2d64f45574493d05fb7771/lib/crewai/src/crewai/tools/tool_usage.py#L423-L427
267269
268- def _get_span_name (
269- self , instance : "ToolUsage" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]
270- ) -> str :
270+ def _get_span_name (self , instance : "ToolUsage" , args : Tuple [Any , ...], kwargs : Mapping [str , Any ]) -> str :
271271 tool : Optional [CrewStructuredTool ] = args [1 ] if len (args ) > 1 else kwargs .get ("tool" )
272272 tool_name = getattr (tool , "name" , None ) if tool else None
273273 return f"{ _OPERATION_EXECUTE_TOOL } { tool_name } " if tool_name else _OPERATION_EXECUTE_TOOL
@@ -280,6 +280,7 @@ def _get_attributes(
280280 attributes : Dict [str , Any ] = {
281281 GEN_AI_OPERATION_NAME : _OPERATION_EXECUTE_TOOL ,
282282 GEN_AI_PROVIDER_NAME : _PROVIDER_CREWAI ,
283+ GEN_AI_TOOL_TYPE : "function" ,
283284 }
284285
285286 if tool :
@@ -291,9 +292,6 @@ def _get_attributes(
291292 attributes [GEN_AI_TOOL_DESCRIPTION ] = tool_desc
292293
293294 if calling :
294- call_id = getattr (calling , "id" , None )
295- if call_id :
296- attributes [GEN_AI_TOOL_CALL_ID ] = str (call_id )
297295 call_args = getattr (calling , "arguments" , None )
298296 if call_args :
299297 attributes [GEN_AI_TOOL_CALL_ARGUMENTS ] = self ._serialize_to_json (call_args )
@@ -308,3 +306,7 @@ def _get_attributes(
308306 attributes [GEN_AI_REQUEST_MODEL ] = model
309307
310308 return attributes
309+
310+ def _on_success (self , span : trace .Span , result : Any ) -> None :
311+ if result is not None :
312+ span .set_attribute (GEN_AI_TOOL_CALL_RESULT , self ._serialize_to_json (result ))
0 commit comments