Skip to content

[opentelemetry-instrumentation-genai-langchain] Capture prompt template name and variables on chat - #730

Draft
rads-1996 wants to merge 14 commits into
open-telemetry:mainfrom
rads-1996:support-prompt-vars-on-chat
Draft

rads-1996 wants to merge 14 commits into
open-telemetry:mainfrom
rads-1996:support-prompt-vars-on-chat

Conversation

@rads-1996

Copy link
Copy Markdown
Contributor

Description

Fixes - #585

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How has this been tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. List any relevant details for your test
configuration.

  • uv run tox -e py312-test-instrumentation-genai-langchain-- -q
  • uv run tox -e py312-test-instrumentation-genai-langchain-conformance -- -q
  • uv run --python 3.12 tox -e lint-instrumentation-genai-langchain

Checklist

See CONTRIBUTING.md
for the style guide, changelog guidance, and more.

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

@rads-1996 rads-1996 changed the title Capture prompt template name and variables on chat [opentelemetry-instrumentation-genai-langchain] Capture prompt template name and variables on chat Sep 16, 2026
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Sep 17, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-09-18 21:10 UTC

Move out of draft to request review.

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.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@rads-1996
rads-1996 marked this pull request as ready for review September 17, 2026 15:25
@rads-1996
rads-1996 requested a review from a team as a code owner September 17, 2026 15:25
Copilot AI lite review requested due to automatic review settings September 17, 2026 15:25

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.

🟡 Changes recommended

Unresolved moderate callback-handling findings require changes before approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds LangChain prompt-template names and variables to chat telemetry.

Changes:

  • Captures and propagates prompt context.
  • Adds unit/integration tests and VCR cassettes.
  • Adds a changelog entry.
File summaries
File Reviewed changes and findings
instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_llm_call.py Adds integration coverage. Nits (1 vote each): add sync/async streaming and async .ainvoke() coverage.
instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_invocation_manager.py Tests prompt-context storage.
instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py Tests callback propagation.
instrumentation/opentelemetry-instrumentation-genai-langchain/tests/cassettes/test_chat_openai_prompt_template.yaml Adds prompt-template replay data.
instrumentation/opentelemetry-instrumentation-genai-langchain/tests/cassettes/test_chat_openai_chat_prompt_template.yaml Adds chat prompt-template replay data.
instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/invocation_manager.py Stores prompt context.
instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py Moderate findings: normalize prompt names, use callback-name and ID fallbacks, derive and safely serialize template variables, and scope prompt context correctly. Findings have 1 vote each except ID detection, which has 2 votes.
instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/730.added Documents the feature.
Review details

Suppressed comments (7)

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

  • metadata is typed as dict[str, Any], so a non-string prompt_name is assigned directly to InferenceInvocation.prompt_name and can emit an invalid non-string gen_ai.prompt.name attribute. Normalize this metadata value to a string (or ignore it), matching the existing metadata-name handling in resolve_agent_name.
                name=(metadata or {}).get("prompt_name") or template_type,

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

  • When LangChain supplies the run name through the callback name kwarg (including the serialized=None case already handled above), this remains None, so a PromptTemplate run is not recognized and its context never reaches the chat span. Use the callback name as a fallback, as resolve_agent_name does in operation_mapping.py:122-128.
        template_type = serialized.get("name")

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

  • This stores the entire chain input as prompt variables. LangChain prompt serialization exposes the declared input_variables and can carry partial_variables; extra chain-state keys are not template variables, while partials are rendered values. Emitting raw inputs therefore leaks unrelated state and omits partial values. Derive the mapping from the serialized template variables and merge the rendered partials before assigning it.
                variables=inputs,

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

  • The prompt context is stored on the parent run but is never consumed after a chat reads it. LangChain RunnableSequence invokes sibling steps under that same parent, so a chain such as prompt | model1 | model2 leaves the first prompt context attached to the sequence and incorrectly adds those attributes to model2, which receives model1's output rather than the template. Consume the context for the associated chat or scope it to the next model run.
        if parent_run_id is not None:
            prompt_context = self._invocation_manager.get_prompt_context(
                parent_run_id
            )

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

  • ChatPromptTemplate accepts non-JSON values such as list[BaseMessage] for a MessagesPlaceholder. When content capture is enabled, InferenceInvocation serializes every non-string prompt variable with json.dumps; these message objects are not handled by the util encoder, so finalizing the chat can raise TypeError from telemetry. Normalize such values or make prompt-variable serialization failure-safe before passing arbitrary inputs through.
                variables=inputs,

instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_llm_call.py:110

  • The new coverage only exercises the non-streaming invoke path. This instrumentation also emits chat spans for stream and astream, so add equivalent sync and async streaming coverage to verify that the prompt name and variables survive stream finalization.
            response = (prompt | model).invoke(
                {"question": "What's the weather like in Seattle?"},
                config={"metadata": {"prompt_name": "weather_prompt"}},
            )

instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_llm_call.py:107

  • The new integration coverage only exercises synchronous .invoke(), but ChatPromptTemplate | ChatOpenAI also supports asynchronous .ainvoke() through LangChain's async callback-manager path. Add a VCR-backed async case that asserts the same prompt attributes; otherwise this new propagation is unverified for a supported call variant.
def test_chat_openai_prompt_template(
    span_exporter, tracer_provider, meter_provider, logger_provider, vcr
):
    model = ChatOpenAI(model="gpt-4.1", max_tokens=100)
    prompt = PromptTemplate.from_template(
        "Answer this weather question briefly: {question}"
    )

    with instrument(
        LangChainInstrumentor(),
        tracer_provider=tracer_provider,
        meter_provider=meter_provider,
        logger_provider=logger_provider,
        content_capture="SPAN_ONLY",
    ):
        with vcr.use_cassette("test_chat_openai_prompt_template.yaml"):
            response = (prompt | model).invoke(
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@rads-1996
rads-1996 marked this pull request as draft September 17, 2026 16:53
@rads-1996
rads-1996 force-pushed the support-prompt-vars-on-chat branch from 8e90f61 to 07e9354 Compare September 17, 2026 20:45
@rads-1996
rads-1996 requested a lite review from Copilot September 17, 2026 20:55

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.

🟡 Changes recommended

Prompt variables currently include unused inputs and async propagation lacks coverage.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Lite

@rads-1996
rads-1996 force-pushed the support-prompt-vars-on-chat branch from bda8eee to 14ad9ed Compare September 18, 2026 16:04

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.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

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.

🔵 Needs a closer look

One or more issues must be addressed before approval.

Review details

Suppressed comments (6)

Previously missed (1) — in code that hasn't changed since the last review.

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

  • These values are arbitrary objects, but the shared prompt-variable serializer later calls gen_ai_json_dumps directly for every non-string value without a fallback. A normal non-JSON-serializable prompt input (for example a custom object or message list) can therefore raise during invocation.stop() and turn a successful chat into a callback failure; normalize unsupported values or make the shared serializer fail open, with a regression test for this path.

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

  • ChatPromptTemplate serializes optional placeholders (for example MessagesPlaceholder(optional=True)) under kwargs["optional_variables"], but this set only includes input_variables and partials. Values supplied for an optional placeholder are therefore dropped, so the corresponding gen_ai.prompt.variable.<name> is never emitted; include validated optional_variables when building declared_names and add a regression test.
    declared_names = set(input_variables) | partial_names
    for name in declared_names:
        if name in inputs:
            variables[name] = inputs[name]

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

  • metadata["prompt_name"] is untyped but is passed directly into the str | None prompt-name field. If a caller supplies a non-string value, the finished span receives a non-string gen_ai.prompt.name, violating the semantic-convention attribute type; stringify or validate this metadata value before storing it, as the other metadata-derived names do.
                name=(metadata or {}).get("prompt_name")
                or serialized_name
                or template_type,

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

  • LangChain permits a single-variable PromptTemplate to be invoked with a scalar, and on_chain_start receives that original scalar before LangChain normalizes it to a mapping. With such a call, name in inputs either drops the variable or inputs[name] raises TypeError, so prompt variables are not captured for a supported invocation. Normalize a non-mapping input to the sole declared variable before this loop.
    declared_names = set(input_variables) | partial_names
    for name in declared_names:
        if name in inputs:
            variables[name] = inputs[name]

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

  • The context is stored on the prompt run's immediate parent, but this lookup only checks the model run's immediate parent. In a valid nested composition such as (prompt | transform) | model, the prompt's parent is the inner RunnableSequence while the model's parent is the outer sequence, so the new attributes are silently absent; propagate the context to the enclosing model run or use a hierarchy-aware association, with a nested-sequence regression test.
        if parent_run_id is not None:
            prompt_context = self._invocation_manager.get_prompt_context(
                parent_run_id
            )

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

  • gen_ai.prompt.name is the name that identifies the prompt template, but template_type is only the class name. For an ordinary PromptTemplate.from_template(...) without name or prompt_name, this emits the same PromptTemplate value for every template (and similarly for chat templates), rather than omitting the attribute when no name is available. Keep the type only for classification and pass None unless metadata or a distinct serialized name is present.
                name=(metadata or {}).get("prompt_name")
                or serialized_name
                or template_type,
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants