[opentelemetry-instrumentation-genai-langchain] Capture prompt template name and variables on chat - #730
[opentelemetry-instrumentation-genai-langchain] Capture prompt template name and variables on chat#730rads-1996 wants to merge 14 commits into
Conversation
Pull request dashboard statusWaiting on the author · refreshed 2026-09-18 21:10 UTC Move out of draft to request review. Status above doesn't look right?
|
There was a problem hiding this comment.
🟡 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
metadatais typed asdict[str, Any], so a non-stringprompt_nameis assigned directly toInferenceInvocation.prompt_nameand can emit an invalid non-stringgen_ai.prompt.nameattribute. Normalize this metadata value to a string (or ignore it), matching the existing metadata-name handling inresolve_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
namekwarg (including theserialized=Nonecase already handled above), this remainsNone, so aPromptTemplaterun is not recognized and its context never reaches the chat span. Use the callback name as a fallback, asresolve_agent_namedoes inoperation_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_variablesand can carrypartial_variables; extra chain-state keys are not template variables, while partials are rendered values. Emitting rawinputstherefore 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 | model2leaves the first prompt context attached to the sequence and incorrectly adds those attributes tomodel2, which receivesmodel1'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
ChatPromptTemplateaccepts non-JSON values such aslist[BaseMessage]for aMessagesPlaceholder. When content capture is enabled,InferenceInvocationserializes every non-string prompt variable withjson.dumps; these message objects are not handled by the util encoder, so finalizing the chat can raiseTypeErrorfrom 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
invokepath. This instrumentation also emits chat spans forstreamandastream, 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(), butChatPromptTemplate | ChatOpenAIalso 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.
8e90f61 to
07e9354
Compare
There was a problem hiding this comment.
🟡 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
bda8eee to
14ad9ed
Compare
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
🔵 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_dumpsdirectly 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 duringinvocation.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
ChatPromptTemplateserializes optional placeholders (for exampleMessagesPlaceholder(optional=True)) underkwargs["optional_variables"], but this set only includesinput_variablesand partials. Values supplied for an optional placeholder are therefore dropped, so the correspondinggen_ai.prompt.variable.<name>is never emitted; include validatedoptional_variableswhen buildingdeclared_namesand 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 thestr | Noneprompt-name field. If a caller supplies a non-string value, the finished span receives a non-stringgen_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
PromptTemplateto be invoked with a scalar, andon_chain_startreceives that original scalar before LangChain normalizes it to a mapping. With such a call,name in inputseither drops the variable orinputs[name]raisesTypeError, 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 innerRunnableSequencewhile 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.nameis the name that identifies the prompt template, buttemplate_typeis only the class name. For an ordinaryPromptTemplate.from_template(...)withoutnameorprompt_name, this emits the samePromptTemplatevalue 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 passNoneunless 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
Description
Fixes - #585
Type of change
Please delete options that are not relevant.
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.
Checklist
See CONTRIBUTING.md
for the style guide, changelog guidance, and more.