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
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Environments
.env**
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Git
.git
.gitignore

# Misc
.DS_Store
13 changes: 13 additions & 0 deletions examples/tutorials/10_async/10_temporal/130_langgraph/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# at130-langgraph - Environment Variables
# Copy this file to .env and fill in the values

# API key for your LLM provider
LITELLM_API_KEY=

# LLM base URL (optional - override to use a different provider)
# OPENAI_BASE_URL=

# SGP Configuration (optional - for tracing)
# SGP_API_KEY=
# SGP_ACCOUNT_ID=
# SGP_CLIENT_BASE_URL=
43 changes: 43 additions & 0 deletions examples/tutorials/10_async/10_temporal/130_langgraph/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# syntax=docker/dockerfile:1.3
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/

# Install system dependencies
RUN apt-get update && apt-get install -y \
htop \
vim \
curl \
tar \
python3-dev \
postgresql-client \
build-essential \
libpq-dev \
gcc \
cmake \
netcat-openbsd \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN uv pip install --system --upgrade pip setuptools wheel

ENV UV_HTTP_TIMEOUT=1000

COPY 10_async/10_temporal/130_langgraph/pyproject.toml /app/130_langgraph/pyproject.toml
COPY 10_async/10_temporal/130_langgraph/README.md /app/130_langgraph/README.md

WORKDIR /app/130_langgraph

COPY 10_async/10_temporal/130_langgraph/project /app/130_langgraph/project
COPY 10_async/10_temporal/130_langgraph/tests /app/130_langgraph/tests
COPY test_utils /app/test_utils

RUN uv pip install --system .[dev]

ENV PYTHONPATH=/app

ENV AGENT_NAME=at130-langgraph

CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]

# When we deploy the worker, we will replace the CMD with the following
# CMD ["python", "-m", "run_worker"]
58 changes: 58 additions & 0 deletions examples/tutorials/10_async/10_temporal/130_langgraph/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# at130-langgraph — AgentEx Temporal + LangGraph

