Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update langchain instrumentation to use latest semantic conventions
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from __future__ import annotations

from typing import Any, Optional, cast
from typing import Any, Optional
from uuid import UUID

from langchain_core.callbacks import BaseCallbackHandler
Expand All @@ -21,6 +21,9 @@
from opentelemetry.instrumentation.genai.langchain.utils import (
make_input_message,
make_last_output_message,
normalize_provider,
to_input_messages,
to_output_messages,
)
from opentelemetry.util.genai.handler import TelemetryHandler
from opentelemetry.util.genai.invocation import (
Expand All @@ -29,10 +32,7 @@
WorkflowInvocation,
)
from opentelemetry.util.genai.types import (
InputMessage,
MessagePart,
OutputMessage,
Text,
)


Expand Down Expand Up @@ -91,10 +91,9 @@ def on_chain_start(
else None
)
if suggested_agent_name_lower != agent_invocation_name_lower:
agent_provider = normalize_provider(metadata) or "unknown"
agent = self._telemetry_handler.invoke_local_agent(
provider=metadata.get("ls_provider", "unknown")
if metadata
else "unknown",
provider=agent_provider,
agent_name=suggested_agent_name,
)
agent.input_messages = make_input_message(inputs)
Expand Down Expand Up @@ -190,10 +189,6 @@ def on_chat_model_start(
metadata: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> None:
# Other providers/LLMs may be supported in the future and telemetry for them is skipped for now.
if serialized.get("name") not in ("ChatOpenAI", "ChatBedrock"):
return

if "invocation_params" in kwargs:
params = (
kwargs["invocation_params"].get("params")
Expand All @@ -202,20 +197,22 @@ def on_chat_model_start(
else:
params = kwargs

request_model = "unknown"
# Resolve request_model from common provider-specific keys.
request_model: Optional[str] = None
for model_tag in (
"model_name", # ChatOpenAI
"model_name", # ChatOpenAI / ChatAnthropic
"model_id", # ChatBedrock
"model", # ChatGoogleGenerativeAI / ChatVertexAI / ChatGroq / ChatMistralAI / ChatCohere / ChatOllama / ChatDeepSeek / ChatXAI
):
if (model := (params or {}).get(model_tag)) is not None:
request_model = model
request_model = str(model)
break
elif (model := (metadata or {}).get(model_tag)) is not None:
request_model = model
if (model := (metadata or {}).get(model_tag)) is not None:
request_model = str(model)
break

# Skip telemetry for unsupported request models
if request_model == "unknown":
if request_model is None:
return

# Initialize variables with default values to avoid "possibly unbound" errors
Expand All @@ -234,45 +231,28 @@ def on_chat_model_start(
stop_sequences = params.get("stop")
seed = params.get("seed")
temperature = params.get("temperature")
max_tokens = params.get("max_completion_tokens")
# ``max_completion_tokens`` is OpenAI-specific; fall back to the
# generic ``max_tokens`` used by Anthropic, Mistral, Cohere, etc.
max_tokens = params.get("max_completion_tokens") or params.get(
"max_tokens"
)

provider = "unknown"
provider = normalize_provider(metadata) or "unknown"
if metadata is not None:
provider = metadata.get("ls_provider", "unknown")

# Override with ChatBedrock values if present
if "ls_temperature" in metadata:
temperature = metadata.get("ls_temperature")
if "ls_max_tokens" in metadata:
max_tokens = metadata.get("ls_max_tokens")

input_messages: list[InputMessage] = []
for sub_messages in messages:
for message in sub_messages:
# Cast to Any to avoid type checking issues with LangChain's complex content type
raw_content: Any = message.content
role = message.type
parts: list[Text] = []

if isinstance(raw_content, str):
parts = [Text(content=raw_content, type="text")]
elif isinstance(raw_content, list):
for item in raw_content: # type: ignore[misc]
if isinstance(item, str):
parts.append(Text(content=item, type="text"))
elif isinstance(item, dict):
# Safely extract text content from dict
text_value = item.get("text") # type: ignore[misc]
if isinstance(text_value, str) and text_value:
parts.append(
Text(content=text_value, type="text")
)

input_messages.append(
InputMessage(
parts=cast(list[MessagePart], parts), role=role
)
)
# ``messages`` from on_chat_model_start is ``list[list[BaseMessage]]``
# (one inner list per generation request). Flatten and let
# :func:`to_input_messages` produce spec-conformant ``InputMessage`` s
# with proper roles, tool-call requests, tool results, and reasoning.
flattened: list[BaseMessage] = [
msg for sub in messages for msg in sub
]
input_messages = to_input_messages(flattened)

llm_invocation = self._telemetry_handler.inference(
provider,
Expand Down Expand Up @@ -311,57 +291,50 @@ def on_llm_end(
output_messages: list[OutputMessage] = []
for generation in getattr(response, "generations", []):
for chat_generation in generation:
# Get finish reason
finish_reason = "unknown" # Default value
message = chat_generation.message
if message is None:
continue

# Resolve finish_reason from generation_info or response
# metadata. Modern langchain-aws (>= 0.2) emits ``stop_reason``
# (snake_case); older versions used ``stopReason``. Empty
# values are filtered out by util-genai when emitting
# ``gen_ai.response.finish_reasons``.
finish_reason = ""
generation_info = getattr(
chat_generation, "generation_info", None
)
if generation_info is not None:
finish_reason = generation_info.get(
"finish_reason", "unknown"
finish_reason = generation_info.get("finish_reason", "")
if not finish_reason and message.response_metadata:
response_metadata = message.response_metadata
finish_reason = (
response_metadata.get("stop_reason")
or response_metadata.get("stopReason")
or ""
)

if chat_generation.message:
# Get finish reason if generation_info is None above
if (
generation_info is None
and chat_generation.message.response_metadata
):
finish_reason = (
chat_generation.message.response_metadata.get(
"stopReason", "unknown"
)
)

# Get message content
parts = [
Text(
content=chat_generation.message.content,
type="text",
)
]
role = chat_generation.message.type
output_message = OutputMessage(
role=role,
parts=cast(list[MessagePart], parts),
finish_reason=finish_reason,
)
output_messages.append(output_message)

# Get token usage if available
if chat_generation.message.usage_metadata:
input_tokens = (
chat_generation.message.usage_metadata.get(
"input_tokens", 0
)
)
# Convert via message_conversion so AIMessage tool calls,
# reasoning blocks, and structured content are preserved.
converted = to_output_messages(
[message], finish_reason=finish_reason
)
output_messages.extend(converted)

# Token usage (extracted regardless of whether the message
# produced output parts — some tool-call-only responses can
# still report token counts). Only set the counts when the
# provider actually reported them; defaulting to 0 would
# fabricate telemetry when the keys are absent.
if message.usage_metadata:
input_tokens = message.usage_metadata.get("input_tokens")
if input_tokens is not None:
llm_invocation.input_tokens = input_tokens

output_tokens = (
chat_generation.message.usage_metadata.get(
"output_tokens", 0
)
)
output_tokens = message.usage_metadata.get(
"output_tokens"
)
if output_tokens is not None:
llm_invocation.output_tokens = output_tokens

llm_invocation.output_messages = output_messages
Expand Down

This file was deleted.

Loading