8181logger = logging .getLogger (__name__ )
8282
8383
84+ def _as_dict (value : Any ) -> Optional [dict [str , Any ]]:
85+ if isinstance (value , dict ):
86+ return cast (dict [str , Any ], value )
87+ return None
88+
89+
8490def _has_goto (output : Any ) -> bool :
8591 """Detect LangGraph ``Command(goto=...)`` patterns in chain/tool output.
8692
@@ -96,17 +102,18 @@ def _has_goto(output: Any) -> bool:
96102 return bool (getattr (output , "goto" , None ))
97103
98104 # Dict — check for "goto" key or Command-like values
99- if isinstance (output , dict ):
100- if output .get ("goto" ):
105+ output_dict = _as_dict (output )
106+ if output_dict is not None :
107+ if output_dict .get ("goto" ):
101108 return True
102- for val in output .values ():
109+ for val in output_dict .values ():
103110 if hasattr (val , "goto" ) and hasattr (val , "update" ):
104111 if getattr (val , "goto" , None ):
105112 return True
106113
107114 # List/tuple — check elements
108115 if isinstance (output , (list , tuple )):
109- for item in output :
116+ for item in cast ( Any , output ) :
110117 if hasattr (item , "goto" ) and hasattr (item , "update" ):
111118 if getattr (item , "goto" , None ):
112119 return True
@@ -120,7 +127,8 @@ def _extract_chain_messages(data: Any) -> Any:
120127 LangChain stores messages under various keys depending on the
121128 chain type. Returns the first non-None value found, or ``None``.
122129 """
123- if not isinstance (data , dict ):
130+ data_dict = _as_dict (data )
131+ if data_dict is None :
124132 return data
125133
126134 for key in (
@@ -133,7 +141,7 @@ def _extract_chain_messages(data: Any) -> Any:
133141 "answer" ,
134142 "response" ,
135143 ):
136- value = data .get (key )
144+ value = data_dict .get (key )
137145 if value is not None :
138146 return value
139147
@@ -188,10 +196,10 @@ def _handle_model_start(
188196 ** kwargs : Any ,
189197 ) -> None :
190198 """Shared logic for on_chat_model_start and on_llm_start."""
191- if "invocation_params" in kwargs :
199+ invocation_params = _as_dict (kwargs .get ("invocation_params" ))
200+ if invocation_params is not None :
192201 params = (
193- kwargs ["invocation_params" ].get ("params" )
194- or kwargs ["invocation_params" ]
202+ _as_dict (invocation_params .get ("params" )) or invocation_params
195203 )
196204 else :
197205 params = kwargs
@@ -204,10 +212,13 @@ def _handle_model_start(
204212 "engine" ,
205213 "deployment_name" ,
206214 ):
207- if (model := ( params or {}) .get (model_tag )) is not None :
215+ if (model := params .get (model_tag )) is not None :
208216 request_model = model
209217 break
210- elif (model := (metadata or {}).get (model_tag )) is not None :
218+ if (
219+ metadata is not None
220+ and (model := metadata .get (model_tag )) is not None
221+ ):
211222 request_model = model
212223 break
213224
@@ -220,16 +231,14 @@ def _handle_model_start(
220231 temperature = None
221232 max_tokens = None
222233
223- if params is not None :
224- top_p = params .get ("top_p" )
225- frequency_penalty = params .get ("frequency_penalty" )
226- presence_penalty = params .get ("presence_penalty" )
227- stop_sequences = params .get ("stop" )
228- seed = params .get ("seed" )
229- temperature = params .get ("temperature" )
230- max_tokens = params .get ("max_completion_tokens" )
234+ top_p = params .get ("top_p" )
235+ frequency_penalty = params .get ("frequency_penalty" )
236+ presence_penalty = params .get ("presence_penalty" )
237+ stop_sequences = params .get ("stop" )
238+ seed = params .get ("seed" )
239+ temperature = params .get ("temperature" )
240+ max_tokens = params .get ("max_completion_tokens" )
231241
232- invocation_params = kwargs .get ("invocation_params" ) or {}
233242 provider = infer_provider_name (serialized , metadata , invocation_params )
234243 if provider is None :
235244 provider = "unknown"
@@ -247,27 +256,27 @@ def _handle_model_start(
247256 # Additional semconv request attributes
248257 extra_attrs : dict [str , Any ] = {}
249258
250- top_k = params .get ("top_k" ) if params else None
259+ top_k = params .get ("top_k" )
251260 if top_k is not None :
252261 extra_attrs [GenAI .GEN_AI_REQUEST_TOP_K ] = top_k
253262
254263 # Choice count (n) — only set if != 1
255- choice_count = params .get ("n" ) if params else None
264+ choice_count = params .get ("n" )
256265 if isinstance (choice_count , int ) and choice_count != 1 :
257266 extra_attrs [GenAI .GEN_AI_REQUEST_CHOICE_COUNT ] = choice_count
258267
259268 # Output type from response_format
260- if params :
261- response_format = params . get ( " response_format" )
262- if isinstance ( response_format , dict ) :
263- output_type = response_format .get ("type" )
264- if output_type is not None :
265- extra_attrs [GenAI .GEN_AI_OUTPUT_TYPE ] = output_type
266- elif isinstance (response_format , str ):
267- extra_attrs [GenAI .GEN_AI_OUTPUT_TYPE ] = response_format
269+ response_format = params . get ( "response_format" )
270+ response_format_dict = _as_dict ( response_format )
271+ if response_format_dict is not None :
272+ output_type = response_format_dict .get ("type" )
273+ if output_type is not None :
274+ extra_attrs [GenAI .GEN_AI_OUTPUT_TYPE ] = output_type
275+ elif isinstance (response_format , str ):
276+ extra_attrs [GenAI .GEN_AI_OUTPUT_TYPE ] = response_format
268277
269278 # Encoding formats
270- encoding_format = params .get ("encoding_format" ) if params else None
279+ encoding_format = params .get ("encoding_format" )
271280 if encoding_format is not None :
272281 extra_attrs [GenAI .GEN_AI_REQUEST_ENCODING_FORMATS ] = [
273282 encoding_format
@@ -457,22 +466,25 @@ def on_llm_end(
457466 generation_info = getattr (
458467 chat_generation , "generation_info" , None
459468 )
460- if generation_info is not None :
461- finish_reason = generation_info .get (
469+ generation_info_dict = _as_dict (generation_info )
470+ if generation_info_dict is not None :
471+ finish_reason = generation_info_dict .get (
462472 "finish_reason" , "unknown"
463473 )
464474
465475 if chat_generation .message :
466476 # Get finish reason if generation_info is None above
467477 if (
468- generation_info is None
478+ generation_info_dict is None
469479 and chat_generation .message .response_metadata
470480 ):
471- finish_reason = (
472- chat_generation .message .response_metadata .get (
481+ response_metadata = _as_dict (
482+ chat_generation .message .response_metadata
483+ )
484+ if response_metadata is not None :
485+ finish_reason = response_metadata .get (
473486 "stopReason" , "unknown"
474487 )
475- )
476488
477489 # Get message content
478490 parts = [
@@ -490,35 +502,26 @@ def on_llm_end(
490502 output_messages .append (output_message )
491503
492504 # Get token usage if available
493- if chat_generation .message .usage_metadata :
494- input_tokens = (
495- chat_generation .message .usage_metadata .get (
496- "input_tokens" , 0
497- )
498- )
505+ usage_metadata = _as_dict (
506+ chat_generation .message .usage_metadata
507+ )
508+ if usage_metadata is not None :
509+ input_tokens = usage_metadata .get ("input_tokens" , 0 )
499510 llm_invocation .input_tokens = input_tokens
500511
501- output_tokens = (
502- chat_generation .message .usage_metadata .get (
503- "output_tokens" , 0
504- )
505- )
512+ output_tokens = usage_metadata .get ("output_tokens" , 0 )
506513 llm_invocation .output_tokens = output_tokens
507514
508515 # Cache token attributes (when provider exposes them)
509516 # Check direct keys (Anthropic-style) and input_token_details (LangChain-style)
510- cache_read = (
511- chat_generation .message .usage_metadata .get (
512- "cache_read_input_tokens"
513- )
517+ cache_read = usage_metadata .get (
518+ "cache_read_input_tokens"
514519 )
515520 if cache_read is None :
516- input_token_details = (
517- chat_generation .message .usage_metadata .get (
518- "input_token_details"
519- )
521+ input_token_details = _as_dict (
522+ usage_metadata .get ("input_token_details" )
520523 )
521- if isinstance ( input_token_details , dict ) :
524+ if input_token_details is not None :
522525 cache_read = input_token_details .get (
523526 "cache_read"
524527 )
@@ -528,18 +531,14 @@ def on_llm_end(
528531 int (cache_read ),
529532 )
530533
531- cache_creation = (
532- chat_generation .message .usage_metadata .get (
533- "cache_creation_input_tokens"
534- )
534+ cache_creation = usage_metadata .get (
535+ "cache_creation_input_tokens"
535536 )
536537 if cache_creation is None :
537- input_token_details = (
538- chat_generation .message .usage_metadata .get (
539- "input_token_details"
540- )
538+ input_token_details = _as_dict (
539+ usage_metadata .get ("input_token_details" )
541540 )
542- if isinstance ( input_token_details , dict ) :
541+ if input_token_details is not None :
543542 cache_creation = input_token_details .get (
544543 "cache_creation"
545544 )
@@ -551,7 +550,7 @@ def on_llm_end(
551550
552551 llm_invocation .output_messages = output_messages
553552
554- llm_output = getattr (response , "llm_output" , None )
553+ llm_output = _as_dict ( getattr (response , "llm_output" , None ) )
555554 if llm_output is not None :
556555 response_model = llm_output .get ("model_name" ) or llm_output .get (
557556 "model"
@@ -846,9 +845,10 @@ def on_agent_action(
846845 else None
847846 )
848847 if record :
848+ tool_input : Any = getattr (action , "tool_input" , None )
849849 record .stash .setdefault ("pending_actions" , {})[str (run_id )] = {
850850 "tool" : action .tool ,
851- "tool_input" : action . tool_input ,
851+ "tool_input" : tool_input ,
852852 "log" : action .log ,
853853 }
854854
@@ -865,10 +865,11 @@ def on_agent_finish(
865865 record = self ._span_manager .get_record (str (run_id ))
866866 if not record :
867867 return
868- if finish .return_values :
868+ return_values : Any = getattr (finish , "return_values" , None )
869+ if return_values :
869870 record .span .set_attribute (
870871 GenAI .GEN_AI_OUTPUT_MESSAGES ,
871- json .dumps (finish . return_values , default = str ),
872+ json .dumps (return_values , default = str ),
872873 )
873874 record .span .set_status (Status (StatusCode .OK ))
874875 self ._span_manager .end_span (run_id )
0 commit comments