A minimal Temporal-backed [LangGraph](https://langchain-ai.github.io/langgraph/)
agent. It uses the official [`temporalio.contrib.langgraph`](https://docs.temporal.io/develop/python/integrations/langgraph)
plugin so each LangGraph node runs as a durable **Temporal activity** (the LLM
`agent` node) or inline in the **workflow** (the `tools` node) — set per node
with `execute_in`. *Temporal is the runtime; LangGraph is the agent framework.*

> The Temporal LangGraph plugin is currently **experimental**.

## The graph

```
START → agent → (tool calls?) → tools → agent
→ (no tool calls?) → END
```

- `agent` (`execute_in="activity"`): the LLM call — a retried, observable Temporal activity.
- `tools` (`execute_in="workflow"`): runs the tool calls inline in the workflow.

The router and tools are `async` so LangGraph awaits them directly (a sync
callable is offloaded via `run_in_executor`, which Temporal workflows forbid).

## Project structure

```
130_langgraph/
├── project/
│ ├── acp.py # Thin async ACP server; registers the LangGraphPlugin
│ ├── workflow.py # Runs the graph each turn; keeps multi-turn memory
│ ├── graph.py # LangGraph graph; nodes tagged execute_in activity/workflow
│ └── tools.py # Async tool(s)
└── run_worker.py is project/run_worker.py
```

## Running

```bash
agentex agents run --manifest manifest.yaml
```

Open the Temporal UI at http://localhost:8080 to watch the workflow and the
`agent` activity execute. Use `dev.ipynb` to create a task and send messages.

## Adding tools

Define an **async** `@tool` in `project/tools.py` and add it to `TOOLS`. The
model is bound with `TOOLS` and the tool node runs them by name.

For a fuller version with human-in-the-loop approval and graph-introspection
queries, scaffold the `temporal-langgraph` template via `agentex init`.

## Tests

- `tests/test_graph_temporal.py` — hermetic ReAct-loop test with a stub model,
plus a live end-to-end run through the real Temporal plugin (skipped unless
`LITELLM_API_KEY` is set).
- `tests/test_agent.py` — live integration against a running agent.
126 changes: 126 additions & 0 deletions examples/tutorials/10_async/10_temporal/130_langgraph/dev.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "36834357",
"metadata": {},
"outputs": [],
"source": [
"from agentex import Agentex\n",
"\n",
"client = Agentex(base_url=\"http://localhost:5003\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d1c309d6",
"metadata": {},
"outputs": [],
"source": [
"AGENT_NAME = \"at130-langgraph\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9f6e6ef0",
"metadata": {},
"outputs": [],
"source": [
"# (REQUIRED) Create a new task. For Async agents, you must create a task for messages to be associated with.\n",
"import uuid\n",
"\n",
"rpc_response = client.agents.create_task(\n",
" agent_name=AGENT_NAME,\n",
" params={\n",
" \"name\": f\"{str(uuid.uuid4())[:8]}-task\",\n",
" \"params\": {}\n",
" }\n",
")\n",
"\n",
"task = rpc_response.result\n",
"print(task)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b03b0d37",
"metadata": {},
"outputs": [],
"source": [
"# Send an event to the agent\n",
"\n",
"# The response is expected to be a list of TaskMessage objects, which is a union of the following types:\n",
"# - TextContent: A message with just text content \n",
"# - DataContent: A message with JSON-serializable data content\n",
"# - ToolRequestContent: A message with a tool request, which contains a JSON-serializable request to call a tool\n",
"# - ToolResponseContent: A message with a tool response, which contains response object from a tool call in its content\n",
"\n",
"# 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",
"\n",
"rpc_response = client.agents.send_event(\n",
" agent_name=AGENT_NAME,\n",
" params={\n",
" \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n",
" \"task_id\": task.id,\n",
" }\n",
")\n",
"\n",
"event = rpc_response.result\n",
"print(event)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a6927cc0",
"metadata": {},
"outputs": [],
"source": [
"# Subscribe to the async task messages produced by the agent\n",
"from agentex.lib.utils.dev_tools import subscribe_to_async_task_messages\n",
"\n",
"task_messages = subscribe_to_async_task_messages(\n",
" client=client,\n",
" task=task, \n",
" only_after_timestamp=event.created_at, \n",
" print_messages=True,\n",
" rich_print=True,\n",
" timeout=5,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4864e354",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Agent Environment Configuration
# ------------------------------
# This file defines environment-specific settings for your agent.
# This DIFFERS from the manifest.yaml file in that it is used to program things that are ONLY per environment.

# ********** EXAMPLE **********
# schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI
# environments:
# dev:
# auth:
# principal:
# user_id: "1234567890"
# user_name: "John Doe"
# user_email: "john.doe@example.com"
# user_role: "admin"
# user_permissions: "read, write, delete"
# helm_overrides: # This is used to override the global helm values.yaml file in the agentex-agent helm charts
# replicas: 3
# resources:
# requests:
# cpu: "1000m"
# memory: "2Gi"
# limits:
# cpu: "2000m"
# memory: "4Gi"
# env:
# - name: LOG_LEVEL
# value: "DEBUG"
# - name: ENVIRONMENT
# value: "staging"
#
# kubernetes:
# # OPTIONAL - Otherwise it will be derived from separately. However, this can be used to override the derived
# # namespace and deploy it with in the same namespace that already exists for a separate agent.
# namespace: "team-at130-langgraph"
# ********** END EXAMPLE **********

schema_version: "v1" # This is used to validate the file structure and is not used by the agentex CLI
environments:
dev:
auth:
principal:
user_id: # TODO: Fill in
account_id: # TODO: Fill in
helm_overrides:
# This is used to override the global helm values.yaml file in the agentex-agent helm charts
replicaCount: 2
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1000m"
memory: "2Gi"
temporal-worker:
enabled: true
replicaCount: 2
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1000m"
memory: "2Gi"
Loading
Loading