1414
1515from __future__ import annotations
1616
17- from typing import Any , Optional
17+ from typing import Any , Optional , Sequence
1818from uuid import UUID
1919
2020from langchain_core .callbacks import BaseCallbackHandler
2424from opentelemetry .instrumentation .langchain .invocation_manager import (
2525 _InvocationManager ,
2626)
27+ from opentelemetry .semconv ._incubating .attributes import (
28+ gen_ai_attributes as GenAI ,
29+ )
2730from opentelemetry .util .genai .handler import TelemetryHandler
2831from opentelemetry .util .genai .types import (
32+ ContentCapturingMode ,
2933 Error ,
3034 InputMessage ,
3135 LLMInvocation ,
3236 OutputMessage ,
3337 Text ,
3438)
39+ from opentelemetry .util .genai .utils import (
40+ get_content_capturing_mode ,
41+ is_experimental_mode ,
42+ )
43+
44+ GEN_AI_MEMORY_STORE_ID = getattr (
45+ GenAI , "GEN_AI_MEMORY_STORE_ID" , "gen_ai.memory.store.id"
46+ )
47+ GEN_AI_MEMORY_STORE_NAME = getattr (
48+ GenAI , "GEN_AI_MEMORY_STORE_NAME" , "gen_ai.memory.store.name"
49+ )
50+ GEN_AI_MEMORY_QUERY = getattr (
51+ GenAI , "GEN_AI_MEMORY_QUERY" , "gen_ai.memory.query"
52+ )
53+ GEN_AI_MEMORY_SEARCH_RESULT_COUNT = getattr (
54+ GenAI ,
55+ "GEN_AI_MEMORY_SEARCH_RESULT_COUNT" ,
56+ "gen_ai.memory.search.result.count" ,
57+ )
58+ GEN_AI_MEMORY_NAMESPACE = getattr (
59+ GenAI , "GEN_AI_MEMORY_NAMESPACE" , "gen_ai.memory.namespace"
60+ )
61+
62+ _SEARCH_MEMORY_MEMBER = getattr (
63+ getattr (GenAI , "GenAiOperationNameValues" , object ()),
64+ "SEARCH_MEMORY" ,
65+ None ,
66+ )
67+ SEARCH_MEMORY_OPERATION = (
68+ _SEARCH_MEMORY_MEMBER .value
69+ if _SEARCH_MEMORY_MEMBER is not None
70+ else "search_memory"
71+ )
72+
73+ _RETRIEVAL_MEMBER = getattr (
74+ getattr (GenAI , "GenAiOperationNameValues" , object ()),
75+ "RETRIEVAL" ,
76+ None ,
77+ )
78+ RETRIEVAL_OPERATION = (
79+ _RETRIEVAL_MEMBER .value if _RETRIEVAL_MEMBER is not None else "retrieval"
80+ )
3581
3682
3783class OpenTelemetryLangChainCallbackHandler (BaseCallbackHandler ):
@@ -44,6 +90,62 @@ def __init__(self, telemetry_handler: TelemetryHandler) -> None:
4490 self ._telemetry_handler = telemetry_handler
4591 self ._invocation_manager = _InvocationManager ()
4692
93+ @staticmethod
94+ def _resolve_retriever_store_name (
95+ serialized : dict [str , Any ],
96+ metadata : Optional [dict [str , Any ]],
97+ ) -> Optional [str ]:
98+ if metadata and metadata .get ("memory_store_name" ):
99+ return str (metadata ["memory_store_name" ])
100+ if metadata and metadata .get ("ls_retriever_name" ):
101+ return str (metadata ["ls_retriever_name" ])
102+ name = serialized .get ("name" )
103+ return str (name ) if isinstance (name , str ) and name else None
104+
105+ @staticmethod
106+ def _resolve_retriever_store_id (
107+ serialized : dict [str , Any ],
108+ metadata : Optional [dict [str , Any ]],
109+ ) -> Optional [str ]:
110+ if metadata and metadata .get ("memory_store_id" ):
111+ return str (metadata ["memory_store_id" ])
112+
113+ serialized_id = serialized .get ("id" )
114+ if isinstance (serialized_id , str ) and serialized_id :
115+ return serialized_id
116+ if isinstance (serialized_id , list ) and serialized_id :
117+ try :
118+ return "." .join (str (part ) for part in serialized_id ) # type: ignore[reportUnknownArgumentType, reportUnknownVariableType]
119+ except TypeError :
120+ return None
121+ return None
122+
123+ @staticmethod
124+ def _should_capture_memory_query () -> bool :
125+ if not is_experimental_mode ():
126+ return False
127+ try :
128+ mode = get_content_capturing_mode ()
129+ except ValueError :
130+ return False
131+ return mode in (
132+ ContentCapturingMode .SPAN_ONLY ,
133+ ContentCapturingMode .SPAN_AND_EVENT ,
134+ )
135+
136+ @staticmethod
137+ def _is_memory_retriever (
138+ metadata : Optional [dict [str , Any ]],
139+ ) -> bool :
140+ """Detect if a retriever is a memory retriever based on metadata hints."""
141+ if not metadata :
142+ return False
143+ return bool (
144+ metadata .get ("memory_store_name" )
145+ or metadata .get ("memory_store_id" )
146+ or metadata .get ("is_memory_retriever" )
147+ )
148+
47149 def on_chat_model_start (
48150 self ,
49151 serialized : dict [str , Any ],
@@ -268,3 +370,96 @@ def on_llm_error(
268370 )
269371 if llm_invocation .span and not llm_invocation .span .is_recording ():
270372 self ._invocation_manager .delete_invocation_state (run_id = run_id )
373+
374+ def on_retriever_start (
375+ self ,
376+ serialized : dict [str , Any ],
377+ query : str ,
378+ * ,
379+ run_id : UUID ,
380+ parent_run_id : Optional [UUID ] = None ,
381+ tags : Optional [list [str ]] = None ,
382+ metadata : Optional [dict [str , Any ]] = None ,
383+ ** kwargs : Any ,
384+ ) -> None :
385+ provider = "unknown"
386+ if metadata is not None :
387+ provider = metadata .get ("ls_provider" , "unknown" )
388+
389+ attributes : dict [str , Any ] = {}
390+ is_memory = self ._is_memory_retriever (metadata )
391+ operation = (
392+ SEARCH_MEMORY_OPERATION if is_memory else RETRIEVAL_OPERATION
393+ )
394+
395+ if store_name := self ._resolve_retriever_store_name (
396+ serialized , metadata
397+ ):
398+ attributes [GEN_AI_MEMORY_STORE_NAME ] = store_name
399+ if store_id := self ._resolve_retriever_store_id (serialized , metadata ):
400+ attributes [GEN_AI_MEMORY_STORE_ID ] = store_id
401+ if query and self ._should_capture_memory_query ():
402+ attributes [GEN_AI_MEMORY_QUERY ] = query
403+ if metadata and metadata .get ("memory_namespace" ):
404+ attributes [GEN_AI_MEMORY_NAMESPACE ] = metadata ["memory_namespace" ]
405+
406+ llm_invocation = LLMInvocation (
407+ request_model = "" ,
408+ provider = provider ,
409+ operation_name = operation ,
410+ attributes = attributes ,
411+ )
412+ llm_invocation = self ._telemetry_handler .start_llm (
413+ invocation = llm_invocation
414+ )
415+ if llm_invocation .span and store_name :
416+ llm_invocation .span .update_name (f"{ operation } { store_name } " )
417+ self ._invocation_manager .add_invocation_state (
418+ run_id = run_id ,
419+ parent_run_id = parent_run_id ,
420+ invocation = llm_invocation ,
421+ )
422+
423+ def on_retriever_end (
424+ self ,
425+ documents : Sequence [Any ],
426+ * ,
427+ run_id : UUID ,
428+ parent_run_id : Optional [UUID ] = None ,
429+ ** kwargs : Any ,
430+ ) -> None :
431+ llm_invocation = self ._invocation_manager .get_invocation (run_id = run_id )
432+ if llm_invocation is None or not isinstance (
433+ llm_invocation , LLMInvocation
434+ ):
435+ return
436+
437+ llm_invocation .attributes [GEN_AI_MEMORY_SEARCH_RESULT_COUNT ] = len (
438+ documents
439+ )
440+ llm_invocation = self ._telemetry_handler .stop_llm (
441+ invocation = llm_invocation
442+ )
443+ if llm_invocation .span and not llm_invocation .span .is_recording ():
444+ self ._invocation_manager .delete_invocation_state (run_id = run_id )
445+
446+ def on_retriever_error (
447+ self ,
448+ error : BaseException ,
449+ * ,
450+ run_id : UUID ,
451+ parent_run_id : Optional [UUID ] = None ,
452+ ** kwargs : Any ,
453+ ) -> None :
454+ llm_invocation = self ._invocation_manager .get_invocation (run_id = run_id )
455+ if llm_invocation is None or not isinstance (
456+ llm_invocation , LLMInvocation
457+ ):
458+ return
459+
460+ error_otel = Error (message = str (error ), type = type (error ))
461+ llm_invocation = self ._telemetry_handler .fail_llm (
462+ invocation = llm_invocation , error = error_otel
463+ )
464+ if llm_invocation .span and not llm_invocation .span .is_recording ():
465+ self ._invocation_manager .delete_invocation_state (run_id = run_id )
0 commit comments