> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pandaprobe.com/llms.txt
> Use this file to discover all available pages before exploring further.

# LangGraph

> Trace LangGraph agent executions with automatic span capture

### Installation

<Tabs>
  <Tab title="pip">
    ```bash theme={null}
    pip install "pandaprobe[langgraph]"
    ```
  </Tab>

  <Tab title="uv">
    ```bash theme={null}
    uv add "pandaprobe[langgraph]"
    ```
  </Tab>
</Tabs>

### Setup

```python theme={null}
from pandaprobe.integrations.langgraph import LangGraphCallbackHandler

handler = LangGraphCallbackHandler(
    session_id="conversation-123",
    user_id="user-abc",
    tags=["production"],
)
```

<Tip>
  We recommend using UUIDs for `session_id` and `user_id` so traces can be grouped reliably across runs.
</Tip>

### Usage

Pass the handler via LangChain's `config["callbacks"]`:

```python theme={null}
from langgraph.graph import StateGraph

# ... define your graph ...

result = graph.invoke(
    {"messages": [{"role": "user", "content": "Hello!"}]},
    config={"callbacks": [handler]},
)
```

<Warning>
  Unlike other integrations, LangGraph does not use the `instrument()` pattern. You must pass the handler in `config` for each invocation.
</Warning>

### What gets traced

| LangChain Callback                                    | Span Kind                          | Description                               |
| ----------------------------------------------------- | ---------------------------------- | ----------------------------------------- |
| `on_chain_start` / `on_chain_end`                     | `CHAIN` (root) or `AGENT` (nested) | Root chain creates the trace boundary     |
| `on_llm_start` / `on_chat_model_start` / `on_llm_end` | `LLM`                              | Model, parameters, token usage, reasoning |
| `on_tool_start` / `on_tool_end`                       | `TOOL`                             | Tool name, input, output                  |
| `on_retriever_start` / `on_retriever_end`             | `RETRIEVER`                        | Retrieval queries and results             |

### Token usage

Token usage is extracted from LangChain's `usage_metadata` (primary) or legacy `llm_output.token_usage` (fallback). The mapping is: `input_tokens` → `prompt_tokens`, `output_tokens` → `completion_tokens`. Reasoning tokens are subtracted from `output_tokens` when present.

### Example with tools

This example builds a ReAct agent using LangGraph with two tools. We trace the agent via `LangGraphCallbackHandler`:

```python theme={null}
from typing import Annotated

from langchain_core.messages import SystemMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from typing_extensions import TypedDict

import pandaprobe
from pandaprobe.integrations.langgraph import LangGraphCallbackHandler


@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    weather_data = {
        "london": "Cloudy, 15°C, 70% humidity",
        "tokyo": "Sunny, 28°C, 45% humidity",
        "new york": "Partly cloudy, 22°C, 55% humidity",
        "paris": "Rainy, 12°C, 85% humidity",
    }
    return weather_data.get(city.lower(), f"Weather data not available for {city}")


@tool
def get_population(city: str) -> str:
    """Get the approximate population of a city."""
    populations = {
        "london": "8.8 million",
        "tokyo": "13.9 million",
        "new york": "8.3 million",
        "paris": "2.2 million",
    }
    return populations.get(city.lower(), f"Population data not available for {city}")


tools = [get_weather, get_population]
llm = ChatOpenAI(model="gpt-5.4-nano").bind_tools(tools)


class AgentState(TypedDict):
    messages: Annotated[list, add_messages]


def agent_node(state: AgentState) -> dict:
    system = SystemMessage(content="You are a helpful assistant with access to weather and population tools.")
    messages = [system, *state["messages"]]
    return {"messages": [llm.invoke(messages)]}


def should_continue(state: AgentState) -> str:
    last_message = state["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"
    return END


graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("tools", ToolNode(tools))
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")
app = graph.compile()

handler = LangGraphCallbackHandler(tags=["tool-agent", "example"])

result = app.invoke(
    {"messages": [("user", "What's the weather like in London and what's its population?")]},
    config={"callbacks": [handler]},
)

final_message = result["messages"][-1]
print(f"Agent: {final_message.content}")

pandaprobe.flush()
pandaprobe.shutdown()
```

This produces a trace with: `CHAIN` (root) → `LLM` (model call) → `TOOL` (`get_weather`) → `TOOL` (`get_population`) → `LLM` (final response).
