Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
529ebd7
feat: add agent span support in langchain instrumentation
yiyuan-he Sep 24, 2025
b1fe20f
Merge branch 'main' into langchain-agent-spans
yiyuan-he Sep 25, 2025
d030990
fix precommit and typechecks
yiyuan-he Sep 25, 2025
87b6b05
Merge branch 'main' into langchain-agent-spans
yiyuan-he Sep 25, 2025
f1d1cee
update changelog with PR number
yiyuan-he Sep 26, 2025
207c54d
Merge branch 'main' into langchain-agent-spans
yiyuan-he Sep 29, 2025
fdacebd
Merge branch 'main' into langchain-agent-spans
yiyuan-he Sep 30, 2025
d3b2528
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 1, 2025
1b98f9f
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 2, 2025
ae6bf62
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 3, 2025
744ddb6
address pr comments
yiyuan-he Oct 6, 2025
82bfb84
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 9, 2025
6fb9f9f
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 10, 2025
7807450
apply ruff format
yiyuan-he Oct 13, 2025
9058d97
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 13, 2025
e95d658
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 15, 2025
86033c3
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 16, 2025
4de0f08
Merge branch 'main' into langchain-agent-spans
yiyuan-he Oct 21, 2025
c33571b
Merge branch 'main' into langchain-agent-spans
yiyuan-he Nov 24, 2025
370d930
address pr comments for agent span creation
Nov 25, 2025
f97424c
Merge branch 'main' into langchain-agent-spans
yiyuan-he Nov 25, 2025
ca54118
Merge branch 'main' into langchain-agent-spans
yiyuan-he Dec 3, 2025
e0881b9
Merge branch 'main' into langchain-agent-spans
yiyuan-he Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Added agent span support for GenAI LangChain instrumentation with `invoke_agent` operation and chain tracking.
([#3788](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3788))
- Added span support for genAI langchain llm invocation.
([#3665](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3665))
([#3665](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3665))
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
from typing import Any
from uuid import UUID

from langchain_core.agents import AgentAction, AgentFinish # type: ignore
from langchain_core.callbacks import BaseCallbackHandler # type: ignore
from langchain_core.messages import BaseMessage # type: ignore
from langchain_core.outputs import LLMResult # type: ignore

from opentelemetry.instrumentation.langchain.span_manager import _SpanManager
from opentelemetry.instrumentation.langchain.span_manager import (
_OPERATION_INVOKE_AGENT,
_SpanManager,
)
from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAI,
)
Expand Down Expand Up @@ -49,9 +53,9 @@ def on_chat_model_start(
messages: list[list[BaseMessage]], # type: ignore
*,
run_id: UUID,
tags: list[str] | None,
parent_run_id: UUID | None,
metadata: dict[str, Any] | None,
tags: list[str] | None = None,
parent_run_id: UUID | None = None,
metadata: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
# Other providers/LLMs may be supported in the future and telemetry for them is skipped for now.
Expand Down Expand Up @@ -141,7 +145,7 @@ def on_llm_end(
response: LLMResult, # type: ignore [reportUnknownParameterType]
*,
run_id: UUID,
parent_run_id: UUID | None,
parent_run_id: UUID | None = None,
**kwargs: Any,
) -> None:
span = self.span_manager.get_span(run_id)
Expand Down Expand Up @@ -218,7 +222,114 @@ def on_llm_error(
error: BaseException,
*,
run_id: UUID,
parent_run_id: UUID | None,
parent_run_id: UUID | None = None,
**kwargs: Any,
) -> None:
self.span_manager.handle_error(error, run_id)

def on_chain_start(
self,
serialized: dict[str, Any],
inputs: dict[str, Any],
*,
run_id: UUID,
parent_run_id: UUID | None = None,
tags: list[str] | None = None,
metadata: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
"""Run when chain starts running."""
# Extract chain name from serialized or kwargs
chain_name = "unknown"
if (
serialized
and "kwargs" in serialized
and serialized["kwargs"].get("name")
):
chain_name = serialized["kwargs"]["name"]
elif kwargs.get("name"):
chain_name = kwargs["name"]
elif serialized.get("name"):
chain_name = serialized["name"]
elif "id" in serialized:
chain_name = serialized["id"][-1]

span = self.span_manager.create_chain_span(
Comment thread
lmolkova marked this conversation as resolved.
run_id=run_id,
parent_run_id=parent_run_id,
chain_name=chain_name,
)

# If this is an agent chain, set agent-specific attributes
if metadata and "agent_name" in metadata:
span.set_attribute(GenAI.GEN_AI_AGENT_NAME, metadata["agent_name"])
span.set_attribute(
GenAI.GEN_AI_OPERATION_NAME, _OPERATION_INVOKE_AGENT
)

def on_chain_end(
self,
outputs: dict[str, Any],
*,
run_id: UUID,
parent_run_id: UUID | None = None,
tags: list[str] | None = None,
**kwargs: Any,
) -> None:
"""Run when chain ends running."""
self.span_manager.end_span(run_id)

def on_chain_error(
self,
error: BaseException,
*,
run_id: UUID,
parent_run_id: UUID | None = None,
tags: list[str] | None = None,
**kwargs: Any,
) -> None:
"""Run when chain errors."""
self.span_manager.handle_error(error, run_id)

def on_agent_action(
self,
action: AgentAction, # type: ignore[type-arg]
*,
run_id: UUID,
parent_run_id: UUID | None = None,
tags: list[str] | None = None,
**kwargs: Any,
) -> None:
"""Run on agent action."""
# Agent actions are tracked as part of the chain span
# We can add attributes to the existing span if needed
span = self.span_manager.get_span(run_id)
if span:
tool = getattr(action, "tool", None) # type: ignore[arg-type]
if tool:
span.set_attribute("langchain.agent.action.tool", tool)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

I guess your intention is to use gen_ai.tool.name, gen_ai.tool.call.result and gen_ai.tool.call.arguments which are on execute_tool. Please use these gen-ai attributes. Also another PR updating invoke_agent with the above attributes would also be required.

tool_input = getattr(action, "tool_input", None) # type: ignore[arg-type]
if tool_input:
span.set_attribute(
"langchain.agent.action.tool_input", str(tool_input)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should this be gen_ai.tool.call.arguments ?

)

def on_agent_finish(
self,
finish: AgentFinish, # type: ignore[type-arg]
*,
run_id: UUID,
parent_run_id: UUID | None = None,
tags: list[str] | None = None,
**kwargs: Any,
) -> None:
"""Run on agent finish."""
# Agent finish is tracked as part of the chain span
span = self.span_manager.get_span(run_id)
if span:
return_values = getattr(finish, "return_values", None) # type: ignore[arg-type]
if return_values and "output" in return_values:
span.set_attribute(
"langchain.agent.finish.output",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should this be either [gen_ai.tool.call.result](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/gen-ai.md) or gen_ai.output.messages ?

str(return_values["output"]),
)
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
from opentelemetry.trace import Span, SpanKind, Tracer, set_span_in_context
from opentelemetry.trace.status import Status, StatusCode

__all__ = ["_SpanManager"]
__all__ = ["_SpanManager", "_OPERATION_INVOKE_AGENT"]

# Operation name constants
_OPERATION_INVOKE_AGENT = "invoke_agent"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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



@dataclass
Expand Down Expand Up @@ -91,6 +94,51 @@ def create_chat_span(

return span

def create_agent_span(
self,
run_id: UUID,
parent_run_id: Optional[UUID],
agent_name: Optional[str] = None,
) -> Span:
"""Create a span for agent invocation."""
# Use "unknown" as default if agent_name is not provided
effective_agent_name = agent_name or "unknown"
Comment thread
lmolkova marked this conversation as resolved.
Outdated
span_name = f"{_OPERATION_INVOKE_AGENT} {effective_agent_name}"
span = self._create_span(
run_id=run_id,
parent_run_id=parent_run_id,
span_name=span_name,
kind=SpanKind.CLIENT,
Comment thread
lmolkova marked this conversation as resolved.
Outdated
)
span.set_attribute(
GenAI.GEN_AI_OPERATION_NAME,
_OPERATION_INVOKE_AGENT,
)
span.set_attribute(GenAI.GEN_AI_AGENT_NAME, effective_agent_name)
Comment thread
lmolkova marked this conversation as resolved.
Outdated

return span
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Invoke agent span must have gen_ai.provider.name attribute with a name of genai system used under (e.g. openai, vertex_ai, etc). Is it possible to populate them here?

there are more recommended/opt-in attributes documented in https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-agent-spans.md#invoke-agent-span


def create_chain_span(
self,
run_id: UUID,
parent_run_id: Optional[UUID],
chain_name: str,
) -> Span:
"""Create a span for chain execution.

Chains are internal operations by default and don't have gen_ai.operation.name.
However, if the chain represents an agent (determined by metadata in the callback),
the operation name and agent name attributes will be set separately by the
callback handler to make it an agent span.
"""
span = self._create_span(
run_id=run_id,
parent_run_id=parent_run_id,
span_name=f"chain {chain_name}",
Comment thread
lmolkova marked this conversation as resolved.
kind=SpanKind.INTERNAL,
)
return span

def end_span(self, run_id: UUID) -> None:
state = self.spans[run_id]
for child_id in state.children:
Expand Down
Loading