Skip to content

Commit 4a776bd

Browse files
authored
Merge pull request #43 from CopilotKit/claude/deploy-to-render
feat: add Render deployment blueprint
2 parents 6a00ff7 + c00975e commit 4a776bd

File tree

7 files changed

+143
-10
lines changed

7 files changed

+143
-10
lines changed

.env.example

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
OPENAI_API_KEY=
1+
OPENAI_API_KEY=
2+
3+
# LLM model — strong models are required for reliable UI generation
4+
# Recommended: gpt-5.4, gpt-5.4-pro, claude-opus-4-6, gemini-3.1-pro
5+
LLM_MODEL=gpt-5.4-2026-03-05
6+
7+
# Rate limiting (per IP) — disabled by default
8+
RATE_LIMIT_ENABLED=false
9+
RATE_LIMIT_WINDOW_MS=60000
10+
RATE_LIMIT_MAX=40

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ bun.lockb
6060

6161
# Demos
6262
.demos
63+
64+
# References
65+
.references

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ make setup # Install deps + create .env template
2323
make dev # Start all services
2424
```
2525

26+
> **Strong models required.** Generative UI demands high-capability models that can produce complex, well-structured HTML/SVG in a single pass. Set `LLM_MODEL` in your `.env` to one of:
27+
>
28+
> | Model | Provider |
29+
> |-------|----------|
30+
> | `gpt-5.4` / `gpt-5.4-pro` | OpenAI |
31+
> | `claude-opus-4-6` | Anthropic |
32+
> | `gemini-3.1-pro` | Google |
33+
>
34+
> Smaller or weaker models will produce broken layouts, missing interactivity, or incomplete visualizations.
35+
2636
- **App**: http://localhost:3000
2737
- **Agent**: http://localhost:8123
2838

apps/agent/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
It defines the workflow graph, state, tools, nodes and edges.
44
"""
55

6+
import os
7+
68
from copilotkit import CopilotKitMiddleware
79
from langchain.agents import create_agent
810
from langchain_openai import ChatOpenAI
@@ -17,7 +19,7 @@
1719
_skills_text = load_all_skills()
1820

1921
agent = create_agent(
20-
model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
22+
model=ChatOpenAI(model=os.environ.get("LLM_MODEL", "gpt-5.4-2026-03-05")),
2123
tools=[query_data, *todo_tools, generate_form, *template_tools],
2224
middleware=[CopilotKitMiddleware()],
2325
state_schema=AgentState,

apps/app/src/app/api/copilotkit/route.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,72 @@ import {
66
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
77
import { NextRequest } from "next/server";
88

9+
// Simple in-memory sliding-window rate limiter (per IP)
10+
// Enable via RATE_LIMIT_ENABLED=true — off by default.
11+
// For high-traffic deployments, consider Redis-backed rate limiting instead.
12+
const RATE_LIMIT_ENABLED = process.env.RATE_LIMIT_ENABLED === "true";
13+
const RATE_LIMIT_WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS) || 60_000;
14+
const RATE_LIMIT_MAX = Number(process.env.RATE_LIMIT_MAX) || 40;
15+
const hits = new Map<string, number[]>();
16+
17+
function isRateLimited(ip: string): boolean {
18+
if (!RATE_LIMIT_ENABLED) return false;
19+
const now = Date.now();
20+
const timestamps = hits.get(ip)?.filter(t => t > now - RATE_LIMIT_WINDOW_MS) ?? [];
21+
timestamps.push(now);
22+
hits.set(ip, timestamps);
23+
return timestamps.length > RATE_LIMIT_MAX;
24+
}
25+
26+
// Prune stale entries every 5 min to prevent unbounded memory growth
27+
if (RATE_LIMIT_ENABLED) {
28+
setInterval(() => {
29+
const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
30+
hits.forEach((timestamps, ip) => {
31+
const recent = timestamps.filter(t => t > cutoff);
32+
if (recent.length === 0) hits.delete(ip);
33+
else hits.set(ip, recent);
34+
});
35+
}, 300_000);
36+
}
37+
38+
// Normalize Render's fromService hostport (bare host:port) into a full URL
39+
const raw = process.env.LANGGRAPH_DEPLOYMENT_URL;
40+
const deploymentUrl = !raw
41+
? "http://localhost:8123"
42+
: raw.startsWith("http")
43+
? raw
44+
: `http://${raw}`;
45+
946
// 1. Define the agent connection to LangGraph
1047
const defaultAgent = new LangGraphAgent({
11-
deploymentUrl: process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
48+
deploymentUrl,
1249
graphId: "sample_agent",
1350
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
1451
});
1552

