HR-Assistant / docs /how_langgraph_works.md
owenkaplinsky
Clean initial commit for HuggingFace
363cda9
# 🧭 LangGraph Overview: Message Flow, Tool Execution & GPT-OSS Integration
LangGraph is a workflow engine for building agentic systems on top of LangChain.
It models the reasoning–action loop between models and tools using a transparent graph of nodes.
This document explains:
1. How LangGraph message flow works
2. How tools and tool calls are represented
3. How your custom GPT-OSS (OpenRouter) wrapper integrates via bind_tools()
---
## ⚙️ Core Concept
LangGraph passes a `state` object between nodes, usually defined like this:
```python
from typing import Annotated, List, TypedDict, Any
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[List[Any], add_messages]
```
The `messages` list holds the entire conversation: user inputs, model responses, and tool outputs.
Each node reads this list, adds new messages, and returns an updated state.
**LangGraph uses LangChain message objects:**
- 🧑‍💼 HumanMessage — from the user
- 🤖 AIMessage — from the model (may include tool_calls)
- 🧰 ToolMessage — from a tool
- ⚙️ SystemMessage — optional context
## 🧩 The Typical Agent Flow
```sql
HumanMessage ─► LLMNode ─► ToolNode ─► LLMNode ─► Final Answer
```
### 1️⃣ Human input
```python
input_state = {
"messages": [
HumanMessage(
content="Compute 8 * 12 using calculator tool"
)
]
}
```
LangGraph starts from `START` and passes this to the first node (the model).
### 2️⃣ Model response: tool call
Your model (`ChatOpenRouter` running GPT-OSS) examines the conversation and returns an `AIMessage`:
```json
{
"content": "",
"tool_calls": [
{
"id": "call_1",
"function": {"name": "calculator", "arguments": "{\"a\":8,\"b\":12,\"op\":\"mul\"}"}
}
],
"finish_reason": "tool_calls"
}
```
✅ LangGraph detects `.tool_calls` and automatically routes the next step to the ToolNode.
### 3️⃣ Tool execution
The ***ToolNode*** executes the requested tool and adds a `ToolMessage` to the state:
```python
ToolMessage(
content='96.0',
name='calculator',
tool_call_id='call_1'
)
```
### 4️⃣ LLM continuation
The LLM now sees:
```
[
HumanMessage(...),
AIMessage(..., tool_calls=[...]),
ToolMessage(name="calculator", content="96.0")
]
```
It generates a final summary message:
```text
"The result of 8 × 12 is **96**."
```
Since this new message has no further `tool_calls`, LangGraph ends the workflow.
## 🧠 Internal Message Logic
| Step | Message Type | Produced By | Purpose |
| ---- | -------------- | ----------- | ------------------- |
| 1 | `HumanMessage` | user | Input |
| 2 | `AIMessage` | model | Requests tool |
| 3 | `ToolMessage` | ToolNode | Returns tool output |
| 4 | `AIMessage` | model | Final answer |
LangGraph uses conditional edges to decide whether to continue looping:
```python
workflow.add_conditional_edges(
"agent",
lambda state: "tools" if state["messages"][-1].tool_calls else END
)
```
This keeps running until no more tool calls are made.
## ⚙️ Example Graph Definition
```python
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from src.core.llm_providers.openrouter_llm import ChatOpenRouter
@tool
def calculator(a: float, b: float, op: str) -> float:
"""Perform a basic arithmetic operation."""
if op == "add": return a + b
if op == "sub": return a - b
if op == "mul": return a * b
if op == "div": return a / b
tools = [calculator]
# Initialize GPT-OSS LLM
llm = ChatOpenRouter(model_name="openai/gpt-oss-120b", temperature=0.0)
# ✅ Bind tools — this injects tool schemas into the model's context
llm_with_tools = llm.bind_tools(tools)
def call_model(state: State) -> State:
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
workflow = StateGraph(State)
workflow.add_node("agent", call_model)
workflow.add_node("tools", ToolNode(tools))
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent", lambda s: "tools" if s["messages"][-1].tool_calls else END
)
workflow.add_edge("tools", "agent")
agent = workflow.compile()
input_state = {"messages": [HumanMessage(content="Compute 8 * 12 using calculator tool")]}
print(agent.invoke(input_state))
```
***>>> Check `notebooks/playground.ipynb` to see it in action!***
```mermaid
graph TD;
__start__(<p>__start__</p>)
agent(agent)
tools(tools)
__end__(<p>__end__</p>)
__start__ --> agent;
agent -->|tool_calls| tools;
tools --> agent;
agent -->|no tool_calls| __end__;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
```
## 🧩 How bind_tools() Works Internally
`bind_tools()` is the bridge between LangGraph and your LLM.
When you call:
```python
llm_with_tools = llm.bind_tools(tools)
```
LangChain:
1. Extracts each tool's name, description, and argument schema.
2. Converts them into an OpenAI function-calling schema JSON block (like `tools=[{"type":"function","function":{"name":...}}]`).
3. Attaches that schema to the model's context before each inference call.
So GPT-OSS sees an augmented prompt like this:
> “You have access to the following tools:
> calculator(a: float, b: float, op: str) — Perform a basic arithmetic operation.”
During inference, the model can reason (internally) about which tool to call and output structured JSON like:
```json
{
"tool_calls": [{
"function": {
"name": "calculator",
"arguments": "{\"a\":8,\"b\":12,\"op\":\"mul\"}"
}
}]
}
```
---
## 🧱 GPT-OSS Integration via ChatOpenRouter
To connect GPT-OSS via OpenRouter, use your wrapper:
```python
# src/core/llm_providers/openrouter_llm.py
from langchain_openai import ChatOpenAI
from pydantic_settings import BaseSettings, SettingsConfigDict
class OpenRouterSettings(BaseSettings):
OPENROUTER_API_KEY: str
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
class ChatOpenRouter(ChatOpenAI):
"""OpenRouter wrapper for GPT-OSS and other open models."""
def __init__(
self,
model_name: str = "openai/gpt-oss-120b",
base_url: str = "https://openrouter.ai/api/v1",
temperature: float = 0.2,
**kwargs,
):
settings = OpenRouterSettings()
super().__init__(
model_name=model_name,
openai_api_base=base_url,
openai_api_key=settings.OPENROUTER_API_KEY,
temperature=temperature,
**kwargs,
)
```
Then simply:
```python
llm = ChatOpenRouter()
llm_with_tools = llm.bind_tools(tools)
```
✅ This passes your validated API key to the OpenRouter endpoint.
✅ bind_tools() adds your tool schemas to the model input so GPT-OSS knows which functions exist.
✅ LangGraph handles execution and looping automatically.
---
## 🔁 Full Flow Recap
| Step | Component | What Happens |
| ---- | ---------------------- | ---------------------------------------------------------- |
| 1 | `ChatOpenRouter` | Sends messages to GPT-OSS via OpenRouter API |
| 2 | `bind_tools()` | Injects tool schema into model context |
| 3 | `AIMessage.tool_calls` | Model outputs structured tool call |
| 4 | `ToolNode` | Executes the requested function |
| 5 | `ToolMessage` | Returns tool result to model |
| 6 | `AIMessage` | Model produces natural-language final answer |
| ✅ | LangGraph | Orchestrates routing and maintains full conversation state |
## ✅ TL;DR
- LangGraph represents an agent loop as a message-passing graph.
- Messages include `HumanMessage`, `AIMessage`, and `ToolMessage`.
- `bind_tools()` injects your tool schemas into the LLM's context so it can call them.
- The ToolNode executes the functions and feeds results back into the loop.
- Your `ChatOpenRouter` wrapper lets GPT-OSS models participate in this system seamlessly.