Skip to content

Commit 05e1357

Browse files
nagkumar91Copilot
andcommitted
fix(langchain): clear PR checks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 06501f8 commit 05e1357

7 files changed

Lines changed: 183 additions & 137 deletions

File tree

instrumentation-genai/opentelemetry-instrumentation-langchain/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Enhanced the LangChain instrumentor with semconv-aligned model, agent,
11+
workflow, tool, and retriever tracing plus richer event and W3C propagation
12+
handling. ([#4389](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4389))
1013
- Added span support for genAI langchain llm invocation.
1114
([#3665](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3665))
1215
- Added support to call genai utils handler for langchain LLM invocations.

instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py

Lines changed: 71 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@
8181
logger = 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+
8490
def _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)

instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/content_recording.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
content should be recorded on spans and events.
1919
"""
2020

21+
from typing import Optional
22+
2123
from opentelemetry.util.genai.types import ContentCapturingMode
2224
from opentelemetry.util.genai.utils import (
2325
get_content_capturing_mode,
@@ -91,7 +93,7 @@ def should_record_system_instructions(policy: ContentPolicy) -> bool:
9193

9294
# -- Default singleton --------------------------------------------------------
9395

94-
_default_policy: ContentPolicy | None = None
96+
_default_policy: Optional[ContentPolicy] = None
9597

9698

9799
def get_content_policy() -> ContentPolicy:

0 commit comments

Comments
 (0)