Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions examples/langgraph-code-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# OpenAI API Key (required)
OPENAI_API_KEY=your_openai_api_key_here

# Traceroot API Key (required)
TRACEROOT_API_KEY=your_traceroot_api_key_here

# Traceroot Host URL
TRACEROOT_HOST_URL=http://localhost:8000
31 changes: 31 additions & 0 deletions examples/langgraph-code-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# LangGraph Code Agent

Multi-agent code generator using LangGraph, instrumented with [Traceroot](https://traceroot.ai).

Orchestrates 4 agents in a pipeline with automatic retry on failure:

```
Plan → Code → Execute → Summarize (→ retry if failed)
```

## Setup

```bash
cp .env.example .env # fill in your API keys
```

With `uv` (recommended):
```bash
uv run --with-requirements requirements.txt python server.py
```

## Usage

**Server mode** (default):
```bash
python server.py

curl -X POST http://localhost:9999/code \
-H "Content-Type: application/json" \
-d '{"query": "2 sum in python"}'
```
254 changes: 254 additions & 0 deletions examples/langgraph-code-agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
"""
Multi-agent code generator using LangGraph.

A pipeline of 4 agents:
Plan -> Code -> Execute -> Summarize

With automatic retry on execution failure (up to 2 retries).
"""

import logging
import os
import subprocess
import sys
import tempfile
from typing import Any, TypedDict

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------


class AgentState(TypedDict):
query: str
is_coding: bool
plan: str
code: str
execution_result: dict[str, Any]
response: str | None
retry_count: int
max_retries: int
last_summary: str


# ---------------------------------------------------------------------------
# Plan agent
# ---------------------------------------------------------------------------


class PlanResponse(BaseModel):
is_coding: bool = Field(description="Whether the query is coding-related")
plan: str | None = Field(default=None, description="Plan for coding tasks")
response: str | None = Field(default=None, description="Direct response for non-coding queries")


def plan_node(state: AgentState) -> dict:
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a planning agent. Determine if the query is coding-related. "
"If yes, set is_coding=true and provide a concise plan. "
"If not, set is_coding=false and answer directly. "
"If a previous summary is provided, learn from failures and improve.",
),
("human", "{query}"),
]
)

query = state["query"]
if state["last_summary"]:
query = f"{query}\n\nPrevious attempt summary:\n{state['last_summary']}"

chain = prompt | llm.with_structured_output(PlanResponse)
result = chain.invoke({"query": query})

return {
"is_coding": result.is_coding,
"plan": result.plan or "",
"response": result.response,
}


# ---------------------------------------------------------------------------
# Code agent
# ---------------------------------------------------------------------------


def code_node(state: AgentState) -> dict:
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a Python coding agent. Write clean, executable Python code. "
"Include necessary imports. Return ONLY the code, no explanations. "
"If historical context is provided, avoid repeating previous mistakes.",
),
("human", "{query}\n\nPlan: {plan}\n\nContext: {context}\n\nWrite the Python code."),
]
)

chain = prompt | llm
response = chain.invoke(
{
"query": state["query"],
"plan": state["plan"],
"context": state["last_summary"],
}
)

code = response.content.strip()
if code.startswith("```python"):
code = code[9:]
elif code.startswith("```"):
code = code[3:]
if code.endswith("```"):
code = code[:-3]

return {"code": code.strip()}


# ---------------------------------------------------------------------------
# Execution agent
# ---------------------------------------------------------------------------


def execute_node(state: AgentState) -> dict:
# WARNING: This example executes LLM-generated code without sandboxing.
# In production, use a sandboxed environment (Docker, gVisor, etc.)
# and enforce resource limits (CPU, memory, network).
try:
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(state["code"])
tmp = f.name

result = subprocess.run(
[sys.executable, tmp],
capture_output=True,
text=True,
timeout=30,
)
return {
"execution_result": {
"success": result.returncode == 0,
"stdout": result.stdout,
"stderr": result.stderr,
}
}
except subprocess.TimeoutExpired:
return {
"execution_result": {"success": False, "stdout": "", "stderr": "Timed out after 30s"}
}
except Exception as e:
return {"execution_result": {"success": False, "stdout": "", "stderr": str(e)}}
finally:
try:
os.unlink(tmp)
except Exception:
pass


# ---------------------------------------------------------------------------
# Summarize agent
# ---------------------------------------------------------------------------


def summarize_node(state: AgentState) -> dict:
if not state["is_coding"]:
return {"response": state["response"]}

llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Summarize the coding task result. Be concise and helpful. "
"Show the output or explain errors.",
),
(
"human",
"Query: {query}\nPlan: {plan}\nCode:\n```python\n{code}\n```\n"
"Success: {success}\nOutput: {stdout}\nError: {stderr}\n"
"Retry #{retry_count}",
),
]
)

er = state["execution_result"]
chain = prompt | llm
response = chain.invoke(
{
"query": state["query"],
"plan": state["plan"],
"code": state["code"],
"success": er.get("success", False),
"stdout": er.get("stdout", ""),
"stderr": er.get("stderr", ""),
"retry_count": state["retry_count"],
}
)

return {
"response": response.content,
"last_summary": response.content,
"retry_count": state["retry_count"] + (0 if er.get("success") else 1),
}


# ---------------------------------------------------------------------------
# Graph
# ---------------------------------------------------------------------------


def _should_code(state: AgentState) -> str:
return "code" if state["is_coding"] else "end"


def _should_retry(state: AgentState) -> str:
er = state["execution_result"]
if state["is_coding"] and not er.get("success") and state["retry_count"] < state["max_retries"]:
return "retry"
return "end"


def build_graph():
wf = StateGraph(AgentState)
wf.add_node("planning", plan_node)
wf.add_node("coding", code_node)
wf.add_node("execute", execute_node)
wf.add_node("summarize", summarize_node)

wf.set_entry_point("planning")
wf.add_conditional_edges("planning", _should_code, {"code": "coding", "end": "summarize"})
wf.add_edge("coding", "execute")
wf.add_edge("execute", "summarize")
wf.add_conditional_edges("summarize", _should_retry, {"retry": "planning", "end": END})

return wf.compile()


def process_query(query: str) -> str:
graph = build_graph()
result = graph.invoke(
{
"query": query,
"is_coding": False,
"plan": "",
"code": "",
"execution_result": {},
"response": None,
"retry_count": 0,
"max_retries": 2,
"last_summary": "",
}
)
return result["response"]
11 changes: 11 additions & 0 deletions examples/langgraph-code-agent/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
langchain==0.3.26
langgraph==0.4.9
langchain-openai==0.3.25
openai>=1.0.0
python-dotenv>=1.0.0
fastapi==0.115.12
uvicorn==0.34.3

# When running from this repo, use the local SDK:
-e ../../traceroot-py
# When traceroot is published to PyPI, replace with: traceroot
73 changes: 73 additions & 0 deletions examples/langgraph-code-agent/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Multi-agent code generator with Traceroot observability.

Usage:
cp env.example .env
pip install -r requirements.txt
python server.py

curl -X POST http://localhost:9999/code \
-H "Content-Type: application/json" \
-d '{"query": "Write a two sum solution"}'
"""

import logging

from dotenv import find_dotenv, load_dotenv

dotenv_path = find_dotenv()
if dotenv_path:
load_dotenv(dotenv_path)
else:
print("No .env file found (find_dotenv returned None).\nUsing process environment variables.")

# Initialize Traceroot BEFORE importing LangChain so instrumentation hooks in
import traceroot
from traceroot import Integration, observe, using_attributes

traceroot.initialize(integrations=[Integration.LANGCHAIN])

import uvicorn
from agent import process_query
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="LangGraph Code Agent")

DEFAULT_QUERY = (
"Given an array of integers nums and an integer target, return indices of "
"the two numbers such that they add up to target. nums = [2,7,11,15], target = 9."
)


class CodeRequest(BaseModel):
query: str = DEFAULT_QUERY


@app.post("/code")
@observe(type="span")
async def code_endpoint(request: CodeRequest) -> dict[str, str]:
try:
with using_attributes(
user_id="example-user",
session_id="code-agent-session",
):
result = process_query(request.query)
return {"status": "success", "response": result}
except Exception as e:
logger.error(f"Error: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e


if __name__ == "__main__":
PORT = 9999
print(f"Server running on http://localhost:{PORT}")
print(
f"Try: curl -X POST http://localhost:{PORT}/code "
f'-H "Content-Type: application/json" '
f'-d \'{{"query": "2 sum in python"}}\''
)
uvicorn.run(app, host="0.0.0.0", port=PORT)
8 changes: 8 additions & 0 deletions examples/openai-tool-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# OpenAI API Key (required)
OPENAI_API_KEY=your_openai_api_key_here

# Traceroot API Key (required)
TRACEROOT_API_KEY=your_traceroot_api_key_here

# Traceroot Host URL
TRACEROOT_HOST_URL=http://localhost:8000
Loading
Loading