33from typing import Dict
44from typing import List
55from typing import Optional
6+ from weakref import WeakKeyDictionary
67
78from ddtrace .internal import core
89from ddtrace .internal .logger import get_logger
2627log = get_logger (__name__ )
2728
2829
30+ OP_NAMES_TO_SPAN_KIND = {
31+ "crew" : "workflow" ,
32+ "task" : "task" ,
33+ "agent" : "agent" ,
34+ "tool" : "tool" ,
35+ "flow" : "workflow" ,
36+ "flow_method" : "task" ,
37+ }
38+
39+
2940class CrewAIIntegration (BaseLLMIntegration ):
3041 _integration_name = "crewai"
3142 # the CrewAI integration's task span linking relies on keeping track of an internal Datadog crew ID,
3243 # which follows the format "crew_{trace_id}_{root_span_id}".
3344 _crews_to_task_span_ids : Dict [str , List [str ]] = {} # maps crew ID to list of task span_ids
3445 _crews_to_tasks : Dict [str , Dict [str , Any ]] = {} # maps crew ID to dictionary of task_id to span_id and span_links
3546 _planning_crew_ids : List [str ] = [] # list of crew IDs that correspond to planning crew instances
47+ _flow_span_to_method_to_span_dict : WeakKeyDictionary [Span , Dict [str , Dict [str , Any ]]] = WeakKeyDictionary ()
3648
3749 def trace (self , pin : Pin , operation_id : str , submit_to_llmobs : bool = False , ** kwargs : Dict [str , Any ]) -> Span :
3850 if kwargs .get ("_ddtrace_ctx" ):
@@ -56,6 +68,15 @@ def trace(self, pin: Pin, operation_id: str, submit_to_llmobs: bool = False, **k
5668 self ._crews_to_task_span_ids .get (crew_id , []).append (str (span .span_id ))
5769 task_node = self ._crews_to_tasks .get (crew_id , {}).setdefault (str (task_id ), {})
5870 task_node ["span_id" ] = str (span .span_id )
71+ if kwargs .get ("operation" ) == "flow" :
72+ self ._flow_span_to_method_to_span_dict [span ] = {}
73+ if kwargs .get ("operation" ) == "flow_method" :
74+ span_name = kwargs .get ("span_name" , "" )
75+ method_name : str = span_name if isinstance (span_name , str ) else ""
76+ if span ._parent is None :
77+ return span
78+ span_dict = self ._flow_span_to_method_to_span_dict .get (span ._parent , {}).setdefault (method_name , {})
79+ span_dict ["span_id" ] = str (span .span_id )
5980 return span
6081
6182 def _get_current_ctx (self , pin ):
@@ -74,7 +95,7 @@ def _llmobs_set_tags(
7495 response : Optional [Any ] = None ,
7596 operation : str = "" ,
7697 ) -> None :
77- span ._set_ctx_item (SPAN_KIND , "workflow" if operation == "crew" else operation )
98+ span ._set_ctx_item (SPAN_KIND , OP_NAMES_TO_SPAN_KIND . get ( operation , "task" ) )
7899 if operation == "crew" :
79100 crew_id = _get_crew_id (span , "crew" )
80101 self ._llmobs_set_tags_crew (span , args , kwargs , response )
@@ -88,9 +109,13 @@ def _llmobs_set_tags(
88109 self ._llmobs_set_tags_agent (span , args , kwargs , response )
89110 elif operation == "tool" :
90111 self ._llmobs_set_tags_tool (span , args , kwargs , response )
112+ elif operation == "flow" :
113+ self ._llmobs_set_tags_flow (span , args , kwargs , response )
114+ elif operation == "flow_method" :
115+ self ._llmobs_set_tags_flow_method (span , args , kwargs , response )
91116
92117 def _llmobs_set_tags_crew (self , span , args , kwargs , response ):
93- crew_instance = kwargs .get ( " instance" )
118+ crew_instance = kwargs .pop ( "_dd. instance", None )
94119 crew_id = _get_crew_id (span , "crew" )
95120 task_span_ids = self ._crews_to_task_span_ids .get (crew_id , [])
96121 if task_span_ids :
@@ -117,7 +142,7 @@ def _llmobs_set_tags_crew(self, span, args, kwargs, response):
117142
118143 def _llmobs_set_tags_task (self , span , args , kwargs , response ):
119144 crew_id = _get_crew_id (span , "task" )
120- task_instance = kwargs .get ( " instance" )
145+ task_instance = kwargs .pop ( "_dd. instance", None )
121146 task_id = getattr (task_instance , "id" , None )
122147 task_name = getattr (task_instance , "name" , "" )
123148 task_description = getattr (task_instance , "description" , "" )
@@ -151,7 +176,7 @@ def _llmobs_set_tags_agent(self, span, args, kwargs, response):
151176 """Set span links and metadata for agent spans.
152177 Agent spans are 1:1 with its parent (task/tool) span, so we link them directly here, even on the parent itself.
153178 """
154- agent_instance = kwargs .get ("instance" )
179+ agent_instance = kwargs .get ("_dd. instance" , None )
155180 self ._tag_agent_manifest (span , agent_instance )
156181 agent_role = getattr (agent_instance , "role" , "" )
157182 task_description = getattr (kwargs .get ("task" ), "description" , "" )
@@ -183,7 +208,7 @@ def _llmobs_set_tags_agent(self, span, args, kwargs, response):
183208 span ._set_ctx_item (OUTPUT_VALUE , response )
184209
185210 def _llmobs_set_tags_tool (self , span , args , kwargs , response ):
186- tool_instance = kwargs .get ( " instance" )
211+ tool_instance = kwargs .pop ( "_dd. instance", None )
187212 tool_name = getattr (tool_instance , "name" , "" )
188213 description = _extract_tool_description_field (getattr (tool_instance , "description" , "" ))
189214 span ._set_ctx_items (
@@ -247,6 +272,69 @@ def _get_agent_tools(self, tools):
247272 formatted_tools .append (tool_dict )
248273 return formatted_tools
249274
275+ def _llmobs_set_tags_flow (self , span , args , kwargs , response ):
276+ span ._set_ctx_items ({NAME : span .name or "CrewAI Flow" , OUTPUT_VALUE : str (response )})
277+ return
278+
279+ def _llmobs_set_tags_flow_method (self , span , args , kwargs , response ):
280+ flow_instance = kwargs .pop ("_dd.instance" , None )
281+ input_dict = {"args" : [str (arg ) for arg in args [2 :]], "kwargs" : {k : str (v ) for k , v in kwargs .items ()}}
282+ span_links = (
283+ self ._flow_span_to_method_to_span_dict .get (span ._parent , {}).get (span .name , {}).get ("span_links" , [])
284+ )
285+ if span .name in getattr (flow_instance , "_start_methods" , []):
286+ span_links .append (
287+ {
288+ "span_id" : str (span ._parent .span_id ),
289+ "trace_id" : format_trace_id (span .trace_id ),
290+ "attributes" : {"from" : "input" , "to" : "input" },
291+ }
292+ )
293+ span ._set_ctx_items (
294+ {
295+ NAME : span .name or "Flow Method" ,
296+ INPUT_VALUE : input_dict ,
297+ OUTPUT_VALUE : str (response ),
298+ SPAN_LINKS : span_links ,
299+ }
300+ )
301+ return
302+
303+ def _llmobs_set_span_link_on_flow (self , flow_span , args , kwargs , flow_instance ):
304+ trigger_method = get_argument_value (args , kwargs , 0 , "trigger_method" , optional = True )
305+ if not self .llmobs_enabled or not trigger_method :
306+ return
307+ trigger_span_dict = self ._flow_span_to_method_to_span_dict .get (flow_span , {}).get (trigger_method )
308+ if not trigger_span_dict :
309+ return
310+ listeners = getattr (flow_instance , "_listeners" , [])
311+ # Check trigger method against each listener methods' triggers
312+ for listener_name , (_ , listener_triggers ) in listeners .items ():
313+ if trigger_method not in listener_triggers :
314+ continue
315+ span_dict = self ._flow_span_to_method_to_span_dict .get (flow_span , {}).setdefault (listener_name , {})
316+ span_dict ["trace_id" ] = format_trace_id (flow_span .trace_id )
317+ span_links = span_dict .setdefault ("span_links" , [])
318+ span_links .append (
319+ {
320+ "span_id" : str (trigger_span_dict ["span_id" ]),
321+ "trace_id" : format_trace_id (flow_span .trace_id ),
322+ "attributes" : {"from" : "output" , "to" : "input" },
323+ }
324+ )
325+ # If no listeners are triggered/AND_triggered, then link trigger span to its parent.
326+ if not any (trigger_method in listener_triggers for _ , (_ , listener_triggers ) in listeners .items ()):
327+ flow_span_span_links = flow_span ._get_ctx_item (SPAN_LINKS ) or []
328+ flow_span_span_links .append (
329+ {
330+ "span_id" : str (trigger_span_dict ["span_id" ]),
331+ "trace_id" : format_trace_id (flow_span .trace_id ),
332+ "attributes" : {"from" : "output" , "to" : "output" },
333+ }
334+ )
335+ flow_span ._set_ctx_item (SPAN_LINKS , flow_span_span_links )
336+ return
337+
250338 def _llmobs_set_span_link_on_task (self , span , args , kwargs ):
251339 """Set span links for the next queued task in a CrewAI workflow.
252340 This happens between task executions, (the current span is the crew span and the task span hasn't started yet)
0 commit comments