Skip to content

Commit fb2c361

Browse files
authored
Merge pull request #53 from CopilotKit/fix/native-python-agent
fix: native Python agent for Render deployment
2 parents 38e8c5e + 1bfd51b commit fb2c361

File tree

8 files changed

+344
-478
lines changed

8 files changed

+344
-478
lines changed

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
]

apps/agent/skills/advanced-visualization/SKILL.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,50 @@ import mermaid from 'https://esm.sh/mermaid@11/dist/mermaid.esm.min.mjs';
698698
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script>
699699
```
700700

701+
### Three.js Coordinate Conventions
702+
703+
Three.js uses a **right-handed Y-up** coordinate system:
704+
- **X** = right (positive) / left (negative)
705+
- **Y** = up (positive) / down (negative)
706+
- **Z** = toward the viewer (positive) / away from the viewer (negative)
707+
708+
**Critical for vehicles and aircraft:** The fuselage/body extends along **Z** (nose at -Z, tail at +Z). Wings extend along **X** (left/right). The vertical stabilizer extends along **Y**.
709+
710+
When building an aircraft from primitives:
711+
- **Fuselage** = cylinder or box, long axis along **Z** (use `geometry` default or rotate 90° around X)
712+
- **Wings** = flat box, wide along **X**, thin along **Y**, short along **Z**
713+
- **Tail fin** = flat box, tall along **Y**, thin along **X**, short along **Z**
714+
715+
```javascript
716+
// Correct aircraft orientation example:
717+
// Fuselage along Z
718+
const fuselage = new THREE.Mesh(
719+
new THREE.CylinderGeometry(0.15, 0.08, 2.0, 12),
720+
material
721+
);
722+
fuselage.rotation.x = Math.PI / 2; // CylinderGeometry default is Y-up, rotate to Z-forward
723+
724+
// Wings along X
725+
const wing = new THREE.Mesh(
726+
new THREE.BoxGeometry(2.5, 0.03, 0.4), // wide X, thin Y, short Z
727+
material
728+
);
729+
730+
// Vertical stabilizer along Y
731+
const tailFin = new THREE.Mesh(
732+
new THREE.BoxGeometry(0.03, 0.4, 0.3), // thin X, tall Y, short Z
733+
material
734+
);
735+
tailFin.position.set(0, 0.2, 0.9); // above and behind
736+
```
737+
738+
**Rotation axes for flight dynamics:**
739+
- **Pitch** = rotation around **X** (nose up/down)
740+
- **Roll** = rotation around **Z** (wings tilt)
741+
- **Yaw** = rotation around **Y** (nose left/right)
742+
743+
**Common mistake:** Using the wing box as the fuselage (wide along X instead of Z). Always verify: the longest dimension of the fuselage should be along Z.
744+
701745
---
702746

703747
## Part 8: Quality Checklist
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 and self._insertion_order:
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)