Skip to content

Commit 1c291ed

Browse files
danielmillerpclaude
andcommitted
feat(cli): add Temporal + LangGraph agent template
Adds a `temporal-langgraph` init template (and registers it in the Temporal submenu of `agentex init`), filling the gap left by the existing `temporal-openai-agents` and `temporal-pydantic-ai` templates — LangGraph previously only had `default-langgraph` and `sync-langgraph`. Pattern: Temporal as the runtime, LangGraph as the agent framework. The workflow walks the LangGraph agent loop but dispatches each node as a durable Temporal activity (one per LLM call, one per tool call) — the "Temporal in the agent loop" pattern, implemented with the released SDK and stock LangGraph (no unreleased plugin required). Showcases: - nodes-as-activities (call_model_activity / execute_tool_activity) - human-in-the-loop tool gating via a Temporal signal (provide_approval) - live introspection queries (status, pending approval, graph mermaid/ascii, graph state) - token streaming to Agentex, multi-turn memory on the workflow, tracing Also adds tests/lib/cli/test_init_templates.py, which renders every init template and asserts the output is valid, substituted Python (catching .j2 templating regressions), with focused coverage of the new template. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a66d239 commit 1c291ed

19 files changed

Lines changed: 1722 additions & 0 deletions

src/agentex/lib/cli/commands/init.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class TemplateType(str, Enum):
2525
TEMPORAL = "temporal"
2626
TEMPORAL_OPENAI_AGENTS = "temporal-openai-agents"
2727
TEMPORAL_PYDANTIC_AI = "temporal-pydantic-ai"
28+
TEMPORAL_LANGGRAPH = "temporal-langgraph"
2829
DEFAULT = "default"
2930
DEFAULT_LANGGRAPH = "default-langgraph"
3031
DEFAULT_PYDANTIC_AI = "default-pydantic-ai"
@@ -64,6 +65,7 @@ def create_project_structure(
6465
TemplateType.TEMPORAL: ["acp.py", "workflow.py", "run_worker.py"],
6566
TemplateType.TEMPORAL_OPENAI_AGENTS: ["acp.py", "workflow.py", "run_worker.py", "activities.py"],
6667
TemplateType.TEMPORAL_PYDANTIC_AI: ["acp.py", "workflow.py", "run_worker.py", "agent.py", "tools.py"],
68+
TemplateType.TEMPORAL_LANGGRAPH: ["acp.py", "workflow.py", "run_worker.py", "activities.py", "graph.py", "tools.py"],
6769
TemplateType.DEFAULT: ["acp.py"],
6870
TemplateType.DEFAULT_LANGGRAPH: ["acp.py", "graph.py", "tools.py"],
6971
TemplateType.DEFAULT_PYDANTIC_AI: ["acp.py", "agent.py", "tools.py"],
@@ -195,6 +197,7 @@ def validate_agent_name(text: str) -> bool | str:
195197
{"name": "Basic Temporal", "value": TemplateType.TEMPORAL},
196198
{"name": "Temporal + OpenAI Agents SDK (Recommended)", "value": TemplateType.TEMPORAL_OPENAI_AGENTS},
197199
{"name": "Temporal + Pydantic AI", "value": TemplateType.TEMPORAL_PYDANTIC_AI},
200+
{"name": "Temporal + LangGraph", "value": TemplateType.TEMPORAL_LANGGRAPH},
198201
],
199202
).ask()
200203
if not template_type:
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# {{ agent_name }} - Environment Variables
2+
# Copy this file to .env and fill in the values
3+
4+
# API key for your LLM provider
5+
LITELLM_API_KEY=
6+
7+
# LLM base URL (optional - override to use a different provider)
8+
# OPENAI_BASE_URL=
9+
10+
# SGP Configuration (optional - for tracing)
11+
# SGP_API_KEY=
12+
# SGP_ACCOUNT_ID=
13+
# SGP_CLIENT_BASE_URL=
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
nodejs \
19+
npm \
20+
&& apt-get clean \
21+
&& rm -rf /var/lib/apt/lists/**
22+
23+
# Install tctl (Temporal CLI)
24+
RUN curl -L https://github.com/temporalio/tctl/releases/download/v1.18.1/tctl_1.18.1_linux_arm64.tar.gz -o /tmp/tctl.tar.gz && \
25+
tar -xzf /tmp/tctl.tar.gz -C /usr/local/bin && \
26+
chmod +x /usr/local/bin/tctl && \
27+
rm /tmp/tctl.tar.gz
28+
29+
ENV UV_COMPILE_BYTECODE=1
30+
ENV UV_LINK_MODE=copy
31+
ENV UV_HTTP_TIMEOUT=1000
32+
33+
WORKDIR /app/{{ project_path_from_build_root }}
34+
35+
# Copy dependency files for layer caching
36+
COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./
37+
38+
# Install dependencies (without project itself, for layer caching)
39+
RUN --mount=type=cache,target=/root/.cache/uv \
40+
uv sync --locked --no-install-project --no-dev
41+
42+
# Copy the project code
43+
COPY {{ project_path_from_build_root }}/project ./project
44+
45+
# Install the project
46+
RUN --mount=type=cache,target=/root/.cache/uv \
47+
uv sync --locked --no-dev
48+
49+
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"
50+
51+
# Run the ACP server using uvicorn
52+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
53+
54+
# When we deploy the worker, we will replace the CMD with the following
55+
# CMD ["python", "-m", "run_worker"]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
node \
19+
npm \
20+
&& apt-get clean \
21+
&& rm -rf /var/lib/apt/lists/*
22+
23+
# Install tctl (Temporal CLI)
24+
RUN curl -L https://github.com/temporalio/tctl/releases/download/v1.18.1/tctl_1.18.1_linux_arm64.tar.gz -o /tmp/tctl.tar.gz && \
25+
tar -xzf /tmp/tctl.tar.gz -C /usr/local/bin && \
26+
chmod +x /usr/local/bin/tctl && \
27+
rm /tmp/tctl.tar.gz
28+
29+
RUN uv pip install --system --upgrade pip setuptools wheel
30+
31+
ENV UV_HTTP_TIMEOUT=1000
32+
33+
# Copy just the requirements file to optimize caching
34+
COPY {{ project_path_from_build_root }}/requirements.txt /app/{{ project_path_from_build_root }}/requirements.txt
35+
36+
WORKDIR /app/{{ project_path_from_build_root }}
37+
38+
# Install the required Python packages
39+
RUN uv pip install --system -r requirements.txt
40+
41+
# Copy the project code
42+
COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project
43+
44+
# Run the ACP server using uvicorn
45+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
46+
47+
# When we deploy the worker, we will replace the CMD with the following
48+
# CMD ["python", "-m", "run_worker"]
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# {{ agent_name }} — AgentEx Temporal + LangGraph
2+
3+
A starter template for building AI agents with AgentEx, [LangGraph](https://langchain-ai.github.io/langgraph/),
4+
and Temporal — where **Temporal is the runtime and LangGraph is the agent framework**.
5+
6+
The agent loop that LangGraph describes is executed by Temporal: each
7+
LangGraph node (the model call, each tool call) runs as its own **durable
8+
Temporal activity**. You get per-node durability, automatic retries, and full
9+
visibility in the Temporal UI — without LangGraph's own runtime or an external
10+
checkpoint database.
11+
12+
This is the *Temporal in the agent loop* pattern (Level 2), implemented with
13+
the released SDK and stock LangGraph — no plugin required.
14+
15+
## What's in the box
16+
17+
- **Durable execution** via a Temporal workflow.
18+
- **Nodes as activities** — one activity per LLM call, one per tool call
19+
(`call_model_activity`, `execute_tool_activity`).
20+
- **Human-in-the-loop** — sensitive tools pause the loop on a Temporal signal
21+
until a human approves or rejects (`provide_approval`).
22+
- **Live introspection via Temporal queries** — `get_status`,
23+
`get_pending_approval`, `get_graph_state`, and `get_graph_mermaid` /
24+
`get_graph_ascii` to render the agent graph while it runs.
25+
- **Streaming responses** — tokens delta-stream to AgentEx via Redis from
26+
inside the model activity.
27+
- **Multi-turn memory** — kept on the workflow instance, durable for free.
28+
- **Tracing/observability** — a per-turn span shipped to SGP/AgentEx.
29+
30+
## The agent graph
31+
32+
```
33+
START --> agent --> (tool calls?) --> tools --> agent
34+
--> (no tool calls?) --> END
35+
```
36+
37+
`project/graph.py` defines this graph with LangGraph. The workflow walks the
38+
same topology, dispatching `agent` and `tools` as activities. Query
39+
`get_graph_mermaid` at runtime to see it rendered.
40+
41+
## Project structure
42+
43+
```
44+
{{ project_name }}/
45+
├── project/
46+
│ ├── __init__.py
47+
│ ├── acp.py # Thin async ACP server (Temporal-wired)
48+
│ ├── workflow.py # Temporal runtime: agent loop, HIL, queries, memory
49+
│ ├── activities.py # LangGraph nodes as durable activities (model + tools)
50+
│ ├── graph.py # LangGraph graph definition + model/message helpers
51+
│ └── run_worker.py # Temporal worker setup
52+
├── Dockerfile
53+
├── manifest.yaml
54+
├── environments.yaml
55+
├── dev.ipynb
56+
{% if use_uv %}└── pyproject.toml{% else %}└── requirements.txt{% endif %}
57+
```
58+
59+
## Running the agent
60+
61+
```bash
62+
{% if use_uv %}agentex uv sync
63+
source .venv/bin/activate{% else %}pip install -r requirements.txt{% endif %}
64+
65+
# Start the agent (ACP server + Temporal worker)
66+
agentex agents run --manifest manifest.yaml
67+
```
68+
69+
The agent starts on port 8000. Open the Temporal UI at http://localhost:8080 to
70+
watch workflows and activities execute. Use `dev.ipynb` to create a task and
71+
send messages.
72+
73+
## Human-in-the-loop
74+
75+
Tools listed in `TOOLS_REQUIRING_APPROVAL` (in `project/tools.py`) pause before
76+
they run. The workflow surfaces the pending call (queryable via
77+
`get_pending_approval`) and waits — durably, for as long as it takes — for a
78+
`provide_approval` signal:
79+
80+
```python
81+
await client.agents.send_signal( # or via the Temporal client
82+
workflow_id=task.id,
83+
signal="provide_approval",
84+
args=[{"approved": True, "approver": "daniel", "reason": "looks good"}],
85+
)
86+
```
87+
88+
If rejected, the rejection is fed back to the model so it can adjust.
89+
90+
## Adding tools
91+
92+
1. Define a `@tool` function in `project/tools.py` and add it to `TOOLS`.
93+
2. (Optional) add its name to `TOOLS_REQUIRING_APPROVAL` to gate it behind
94+
human approval.
95+
96+
That's it — the model is bound with `TOOLS`, and `execute_tool_activity` looks
97+
up tools by name at runtime, so no other wiring is needed.
98+
99+
## Configuration
100+
101+
Tune the model in `project/graph.py` (`MODEL_NAME`) and the system prompt
102+
(`SYSTEM_PROMPT`). Activity timeouts and retry policy live at the top of
103+
`project/workflow.py`.
104+
105+
## Environment variables
106+
107+
Create a `.env` file (see `.env.example`):
108+
109+
```bash
110+
LITELLM_API_KEY=your-litellm-key # copied to OPENAI_API_KEY automatically
111+
# OPENAI_BASE_URL= # optional: point at a different provider
112+
# SGP_API_KEY= # optional: tracing
113+
# SGP_ACCOUNT_ID= # optional: tracing
114+
# SGP_CLIENT_BASE_URL= # optional: tracing
115+
```
116+
117+
## Why Temporal as the runtime?
118+
119+
LangGraph open source isn't a complete agent runtime: productionizing it means
120+
solving state management, checkpoint databases, session recovery after crashes,
121+
timeouts, and health checks. Temporal provides those out of the box. Here it
122+
runs your LangGraph agent durably — if the worker crashes mid-turn, another
123+
worker resumes from the recorded activity results, no LLM re-calls, no lost
124+
state.
125+
126+
Happy building with Temporal + LangGraph!

0 commit comments

Comments
 (0)