1653
// 3. Define the route and CopilotRuntime for the agent
1754
export const POST = async (req: NextRequest) => {
55+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
56+
if (isRateLimited(ip)) {
57+
return new Response("Too many requests", { status: 429 });
58+
}
59+
1860
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
1961
endpoint: "/api/copilotkit",
2062
serviceAdapter: new ExperimentalEmptyAdapter(),
2163
runtime: new CopilotRuntime({
2264
agents: { default: defaultAgent, },
2365
a2ui: { injectA2UITool: true },
24-
mcpApps: {
25-
servers: [{
26-
type: "http",
27-
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
28-
serverId: "example_mcp_app",
29-
}],
30-
},
66+
...(process.env.MCP_SERVER_URL && {
67+
mcpApps: {
68+
servers: [{
69+
type: "http",
70+
url: process.env.MCP_SERVER_URL,
71+
serverId: "example_mcp_app",
72+
}],
73+
},
74+
}),
3175
}),
3276
});
3377

docker/Dockerfile.agent

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM langchain/langgraph-api:3.12
2+
3+
ADD apps/agent /deps/agent
4+
5+
RUN for dep in /deps/*; do \
6+
echo "Installing $dep"; \
7+
if [ -d "$dep" ]; then \
8+
(cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); \
9+
fi; \
10+
done
11+
12+
ENV LANGSERVE_GRAPHS='{"sample_agent": "/deps/agent/main.py:graph"}'
13+
14+
RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license \
15+
&& touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py
16+
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir --no-deps -e /api
17+
18+
WORKDIR /deps/agent

render.yaml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
services:
2+
# ── Agent (LangGraph Python) — private, not exposed to internet ──
3+
- type: pserv
4+
name: open-generative-ui-agent
5+
runtime: docker
6+
plan: starter
7+
dockerfilePath: docker/Dockerfile.agent
8+
healthCheckPath: /ok
9+
envVars:
10+
- key: OPENAI_API_KEY
11+
sync: false
12+
- key: LANGSMITH_API_KEY
13+
sync: false
14+
- key: LLM_MODEL
15+
value: gpt-5.4-2026-03-05
16+
buildFilter:
17+
paths:
18+
- apps/agent/**
19+
- docker/Dockerfile.agent
20+
21+
# ── Frontend (Next.js) — public web service ──
22+
- type: web
23+
name: open-generative-ui-app
24+
runtime: docker
25+
plan: starter
26+
dockerfilePath: docker/Dockerfile.app
27+
envVars:
28+
- key: LANGGRAPH_DEPLOYMENT_URL
29+
fromService:
30+
name: open-generative-ui-agent
31+
type: pserv
32+
property: hostport
33+
- key: LANGSMITH_API_KEY
34+
sync: false
35+
- key: RATE_LIMIT_ENABLED
36+
value: "false"
37+
- key: RATE_LIMIT_WINDOW_MS
38+
value: "60000"
39+
- key: RATE_LIMIT_MAX
40+
value: "40"
41+
buildFilter:
42+
paths:
43+
- apps/app/**
44+
- package.json
45+
- pnpm-lock.yaml
46+
- turbo.json
47+
- docker/Dockerfile.app

0 commit comments

Comments
 (0)