[opentelemetry-instrumentation-genai-langchain] Resolve message roles by class instead of BaseMessage.type - #506
Merged
lmolkova merged 9 commits intoSep 2, 2026
Conversation
A message produced by streaming reported its class name as the role: gen_ai.output.messages carried "role": "AIMessageChunk". Roles were mapped from BaseMessage.type, and every streaming *Chunk class subclasses its non-chunk counterpart while overriding type with its own class name, so the two stay distinguishable when deserialized. The class hierarchy identifies the role; type does not. Match on the class instead. This is not confined to streamed spans. convert_to_messages passes an AIMessageChunk through unchanged, so an application that keeps an accumulated streamed reply in its history also emitted "role": "AIMessageChunk" in gen_ai.input.messages of subsequent non-streamed calls, and on agent and workflow spans. role drives the role badge and the System/User/Assistant/Tool filter in consumers, so an affected row showed a raw Python class name and matched no filter value. The spec role values move to opentelemetry-util-genai as a Role enum, next to FinishReason. semconv constrains roles in the message JSON schemas, which are not code-generated into opentelemetry-semconv, so instrumentations have been spelling the literals out; google-genai already keeps a private copy of this enum. InputMessage.role and OutputMessage.role widen to str | Role, mirroring finish_reason, since the schemas also admit provider-specific roles.
sfc-gh-zeningchen
force-pushed
the
langchain-chunk-message-roles
branch
from
August 31, 2026 02:34
2e512c9 to
2488bf6
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes LangChain message-role normalization so streamed *Chunk messages emit GenAI-semconv roles (e.g., "assistant") instead of LangChain class names (e.g., "AIMessageChunk"), and introduces a shared Role enum in opentelemetry-util-genai to avoid role literal drift across instrumentations.
Changes:
- Add
Roleenum toopentelemetry.util.genai.typesand widen messagerolefields to acceptstr | Role. - Update LangChain role resolution to match by message class hierarchy (covers chunk variants) rather than
BaseMessage.type. - Add/extend tests to cover chunk-role mapping and
RoleJSON serialization.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/types.py | Adds Role enum and allows InputMessage/OutputMessage roles to be `str |
| util/opentelemetry-util-genai/tests/test_utils.py | Adds TestRole ensuring enum values match semconv and serialize correctly. |
| util/opentelemetry-util-genai/.changelog/506.added | Changelog fragment for the new Role enum. |
| instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py | Switches role normalization to class-based mapping (fixing streamed chunk roles). |
| instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py | Adds coverage for streamed output role and parametrized role normalization across message classes. |
| instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/506.fixed | Changelog fragment for the LangChain chunk-role fix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The test matched "\"role\":\"assistant\"" in the serialized output, so a change to the separators gen_ai_json_dumps passes would fail it even though the encoded value stayed correct. Parse the JSON and compare the field.
Pull request dashboard statusMerged · refreshed 2026-09-02 19:34 UTC Status above doesn't look right?
|
lmolkova
approved these changes
Aug 31, 2026
…ge-roles # Conflicts: # instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py # util/opentelemetry-util-genai/tests/test_utils.py
…ault Falling back to BaseMessage.type leaked LangChain names that are not spec roles, such as "chat" for ChatMessage and "remove" for RemoveMessage. _normalize_role now returns None for a class it does not map, and each caller supplies the role its context implies: user for input messages, assistant for output messages. Drop the str | Role annotation on the message dataclasses. Role subclasses str, so plain str already accepts its members.
…es.py Co-authored-by: Liudmila Molkova <neskazu@gmail.com>
…o langchain-chunk-message-roles
Every other LangChain message class encodes its role in the class, so matching by class resolves it. ChatMessage is the exception: it exists to carry an arbitrary speaker, keeps it in a role field, and reports "chat" as its type. It therefore fell through to the caller's default, and ChatMessage(role="assistant") was reported as user on the input side. Read the field for ChatMessage, which covers ChatMessageChunk through inheritance. An empty role falls back to the caller's default, since role is required on both message schemas.
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Sep 2, 2026
…ge-roles # Conflicts: # instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A message produced by streaming reported its LangChain class name as the role:
gen_ai.output.messagescarried"role": "AIMessageChunk".Roles were mapped from
BaseMessage.type. Every streaming*Chunkclasssubclasses its non-chunk counterpart while overriding
typewith its own classname, so the two stay distinguishable when deserialized. The class hierarchy
identifies the role and
typedoes not, so this matches on the class.The effect reaches beyond streamed spans.
convert_to_messagespasses anAIMessageChunkthrough unchanged, so an application that keeps an accumulatedstreamed reply in its history also emitted
"role": "AIMessageChunk"ingen_ai.input.messagesof later non-streamed calls, and on agent and workflowspans.
roledrives the role badge and the System/User/Assistant/Tool filter inconsumers, so an affected row shows a raw Python class name and matches no
filter value.
The spec role values move to
opentelemetry-util-genaias aRoleenum, nextto
FinishReason. semconv constrains roles in the message JSON schemas, whichare not code-generated into
opentelemetry-semconv, so instrumentations havebeen spelling the literals out; google-genai already keeps a private copy of
this enum.
InputMessage.roleandOutputMessage.rolewiden tostr | Role,mirroring
finish_reason, since the schemas also admit provider-specific roles.Fixes #504
Type of change
How has this been tested?
tests/test_callback_handler.pycovers all tenLangChain message classes, each chunk class alongside its non-chunk parent.
test_streamed_output_message_role_is_assistantcovers the role on astreamed
on_llm_end.opentelemetry-util-genai,TestRolechecks the values against themessage schemas and that a member set on a message serializes as
"assistant"rather than
"Role.ASSISTANT".Checklist