Skip to content

Commit fa01cf1

Browse files
danielmillerpclaude
andcommitted
Add LangGraph examples and CLI templates (sync + async)
- Add sync LangGraph example at examples/tutorials/00_sync/030_langgraph - Add async LangGraph example at examples/tutorials/10_async/00_base/100_langgraph - Add sync-langgraph and default-langgraph CLI templates with graph.py, tools.py, and LangGraph deps - Update init.py with new TemplateType entries and sub-menu options for LangGraph Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 66f9ee5 commit fa01cf1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2928
-3
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Environments
24+
.env**
25+
.venv
26+
env/
27+
venv/
28+
ENV/
29+
env.bak/
30+
venv.bak/
31+
32+
# IDE
33+
.idea/
34+
.vscode/
35+
*.swp
36+
*.swo
37+
38+
# Git
39+
.git
40+
.gitignore
41+
42+
# Misc
43+
.DS_Store
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# syntax=docker/dockerfile:1.3
2+
FROM python:3.12-slim
3+
COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/
4+
5+
# Install system dependencies
6+
RUN apt-get update && apt-get install -y \
7+
htop \
8+
vim \
9+
curl \
10+
tar \
11+
python3-dev \
12+
postgresql-client \
13+
build-essential \
14+
libpq-dev \
15+
gcc \
16+
cmake \
17+
netcat-openbsd \
18+
&& apt-get clean \
19+
&& rm -rf /var/lib/apt/lists/*
20+
21+
RUN uv pip install --system --upgrade pip setuptools wheel
22+
23+
ENV UV_HTTP_TIMEOUT=1000
24+
25+
# Copy pyproject.toml and README.md to install dependencies
26+
COPY 00_sync/030_langgraph/pyproject.toml /app/030_langgraph/pyproject.toml
27+
COPY 00_sync/030_langgraph/README.md /app/030_langgraph/README.md
28+
29+
WORKDIR /app/030_langgraph
30+
31+
# Copy the project code
32+
COPY 00_sync/030_langgraph/project /app/030_langgraph/project
33+
34+
# Copy the test files
35+
COPY 00_sync/030_langgraph/tests /app/030_langgraph/tests
36+
37+
# Copy shared test utilities
38+
COPY test_utils /app/test_utils
39+
40+
# Install the required Python packages with dev dependencies
41+
RUN uv pip install --system .[dev]
42+
43+
# Set environment variables
44+
ENV PYTHONPATH=/app
45+
46+
# Set test environment variables
47+
ENV AGENT_NAME=s030-langgraph
48+
49+
# Run the agent using uvicorn
50+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Tutorial 030: Sync LangGraph Agent
2+
3+
This tutorial demonstrates how to build a **synchronous** LangGraph agent on AgentEx with:
4+
- Tool calling (ReAct pattern)
5+
- Streaming token output
6+
- Multi-turn conversation memory via AgentEx checkpointer
7+
- Tracing integration
8+
9+
## Graph Structure
10+
11+
![Graph](graph.png)
12+
13+
## Key Concepts
14+
15+
### Sync ACP
16+
The sync ACP model uses HTTP request/response for communication. The `@acp.on_message_send` handler receives a message and yields streaming events back to the client.
17+
18+
### LangGraph Integration
19+
- **StateGraph**: Defines the agent's state machine with `AgentState` (message history)
20+
- **ToolNode**: Automatically executes tool calls from the LLM
21+
- **tools_condition**: Routes between tool execution and final response
22+
- **Checkpointer**: Uses AgentEx's HTTP checkpointer for cross-request memory
23+
24+
### Streaming
25+
The agent streams tokens as they're generated using `convert_langgraph_to_agentex_events()`, which converts LangGraph's stream events into AgentEx `TaskMessageUpdate` events.
26+
27+
## Files
28+
29+
| File | Description |
30+
|------|-------------|
31+
| `project/acp.py` | ACP server and message handler |
32+
| `project/graph.py` | LangGraph state graph definition |
33+
| `project/tools.py` | Tool definitions (weather example) |
34+
| `tests/test_agent.py` | Integration tests |
35+
| `manifest.yaml` | Agent configuration |
36+
37+
## Running Locally
38+
39+
```bash
40+
# From this directory
41+
agentex agents run
42+
```
43+
44+
## Running Tests
45+
46+
```bash
47+
pytest tests/test_agent.py -v
48+
```
16 KB
Loading
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
build:
2+
context:
3+
root: ../../
4+
include_paths:
5+
- 00_sync/030_langgraph
6+
- test_utils
7+
dockerfile: 00_sync/030_langgraph/Dockerfile
8+
dockerignore: 00_sync/030_langgraph/.dockerignore
9+
10+
local_development:
11+
agent:
12+
port: 8000
13+
host_address: host.docker.internal
14+
paths:
15+
acp: project/acp.py
16+
17+
agent:
18+
acp_type: sync
19+
name: s030-langgraph
20+
description: A sync LangGraph agent with tool calling and streaming
21+
22+
temporal:
23+
enabled: false
24+
25+
credentials:
26+
- env_var_name: OPENAI_API_KEY
27+
secret_name: openai-api-key
28+
secret_key: api-key
29+
- env_var_name: REDIS_URL
30+
secret_name: redis-url-secret
31+
secret_key: url
32+
- env_var_name: SGP_API_KEY
33+
secret_name: sgp-api-key
34+
secret_key: api-key
35+
- env_var_name: SGP_ACCOUNT_ID
36+
secret_name: sgp-account-id
37+
secret_key: account-id
38+
- env_var_name: SGP_CLIENT_BASE_URL
39+
secret_name: sgp-client-base-url
40+
secret_key: url
41+
42+
deployment:
43+
image:
44+
repository: ""
45+
tag: "latest"
46+
47+
global:
48+
agent:
49+
name: "s030-langgraph"
50+
description: "A sync LangGraph agent with tool calling and streaming"
51+
replicaCount: 1
52+
resources:
53+
requests:
54+
cpu: "500m"
55+
memory: "1Gi"
56+
limits:
57+
cpu: "1000m"
58+
memory: "2Gi"

examples/tutorials/00_sync/030_langgraph/project/__init__.py

Whitespace-only changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
ACP (Agent Communication Protocol) handler for Agentex.
3+
4+
This is the API layer — it manages the graph lifecycle and streams
5+
tokens and tool calls from the LangGraph graph to the Agentex frontend.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import os
11+
from typing import AsyncGenerator
12+
13+
from dotenv import load_dotenv
14+
15+
load_dotenv()
16+
17+
import agentex.lib.adk as adk
18+
from project.graph import create_graph
19+
from agentex.lib.adk import create_langgraph_tracing_handler, convert_langgraph_to_agentex_events
20+
from agentex.lib.types.acp import SendMessageParams
21+
from agentex.lib.types.tracing import SGPTracingProcessorConfig
22+
from agentex.lib.utils.logging import make_logger
23+
from agentex.lib.sdk.fastacp.fastacp import FastACP
24+
from agentex.types.task_message_delta import TextDelta
25+
from agentex.types.task_message_update import TaskMessageUpdate
26+
from agentex.types.task_message_content import TaskMessageContent
27+
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
28+
29+
logger = make_logger(__name__)
30+
31+
# Register the Agentex tracing processor so spans are shipped to the backend
32+
add_tracing_processor_config(
33+
SGPTracingProcessorConfig(
34+
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
35+
sgp_account_id=os.environ.get("SGP_ACCOUNT_ID", ""),
36+
sgp_base_url=os.environ.get("SGP_CLIENT_BASE_URL", ""),
37+
))
38+
# Create ACP server
39+
acp = FastACP.create(acp_type="sync")
40+
41+
# Compiled graph (lazy-initialized on first request)
42+
_graph = None
43+
44+
45+
async def get_graph():
46+
"""Get or create the compiled graph instance."""
47+
global _graph
48+
if _graph is None:
49+
_graph = await create_graph()
50+
return _graph
51+
52+
53+
@acp.on_message_send
54+
async def handle_message_send(
55+
params: SendMessageParams,
56+
) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]:
57+
"""Handle incoming messages from Agentex, streaming tokens and tool calls."""
58+
graph = await get_graph()
59+
60+
thread_id = params.task.id
61+
user_message = params.content.content
62+
63+
logger.info(f"Processing message for thread {thread_id}")
64+
65+
async with adk.tracing.span(
66+
trace_id=thread_id,
67+
name="message",
68+
input={"message": user_message},
69+
data={"__span_type__": "AGENT_WORKFLOW"},
70+
) as turn_span:
71+
callback = create_langgraph_tracing_handler(
72+
trace_id=thread_id,
73+
parent_span_id=turn_span.id if turn_span else None,
74+
)
75+
76+
stream = graph.astream(
77+
{"messages": [{"role": "user", "content": user_message}]},
78+
config={
79+
"configurable": {"thread_id": thread_id},
80+
"callbacks": [callback],
81+
},
82+
stream_mode=["messages", "updates"],
83+
)
84+
85+
final_text = ""
86+
async for event in convert_langgraph_to_agentex_events(stream):
87+
# Accumulate text deltas for span output
88+
delta = getattr(event, "delta", None)
89+
if isinstance(delta, TextDelta) and delta.text_delta:
90+
final_text += delta.text_delta
91+
yield event
92+
93+
if turn_span:
94+
turn_span.output = {"final_output": final_text}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""
2+
LangGraph graph definition.
3+
4+
Defines the state, nodes, edges, and compiles the graph.
5+
The compiled graph is the boundary between this module and the API layer.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from typing import Any, Annotated
11+
from datetime import datetime
12+
from typing_extensions import TypedDict
13+
14+
from langgraph.graph import START, StateGraph
15+
from langchain_openai import ChatOpenAI
16+
from langgraph.prebuilt import ToolNode, tools_condition
17+
from langchain_core.messages import SystemMessage
18+
from langgraph.graph.message import add_messages
19+
20+
from project.tools import TOOLS
21+
from agentex.lib.adk import create_checkpointer
22+
23+
MODEL_NAME = "gpt-5"
24+
SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools.
25+
26+
Current date and time: {timestamp}
27+
28+
Guidelines:
29+
- Be concise and helpful
30+
- Use tools when they would help answer the user's question
31+
- If you're unsure, ask clarifying questions
32+
- Always provide accurate information
33+
"""
34+
35+
36+
class AgentState(TypedDict):
37+
"""State schema for the agent graph."""
38+
messages: Annotated[list[Any], add_messages]
39+
40+
41+
async def create_graph():
42+
"""Create and compile the agent graph with checkpointer.
43+
44+
Returns:
45+
A compiled LangGraph StateGraph ready for invocation.
46+
"""
47+
llm = ChatOpenAI(
48+
model=MODEL_NAME,
49+
reasoning={"effort": "high", "summary": "auto"},
50+
)
51+
llm_with_tools = llm.bind_tools(TOOLS)
52+
53+
checkpointer = await create_checkpointer()
54+
55+
def agent_node(state: AgentState) -> dict[str, Any]:
56+
"""Process the current state and generate a response."""
57+
messages = state["messages"]
58+
if not messages or not isinstance(messages[0], SystemMessage):
59+
system_content = SYSTEM_PROMPT.format(
60+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
61+
)
62+
messages = [SystemMessage(content=system_content)] + messages
63+
response = llm_with_tools.invoke(messages)
64+
return {"messages": [response]}
65+
66+
builder = StateGraph(AgentState)
67+
builder.add_node("agent", agent_node)
68+
builder.add_node("tools", ToolNode(tools=TOOLS))
69+
builder.add_edge(START, "agent")
70+
builder.add_conditional_edges("agent", tools_condition, "tools")
71+
builder.add_edge("tools", "agent")
72+
73+
return builder.compile(checkpointer=checkpointer)

0 commit comments

Comments
 (0)