Skip to content
Open
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,112 @@ 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(
Copy link
Member

Choose a reason for hiding this comment

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

let's make sure it creates internal span

Copy link
Author

Choose a reason for hiding this comment

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

Sound good - confirmed chain spans use SpanKind.INTERNAL here:

    def create_chain_span(
        self,
        run_id: UUID,
        parent_run_id: Optional[UUID],
        chain_name: str,
    ) -> Span:
        """Create a span for chain execution."""
        span = self._create_span(
            run_id=run_id,
            parent_run_id=parent_run_id,
            span_name=f"chain {chain_name}",
            kind=SpanKind.INTERNAL,
        )

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)
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)
)

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",
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"


@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"
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,
)
span.set_attribute(
GenAI.GEN_AI_OPERATION_NAME,
_OPERATION_INVOKE_AGENT,
)
span.set_attribute(GenAI.GEN_AI_AGENT_NAME, effective_agent_name)

return 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}",
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
Loading