Skip to content

Commit bbc9e02

Browse files
feat(cli): add Temporal + LangGraph agent template and example (#383)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6669012 commit bbc9e02

36 files changed

Lines changed: 2642 additions & 0 deletions
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+
# at130-langgraph - 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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 10_async/10_temporal/130_langgraph/pyproject.toml /app/130_langgraph/pyproject.toml
26+
COPY 10_async/10_temporal/130_langgraph/README.md /app/130_langgraph/README.md
27+
28+
WORKDIR /app/130_langgraph
29+
30+
COPY 10_async/10_temporal/130_langgraph/project /app/130_langgraph/project
31+
COPY 10_async/10_temporal/130_langgraph/tests /app/130_langgraph/tests
32+
COPY test_utils /app/test_utils
33+
34+
RUN uv pip install --system .[dev]
35+
36+
ENV PYTHONPATH=/app
37+
38+
ENV AGENT_NAME=at130-langgraph
39+
40+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
41+
42+
# When we deploy the worker, we will replace the CMD with the following
43+
# CMD ["python", "-m", "run_worker"]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# at130-langgraph — AgentEx Temporal + LangGraph
2+
3+
A minimal Temporal-backed [LangGraph](https://langchain-ai.github.io/langgraph/)
4+
agent. It uses the official [`temporalio.contrib.langgraph`](https://docs.temporal.io/develop/python/integrations/langgraph)
5+
plugin so each LangGraph node runs as a durable **Temporal activity** (the LLM
6+
`agent` node) or inline in the **workflow** (the `tools` node) — set per node
7+
with `execute_in`. *Temporal is the runtime; LangGraph is the agent framework.*
8+
9+
> The Temporal LangGraph plugin is currently **experimental**.
10+
11+
## The graph
12+
13+
```
14+
START → agent → (tool calls?) → tools → agent
15+
→ (no tool calls?) → END
16+
```
17+
18+
- `agent` (`execute_in="activity"`): the LLM call — a retried, observable Temporal activity.
19+
- `tools` (`execute_in="workflow"`): runs the tool calls inline in the workflow.
20+
21+
The router and tools are `async` so LangGraph awaits them directly (a sync
22+
callable is offloaded via `run_in_executor`, which Temporal workflows forbid).
23+
24+
## Project structure
25+
26+
```
27+
130_langgraph/
28+
├── project/
29+
│ ├── acp.py # Thin async ACP server; registers the LangGraphPlugin
30+
│ ├── workflow.py # Runs the graph each turn; keeps multi-turn memory
31+
│ ├── graph.py # LangGraph graph; nodes tagged execute_in activity/workflow
32+
│ └── tools.py # Async tool(s)
33+
└── run_worker.py is project/run_worker.py
34+
```
35+
36+
## Running
37+
38+
```bash
39+
agentex agents run --manifest manifest.yaml
40+
```
41+
42+
Open the Temporal UI at http://localhost:8080 to watch the workflow and the
43+
`agent` activity execute. Use `dev.ipynb` to create a task and send messages.
44+
45+
## Adding tools
46+
47+
Define an **async** `@tool` in `project/tools.py` and add it to `TOOLS`. The
48+
model is bound with `TOOLS` and the tool node runs them by name.
49+
50+
For a fuller version with human-in-the-loop approval and graph-introspection
51+
queries, scaffold the `temporal-langgraph` template via `agentex init`.
52+
53+
## Tests
54+
55+
- `tests/test_graph_temporal.py` — hermetic ReAct-loop test with a stub model,
56+
plus a live end-to-end run through the real Temporal plugin (skipped unless
57+
`LITELLM_API_KEY` is set).
58+
- `tests/test_agent.py` — live integration against a running agent.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "36834357",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"from agentex import Agentex\n",
11+
"\n",
12+
"client = Agentex(base_url=\"http://localhost:5003\")"
13+
]
14+
},
15+
{
16+
"cell_type": "code",
17+
"execution_count": null,
18+
"id": "d1c309d6",
19+
"metadata": {},
20+
"outputs": [],
21+
"source": [
22+
"AGENT_NAME = \"at130-langgraph\""
23+
]
24+
},
25+
{
26+
"cell_type": "code",
27+
"execution_count": null,
28+
"id": "9f6e6ef0",
29+
"metadata": {},
30+
"outputs": [],
31+
"source": [
32+
"# (REQUIRED) Create a new task. For Async agents, you must create a task for messages to be associated with.\n",
33+
"import uuid\n",
34+
"\n",
35+
"rpc_response = client.agents.create_task(\n",
36+
" agent_name=AGENT_NAME,\n",
37+
" params={\n",
38+
" \"name\": f\"{str(uuid.uuid4())[:8]}-task\",\n",
39+
" \"params\": {}\n",
40+
" }\n",
41+
")\n",
42+
"\n",
43+
"task = rpc_response.result\n",
44+
"print(task)"
45+
]
46+
},
47+
{
48+
"cell_type": "code",
49+
"execution_count": null,
50+
"id": "b03b0d37",
51+
"metadata": {},
52+
"outputs": [],
53+
"source": [
54+
"# Send an event to the agent\n",
55+
"\n",
56+
"# The response is expected to be a list of TaskMessage objects, which is a union of the following types:\n",
57+
"# - TextContent: A message with just text content \n",
58+
"# - DataContent: A message with JSON-serializable data content\n",
59+
"# - ToolRequestContent: A message with a tool request, which contains a JSON-serializable request to call a tool\n",
60+
"# - ToolResponseContent: A message with a tool response, which contains response object from a tool call in its content\n",
61+
"\n",
62+
"# When processing the message/send response, if you are expecting more than TextContent, such as DataContent, ToolRequestContent, or ToolResponseContent, you can process them as well\n",
63+
"\n",
64+
"rpc_response = client.agents.send_event(\n",
65+
" agent_name=AGENT_NAME,\n",
66+
" params={\n",
67+
" \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n",
68+
" \"task_id\": task.id,\n",
69+
" }\n",
70+
")\n",
71+
"\n",
72+
"event = rpc_response.result\n",
73+
"print(event)"
74+
]
75+
},
76+
{
77+
"cell_type": "code",
78+
"execution_count": null,
79+
"id": "a6927cc0",
80+
"metadata": {},
81+
"outputs": [],
82+
"source": [
83+
"# Subscribe to the async task messages produced by the agent\n",
84+
"from agentex.lib.utils.dev_tools import subscribe_to_async_task_messages\n",
85+
"\n",
86+
"task_messages = subscribe_to_async_task_messages(\n",
87+
" client=client,\n",
88+
" task=task, \n",
89+
" only_after_timestamp=event.created_at, \n",
90+
" print_messages=True,\n",
91+
" rich_print=True,\n",
92+
" timeout=5,\n",
93+
")"
94+
]
95+
},
96+
{
97+
"cell_type": "code",
98+
"execution_count": null,
99+
"id": "4864e354",
100+
"metadata": {},
101+
"outputs": [],
102+
"source": []
103+
}
104+
],
105+
"metadata": {
106+
"kernelspec": {
107+
"display_name": ".venv",
108+
"language": "python",
109+
"name": "python3"
110+
},
111+
"language_info": {
112+
"codemirror_mode": {
113+
"name": "ipython",
114+
"version": 3
115+
},
116+
"file_extension": ".py",
117+
"mimetype": "text/x-python",
118+
"name": "python",
119+
"nbconvert_exporter": "python",
120+
"pygments_lexer": "ipython3",
121+
"version": "3.12.9"
122+
}
123+
},
124+
"nbformat": 4,
125+
"nbformat_minor": 5
126+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Agent Environment Configuration
2+
# ------------------------------
3+
# This file defines environment-specific settings for your agent.
4+
# This DIFFERS from the manifest.yaml file in that it is used to program things that are ONLY per environment.
5+
6+
# ********** EXAMPLE **********
7+
# schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI
8+
# environments:
9+
# dev:
10+
# auth:
11+
# principal:
12+
# user_id: "1234567890"
13+
# user_name: "John Doe"
14+
# user_email: "john.doe@example.com"
15+
# user_role: "admin"
16+
# user_permissions: "read, write, delete"
17+
# helm_overrides: # This is used to override the global helm values.yaml file in the agentex-agent helm charts
18+
# replicas: 3
19+
# resources:
20+
# requests:
21+
# cpu: "1000m"
22+
# memory: "2Gi"
23+
# limits:
24+
# cpu: "2000m"
25+
# memory: "4Gi"
26+
# env:
27+
# - name: LOG_LEVEL
28+
# value: "DEBUG"
29+
# - name: ENVIRONMENT
30+
# value: "staging"
31+
#
32+
# kubernetes:
33+
# # OPTIONAL - Otherwise it will be derived from separately. However, this can be used to override the derived
34+
# # namespace and deploy it with in the same namespace that already exists for a separate agent.
35+
# namespace: "team-at130-langgraph"
36+
# ********** END EXAMPLE **********
37+
38+
schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI
39+
environments:
40+
dev:
41+
auth:
42+
principal:
43+
user_id: # TODO: Fill in
44+
account_id: # TODO: Fill in
45+
helm_overrides:
46+
# This is used to override the global helm values.yaml file in the agentex-agent helm charts
47+
replicaCount: 2
48+
resources:
49+
requests:
50+
cpu: "500m"
51+
memory: "1Gi"
52+
limits:
53+
cpu: "1000m"
54+
memory: "2Gi"
55+
temporal-worker:
56+
enabled: true
57+
replicaCount: 2
58+
resources:
59+
requests:
60+
cpu: "500m"
61+
memory: "1Gi"
62+
limits:
63+
cpu: "1000m"
64+
memory: "2Gi"

0 commit comments

Comments
 (0)