1010from contextvars import ContextVar
1111from dataclasses import dataclass
1212from datetime import datetime
13- from typing import Any , Dict , Iterator , List , Literal , Optional
13+ from typing import Any , Dict , Iterator , List , Literal , Optional , cast
1414
1515from haystack import default_from_dict , default_to_dict , logging
1616from haystack .dataclasses import ChatMessage
2525logger = logging .getLogger (__name__ )
2626
2727HAYSTACK_LANGFUSE_ENFORCE_FLUSH_ENV_VAR = "HAYSTACK_LANGFUSE_ENFORCE_FLUSH"
28- _SUPPORTED_GENERATORS = [
29- "AzureOpenAIGenerator" ,
30- "OpenAIGenerator" ,
31- "AnthropicGenerator" ,
32- "HuggingFaceAPIGenerator" ,
33- "HuggingFaceLocalGenerator" ,
34- "CohereGenerator" ,
35- "OllamaGenerator" ,
36- ]
37- _SUPPORTED_CHAT_GENERATORS = [
38- "AmazonBedrockChatGenerator" ,
39- "AzureOpenAIChatGenerator" ,
40- "OpenAIChatGenerator" ,
41- "AnthropicChatGenerator" ,
42- "HuggingFaceAPIChatGenerator" ,
43- "HuggingFaceLocalChatGenerator" ,
44- "CohereChatGenerator" ,
45- "OllamaChatGenerator" ,
46- "GoogleGenAIChatGenerator" ,
47- ]
48- _ALL_SUPPORTED_GENERATORS = _SUPPORTED_GENERATORS + _SUPPORTED_CHAT_GENERATORS
4928
5029# These are the keys used by Haystack for traces and span.
5130# We keep them here to avoid making typos when using them.
5837_COMPONENT_OUTPUT_KEY = "haystack.component.output"
5938_COMPONENT_INPUT_KEY = "haystack.component.input"
6039
40+ # Type alias for observation span types
41+ ObservationSpanType = Literal ["tool" , "agent" , "retriever" , "embedding" , "generation" ]
42+
6143# External session metadata for trace correlation (Haystack system)
6244# Stores trace_id, user_id, session_id, tags, version for root trace creation
6345tracing_context_var : ContextVar [Dict [Any , Any ]] = ContextVar ("tracing_context" )
@@ -340,12 +322,26 @@ def create_span(self, context: SpanContext) -> LangfuseSpan:
340322 span ._span .update_trace (** trace_attrs )
341323
342324 return span
343- elif context .component_type == "ToolInvoker" :
344- return LangfuseSpan (self .tracer .start_as_current_observation (name = context .name , as_type = "tool" ))
325+
326+ span_type = None
327+
328+ if context .component_type == "ToolInvoker" :
329+ span_type = "tool"
345330 elif context .operation_name == "haystack.agent.run" :
346- return LangfuseSpan (self .tracer .start_as_current_observation (name = context .name , as_type = "agent" ))
347- elif context .component_type in _ALL_SUPPORTED_GENERATORS :
348- return LangfuseSpan (self .tracer .start_as_current_observation (name = context .name , as_type = "generation" ))
331+ span_type = "agent"
332+ elif context .component_type and context .component_type .endswith ("Retriever" ):
333+ span_type = "retriever"
334+ elif context .component_type and context .component_type .endswith ("Embedder" ):
335+ span_type = "embedding"
336+ elif context .component_type and context .component_type .endswith ("Generator" ):
337+ span_type = "generation"
338+
339+ if span_type :
340+ return LangfuseSpan (
341+ self .tracer .start_as_current_observation (
342+ name = context .name , as_type = cast (ObservationSpanType , span_type )
343+ )
344+ )
349345 else :
350346 return LangfuseSpan (self .tracer .start_as_current_span (name = context .name ))
351347
@@ -371,14 +367,7 @@ def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
371367 formatted_names = [f"{ name } (x{ count } )" if count > 1 else name for name , count in tool_counts .items ()]
372368 span .raw_span ().update (name = f"{ tool_invoker_name } - { sorted (formatted_names )} " )
373369
374- if component_type in _SUPPORTED_GENERATORS :
375- meta = span .get_data ().get (_COMPONENT_OUTPUT_KEY , {}).get ("meta" )
376- if meta :
377- usage = meta [0 ].get ("usage" )
378- sanitized_usage = _sanitize_usage_data (usage ) if usage else None
379- span .raw_span ().update (usage_details = sanitized_usage , model = meta [0 ].get ("model" ))
380-
381- if component_type in _SUPPORTED_CHAT_GENERATORS :
370+ if component_type and component_type .endswith ("ChatGenerator" ):
382371 replies = span .get_data ().get (_COMPONENT_OUTPUT_KEY , {}).get ("replies" )
383372 if replies :
384373 meta = replies [0 ].meta
@@ -396,6 +385,12 @@ def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
396385 model = meta .get ("model" ),
397386 completion_start_time = completion_start_time ,
398387 )
388+ elif component_type and component_type .endswith ("Generator" ):
389+ meta = span .get_data ().get (_COMPONENT_OUTPUT_KEY , {}).get ("meta" )
390+ if meta :
391+ usage = meta [0 ].get ("usage" )
392+ sanitized_usage = _sanitize_usage_data (usage ) if usage else None
393+ span .raw_span ().update (usage_details = sanitized_usage , model = meta [0 ].get ("model" ))
399394
400395
401396class LangfuseTracer (Tracer ):
0 commit comments