Skip to content

Commit 9b0694b

Browse files
GeneralJerelclaude
andcommitted
fix: replace langgraph-api Docker image with native FastAPI server
The langgraph-api Docker image requires PostgreSQL (DATABASE_URI) which blocked Render deployment. Switch to serving the agent directly via FastAPI + ag_ui_langgraph, matching the Shadify reference pattern. - Replace Dockerfile.agent with native Python runtime on Render - Add BoundedMemorySaver for memory-safe checkpointing (200 thread cap) - Switch frontend from LangGraphAgent to LangGraphHttpAgent (AG-UI protocol) - Remove langgraph-api, langgraph-cli deps (-25 packages) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 38e8c5e commit 9b0694b

7 files changed

Lines changed: 300 additions & 478 deletions

File tree

apps/agent/main.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,31 @@
44
"""
55

66
import os
7+
import warnings
78
from pathlib import Path
89

9-
from copilotkit import CopilotKitMiddleware
10+
from dotenv import load_dotenv
11+
from fastapi import FastAPI
12+
from copilotkit import CopilotKitMiddleware, LangGraphAGUIAgent
13+
from ag_ui_langgraph import add_langgraph_fastapi_endpoint
1014
from deepagents import create_deep_agent
1115
from langchain_openai import ChatOpenAI
1216

17+
from src.bounded_memory_saver import BoundedMemorySaver
1318
from src.query import query_data
1419
from src.todos import AgentState, todo_tools
1520
from src.form import generate_form
1621
from src.templates import template_tools
1722

23+
load_dotenv()
24+
1825
agent = create_deep_agent(
1926
model=ChatOpenAI(model=os.environ.get("LLM_MODEL", "gpt-5.4-2026-03-05")),
2027
tools=[query_data, *todo_tools, generate_form, *template_tools],
2128
middleware=[CopilotKitMiddleware()],
2229
context_schema=AgentState,
2330
skills=[str(Path(__file__).parent / "skills")],
31+
checkpointer=BoundedMemorySaver(max_threads=200),
2432
system_prompt="""
2533
You are a helpful assistant that helps users understand CopilotKit and LangGraph used together.
2634
@@ -69,4 +77,28 @@
6977
""",
7078
)
7179

72-
graph = agent
80+
app = FastAPI()
81+
82+
83+
@app.get("/health")
84+
def health():
85+
return {"status": "ok"}
86+
87+
88+
add_langgraph_fastapi_endpoint(
89+
app=app,
90+
agent=LangGraphAGUIAgent(
91+
name="sample_agent",
92+
description="CopilotKit + LangGraph demo agent",
93+
graph=agent,
94+
),
95+
path="/",
96+
)
97+
98+
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")
99+
100+
if __name__ == "__main__":
101+
import uvicorn
102+
103+
port = int(os.getenv("PORT", "8123"))
104+
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)

apps/agent/pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ description = "A LangGraph agent"
55
requires-python = ">=3.12"
66
dependencies = [
77
"langchain==1.2.0",
8-
"langgraph==1.0.5",
8+
"langgraph==1.0.7", # pinned: BoundedMemorySaver relies on MemorySaver.storage internal
99
"langsmith>=0.4.49",
1010
"openai>=1.68.2,<2.0.0",
1111
"fastapi>=0.115.5,<1.0.0",
1212
"uvicorn>=0.29.0,<1.0.0",
1313
"python-dotenv>=1.0.0,<2.0.0",
14-
"langgraph-cli[inmem]>=0.4.11",
1514
"langchain-openai>=1.1.0",
16-
"copilotkit>=0.1.77",
17-
"langgraph-api>=0.7.16",
15+
"copilotkit>=0.1.78",
16+
"ag-ui-langgraph==0.0.25",
1817
"langchain-mcp-adapters>=0.2.1",
1918
"deepagents>=0.1.0",
2019
]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Bounded checkpoint storage for LangGraph agents.
3+
4+
The default MemorySaver stores all conversation thread checkpoints in memory
5+
indefinitely. On memory-constrained hosts (e.g. Render's 512MB starter plan),
6+
this causes unbounded growth that eventually triggers an OOM kill.
7+
8+
BoundedMemorySaver caps the number of stored threads and evicts the oldest
9+
(FIFO) when the limit is exceeded. Eviction is tracked with an OrderedDict
10+
rather than sorting keys, so eviction order is correct even when thread IDs
11+
are UUIDs or other non-chronological strings.
12+
13+
NOTE: This class relies on MemorySaver.storage (an internal attribute).
14+
The langgraph version is pinned in pyproject.toml to guard against
15+
breaking changes.
16+
17+
NOTE: This class is not thread-safe. It is designed for single-process
18+
async usage (uvicorn). If deploying with multiple worker threads,
19+
wrap put() with a threading.Lock.
20+
"""
21+
22+
import logging
23+
from collections import OrderedDict
24+
25+
from langgraph.checkpoint.memory import MemorySaver
26+
27+
logger = logging.getLogger(__name__)
28+
29+
30+
class BoundedMemorySaver(MemorySaver):
31+
"""MemorySaver that evicts oldest threads when exceeding max_threads."""
32+
33+
def __init__(self, max_threads: int = 200):
34+
super().__init__()
35+
self.max_threads = max_threads
36+
self._insertion_order: OrderedDict[str, None] = OrderedDict()
37+
38+
def put(self, config, checkpoint, metadata, new_versions):
39+
thread_id = config["configurable"]["thread_id"]
40+
# Move to end if already tracked, otherwise insert
41+
self._insertion_order[thread_id] = None
42+
self._insertion_order.move_to_end(thread_id)
43+
44+
result = super().put(config, checkpoint, metadata, new_versions)
45+
46+
while len(self.storage) > self.max_threads:
47+
oldest_thread, _ = self._insertion_order.popitem(last=False)
48+
if oldest_thread in self.storage:
49+
logger.info(
50+
"BoundedMemorySaver: evicting thread %s (%d threads stored)",
51+
oldest_thread,
52+
len(self.storage),
53+
)
54+
del self.storage[oldest_thread]
55+
return result

0 commit comments

Comments
 (0)