Add LangChain invoke workflow and invoke agent span support#4449
Add LangChain invoke workflow and invoke agent span support#4449wrisa wants to merge 47 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds workflow-level tracing for LangChain (top-level chain runs) and refactors LLM span creation to use the newer GenAI invocation APIs, including an example and expanded test coverage.
Changes:
- Add workflow invocation (INTERNAL span) start/stop/error handling via
on_chain_*callbacks. - Refactor LLM spans to use
InferenceInvocation(start_inference(),stop(),fail()), and introduce workflow invocation tracking. - Add
opentelemetry-util-genaidependency plus a LangGraph workflow example and new tests.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| instrumentation-genai/opentelemetry-instrumentation-langchain/tests/test_workflow_chain.py | Adds unit tests validating workflow span creation, CSA propagation, and error/no-op paths. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/invocation_manager.py | Makes invocation state nullable to support runs without an associated GenAI invocation. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py | Implements workflow spans for chains and migrates LLM handling to InferenceInvocation. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/pyproject.toml | Updates core instrumentation dependency and adds explicit opentelemetry-util-genai dependency. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/examples/workflow/requirements.txt | Adds dependencies for the new workflow example. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/examples/workflow/main.py | Adds a LangGraph StateGraph workflow example that invokes an LLM node. |
| instrumentation-genai/opentelemetry-instrumentation-langchain/CHANGELOG.md | Notes the new workflow span support/refactor in the changelog. |
| ) | ||
| return LANGGRAPH_IDENTIFIER in name or LANGGRAPH_IDENTIFIER in graph_id | ||
|
|
||
| return True |
There was a problem hiding this comment.
Just curious, if a top level chain has no serialized data, should it be classified as a workflow?
There was a problem hiding this comment.
The fallback return True is intentional — it handles the case where LangGraph chain doesn't populate serialized but is still the root invocation. Without this, top-level chains with missing serialization would be silently suppressed, producing no span at all for the outermost operation.
I can add clarification comment that would make the intent explicit ?
# No serialized data to inspect, but this is a top-level chain
# (parent_run_id is None). Treat it as a workflow so the outermost
# invocation always gets a span, even if the chain didn't serialize itself.
return True
lmolkova
left a comment
There was a problem hiding this comment.
When I run the example, I see
Not sure if we should expect to see invoke_agent somewhere?
I've tried modifying it to be more complicated
example
```python def build_graph(llm: ChatOpenAI): """Draft → critique → (revise | END) workflow over ``GraphState``."""def draft_node(state: GraphState) -> dict[str, str]:
response = llm.invoke(
[
SystemMessage(
content=(
"You are a concise technical writer. "
"Write a short first-pass response (3-5 sentences)."
)
),
HumanMessage(content=state["request"]),
]
)
return {"draft": response.content}
def critique_node(state: GraphState) -> dict[str, str]:
response = llm.invoke(
[
SystemMessage(
content=(
"You are a strict editor. Review the draft below for "
"factual accuracy, clarity, and tone.\n"
f"If the draft is acceptable as-is, reply with just '{APPROVAL_TOKEN}'.\n"
"Otherwise, list the specific issues to fix in 1-3 bullet points."
)
),
HumanMessage(
content=(
f"Original request:\n{state['request']}\n\n"
f"Draft:\n{state['draft']}"
)
),
]
)
return {"critique": str(response.content)}
def route_after_critique(
state: GraphState,
) -> Literal["revise", "finalize"]:
critique = state["critique"].strip()
return "finalize" if critique.startswith(APPROVAL_TOKEN) else "revise"
def revise_node(state: GraphState) -> dict:
response = llm.invoke(
[
SystemMessage(
content=(
"You are the same technical writer. Rewrite your "
"draft to address every issue raised by the editor. "
"Keep the response concise."
)
),
HumanMessage(
content=(
f"Original request:\n{state['request']}\n\n"
f"Your draft:\n{state['draft']}\n\n"
f"Editor's critique:\n{state['critique']}"
)
),
]
)
return {"final": response.content}
def finalize_node(state: GraphState) -> dict:
return {"final": state["draft"]}
builder = StateGraph(GraphState)
builder.add_node("draft", draft_node)
builder.add_node("critique", critique_node)
builder.add_node("revise", revise_node)
builder.add_node("finalize", finalize_node)
builder.add_edge(START, "draft")
builder.add_edge("draft", "critique")
builder.add_conditional_edges(
"critique",
route_after_critique,
{"revise": "revise", "finalize": "finalize"},
)
builder.add_edge("revise", END)
builder.add_edge("finalize", END)
return builder.compile()
</details>
Here's what I see from this branch:
<img width="888" height="180" alt="Image" src="https://github.com/user-attachments/assets/ecdd0a3e-3146-4ecd-8a56-64764117e8da" />
here's what I see from OpenInference
<img width="893" height="293" alt="Image" src="https://github.com/user-attachments/assets/dee41518-835d-445b-93c7-78f334932a32" />
-----
TL;DR:
It seems we're missing agent-level spans in workflow scenarios, I'd like to make sure we didn't introduce something here that broke it or would prevent from having them in the future
| run_id, parent_run_id, None | ||
| ) | ||
|
|
||
| def on_chain_end( |
There was a problem hiding this comment.
I don't see any content attributes on the workflow span (input/output/instructions) - is it intentional?
The nodes draft, critique, revise, finalize are plain Python functions registered as LangGraph nodes. When LangGraph calls LangGraph doesn't mark its nodes as agents — it's just a graph of Python functions. Hence, there will not be |
Can we discuss it? It seems logical that individual nodes become agents and orchestration over them becomes a workflow. It also aligns with OpenInference and Langfuse if I read this image right. |
lmolkova
left a comment
There was a problem hiding this comment.
@wrisa could you please more this PR to https://github.com/open-telemetry/opentelemetry-python-genai and let's continue the discussion there? Sorry for the inconvenience.
Langfuse (langfuse-python)
Both actually apply the same name-based heuristic as the current implementation — they only mark something as "agent" if the word "agent" appears in the run name or class path. Created issue for further discussion. |
|
|
Closing in favor of open-telemetry/opentelemetry-python-genai#25 |


Description
on_chain_start,on_chain_startandon_chain_errorfor tracking workflow invocation and agent invocation alongside the existing LLM inference spans.examples/agent/main.py)examples/agent/single_node_agent.py)examples/workflow/main.py)Other changes:
opentelemetry-util-genaias an explicit dependency inpyproject.toml.See workflow and inference spans:

See agent and inference spans:

See workflow, agent and inference spans:

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. Please also list any relevant details for your test configuration
Does This PR Require a Core Repo Change?
Checklist:
See contributing.md for styleguide, changelog guidelines, and more.