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
36 changes: 34 additions & 2 deletions apps/agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,31 @@
"""

import os
import warnings
from pathlib import Path

from copilotkit import CopilotKitMiddleware
from dotenv import load_dotenv
from fastapi import FastAPI
from copilotkit import CopilotKitMiddleware, LangGraphAGUIAgent
from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from deepagents import create_deep_agent
from langchain_openai import ChatOpenAI

from src.bounded_memory_saver import BoundedMemorySaver
from src.query import query_data
from src.todos import AgentState, todo_tools
from src.form import generate_form
from src.templates import template_tools

load_dotenv()

agent = create_deep_agent(
model=ChatOpenAI(model=os.environ.get("LLM_MODEL", "gpt-5.4-2026-03-05")),
tools=[query_data, *todo_tools, generate_form, *template_tools],
middleware=[CopilotKitMiddleware()],
context_schema=AgentState,
skills=[str(Path(__file__).parent / "skills")],
checkpointer=BoundedMemorySaver(max_threads=200),
system_prompt="""
You are a helpful assistant that helps users understand CopilotKit and LangGraph used together.

Expand Down Expand Up @@ -69,4 +77,28 @@
""",
)

graph = agent
app = FastAPI()


@app.get("/health")
def health():
return {"status": "ok"}


add_langgraph_fastapi_endpoint(
app=app,
agent=LangGraphAGUIAgent(
name="sample_agent",
description="CopilotKit + LangGraph demo agent",
graph=agent,
),
path="/",
)

warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")

if __name__ == "__main__":
import uvicorn

port = int(os.getenv("PORT", "8123"))
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
7 changes: 3 additions & 4 deletions apps/agent/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ description = "A LangGraph agent"
requires-python = ">=3.12"
dependencies = [
"langchain==1.2.0",
"langgraph==1.0.5",
"langgraph==1.0.7", # pinned: BoundedMemorySaver relies on MemorySaver.storage internal
"langsmith>=0.4.49",
"openai>=1.68.2,<2.0.0",
"fastapi>=0.115.5,<1.0.0",
"uvicorn>=0.29.0,<1.0.0",
"python-dotenv>=1.0.0,<2.0.0",
"langgraph-cli[inmem]>=0.4.11",
"langchain-openai>=1.1.0",
"copilotkit>=0.1.77",
"langgraph-api>=0.7.16",
"copilotkit>=0.1.78",
"ag-ui-langgraph==0.0.25",
"langchain-mcp-adapters>=0.2.1",
"deepagents>=0.1.0",
]
44 changes: 44 additions & 0 deletions apps/agent/skills/advanced-visualization/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,50 @@ import mermaid from 'https://esm.sh/mermaid@11/dist/mermaid.esm.min.mjs';
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script>
```

### Three.js Coordinate Conventions

Three.js uses a **right-handed Y-up** coordinate system:
- **X** = right (positive) / left (negative)
- **Y** = up (positive) / down (negative)
- **Z** = toward the viewer (positive) / away from the viewer (negative)

**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**.

When building an aircraft from primitives:
- **Fuselage** = cylinder or box, long axis along **Z** (use `geometry` default or rotate 90° around X)
- **Wings** = flat box, wide along **X**, thin along **Y**, short along **Z**
- **Tail fin** = flat box, tall along **Y**, thin along **X**, short along **Z**

```javascript
// Correct aircraft orientation example:
// Fuselage along Z
const fuselage = new THREE.Mesh(
new THREE.CylinderGeometry(0.15, 0.08, 2.0, 12),
material
);
fuselage.rotation.x = Math.PI / 2; // CylinderGeometry default is Y-up, rotate to Z-forward

// Wings along X
const wing = new THREE.Mesh(
new THREE.BoxGeometry(2.5, 0.03, 0.4), // wide X, thin Y, short Z
material
);

// Vertical stabilizer along Y
const tailFin = new THREE.Mesh(
new THREE.BoxGeometry(0.03, 0.4, 0.3), // thin X, tall Y, short Z
material
);
tailFin.position.set(0, 0.2, 0.9); // above and behind
```

**Rotation axes for flight dynamics:**
- **Pitch** = rotation around **X** (nose up/down)
- **Roll** = rotation around **Z** (wings tilt)
- **Yaw** = rotation around **Y** (nose left/right)

**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.

---

## Part 8: Quality Checklist
Expand Down
55 changes: 55 additions & 0 deletions apps/agent/src/bounded_memory_saver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Bounded checkpoint storage for LangGraph agents.

The default MemorySaver stores all conversation thread checkpoints in memory
indefinitely. On memory-constrained hosts (e.g. Render's 512MB starter plan),
this causes unbounded growth that eventually triggers an OOM kill.

BoundedMemorySaver caps the number of stored threads and evicts the oldest
(FIFO) when the limit is exceeded. Eviction is tracked with an OrderedDict
rather than sorting keys, so eviction order is correct even when thread IDs
are UUIDs or other non-chronological strings.

NOTE: This class relies on MemorySaver.storage (an internal attribute).
The langgraph version is pinned in pyproject.toml to guard against
breaking changes.

NOTE: This class is not thread-safe. It is designed for single-process
async usage (uvicorn). If deploying with multiple worker threads,
wrap put() with a threading.Lock.
"""

import logging
from collections import OrderedDict

from langgraph.checkpoint.memory import MemorySaver

logger = logging.getLogger(__name__)


class BoundedMemorySaver(MemorySaver):
"""MemorySaver that evicts oldest threads when exceeding max_threads."""

def __init__(self, max_threads: int = 200):
super().__init__()
self.max_threads = max_threads
self._insertion_order: OrderedDict[str, None] = OrderedDict()

def put(self, config, checkpoint, metadata, new_versions):
thread_id = config["configurable"]["thread_id"]
# Move to end if already tracked, otherwise insert
self._insertion_order[thread_id] = None
self._insertion_order.move_to_end(thread_id)

result = super().put(config, checkpoint, metadata, new_versions)

while len(self.storage) > self.max_threads and self._insertion_order:
oldest_thread, _ = self._insertion_order.popitem(last=False)
if oldest_thread in self.storage:
logger.info(
"BoundedMemorySaver: evicting thread %s (%d threads stored)",
oldest_thread,
len(self.storage),
)
del self.storage[oldest_thread]
return result
Loading
Loading