Skip to content

langchain: preserve dict-form output messages - #417

Open
alexliluz wants to merge 4 commits into
open-telemetry:mainfrom
alexliluz:fix/langchain-dict-messages
Open

langchain: preserve dict-form output messages#417
alexliluz wants to merge 4 commits into
open-telemetry:mainfrom
alexliluz:fix/langchain-dict-messages

Conversation

@alexliluz

@alexliluz alexliluz commented Aug 20, 2026

Copy link
Copy Markdown

Description

LangChain accepts role/content dicts as messages. The input path already normalizes them, but the output path only kept AIMessage objects, so workflow and agent spans silently lost dict-form assistant output.

Normalize output messages with LangChain's convert_to_messages before building telemetry, and cover both the converter and on_chain_end path.

Fixes #388

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How has this been tested?

  • uv run tox -e py312-test-instrumentation-genai-langchain-latest (237 passed)
  • uv run tox -e py312-test-instrumentation-genai-langchain-oldest (236 passed, 1 skipped)
  • uv run tox -e typecheck
  • uv run tox -e precommit
  • uv run tox -e py314-test-instrumentation-genai-langchain-conformance (5 existing scenario skips)

Checklist

  • Followed the style guidelines of this project
  • Changelog updated if the change requires an entry
  • Unit tests added

AI-assisted implementation; I reproduced the missing output on current main, reviewed the diff, and ran the checks above locally.

@alexliluz
alexliluz marked this pull request as ready for review August 20, 2026 14:38
@alexliluz
alexliluz requested a review from a team as a code owner August 20, 2026 14:38
Copilot AI lite review requested due to automatic review settings August 20, 2026 14:38
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Aug 20, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-08-21 01:40 UTC

Respond to 6 review items (e.g. link a commit, explain why not, ask a follow-up):

  • Inline threads: 1, 2, 3, 4, 5
  • Top-level threads: 6
Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Should this be with reviewers? Comment /dashboard route:reviewers to route it to them.
  • Anything wrong — including the routing? Report it with what you expected; it helps us improve the dashboard.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes LangChain workflow/agent output telemetry losing assistant messages when outputs are provided in LangChain-supported dict form ({"role": ..., "content": ...}), by normalizing output messages with LangChain’s convert_to_messages before building gen_ai.output.messages.

Changes:

  • Normalize output-side messages via convert_to_messages in to_output_messages, with a safe fallback when normalization fails.
  • Add tests covering dict-form assistant outputs through both the converter (make_output_message) and the workflow on_chain_end path.
  • Add a changelog fragment documenting the fix.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py Normalizes output messages (including role/content dicts) before converting to OutputMessage.
instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py Adds regression tests ensuring dict-form assistant outputs are preserved in telemetry.
instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/417.fixed Documents the bug fix in the package changelog fragments.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

*input* side of the next inference call, not the output side of the
previous one.
LangChain-supported message representations, such as role/content dicts,
are normalized first. Non-``AIMessage`` entries are skipped: only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring needs to be fixed, the non-"ai messages" entries are skipped should be removed.

@@ -0,0 +1 @@
preserve dict-form assistant messages in workflow and agent output telemetry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, new line

@eternalcuriouslearner eternalcuriouslearner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!! requested few changes to address few loopholes.


def to_output_messages(
messages: Iterable[BaseMessage],
messages: Iterable[Any],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(blocking): Can we avoid using Any here?

normalized_messages: Iterable[BaseMessage] = convert_to_messages(
materialized_messages
)
except Exception: # pylint: disable=broad-except

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(blocking): Do we have a test for this? If not can you please add it?

except Exception: # pylint: disable=broad-except
normalized_messages = [
m for m in materialized_messages if isinstance(m, BaseMessage)
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question(blocking): Can we extract this logic into a function like say

def _normalize_messages(
    messages: Iterable[MessageLikeRepresentation],
) -> list[BaseMessage]:
    materialized = list(messages)

    try:
        return convert_to_messages(materialized)
    except Exception:  # pylint: disable=broad-except
        normalized: list[BaseMessage] = []

        for message in materialized:
            try:
                normalized.extend(convert_to_messages([message]))
            except Exception:  # pylint: disable=broad-except
                continue

        return normalized

and use this logic for both to_output_messages and to_input_messages functions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

langchain: dict-form messages raise in on_chain_start and silently drop the span

4 participants