Skip to content

Commit 25f6f15

Browse files
committed
refactor(runtime): Support durable compaction of threads
1 parent 1889c1a commit 25f6f15

23 files changed

Lines changed: 1716 additions & 78 deletions

File tree

examples/integrations/langgraph-python-threads/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ INTELLIGENCE_GATEWAY_WS_URL=ws://localhost:4401
1010
INTELLIGENCE_API_KEY=cpk_sPRVSEED_seed0privat0longtoken00
1111
INTELLIGENCE_ORGANIZATION_ID=casa-de-erlang
1212

13+
# Optional MCP App server for demos. Defaults to Excalidraw when unset.
14+
MCP_SERVER_URL=https://mcp.excalidraw.com
15+
1316
# Optional thread culler tuning
1417
THREAD_STALE_HOURS=3
1518
THREAD_CULL_BATCH_SIZE=1000

examples/integrations/langgraph-python-threads/apps/agent/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,27 @@
77
from langchain.agents import create_agent
88

99
# Data & state tools
10+
from src.a2ui_dynamic_schema import generate_a2ui
11+
from src.a2ui_fixed_schema import search_flights
1012
from src.query import query_data
1113
from src.todos import AgentState, todo_tools
1214

1315
agent = create_agent(
1416
model="openai:gpt-4.1",
15-
tools=[query_data, *todo_tools],
17+
tools=[query_data, *todo_tools, generate_a2ui, search_flights],
1618
middleware=[CopilotKitMiddleware()],
1719
state_schema=AgentState,
1820
system_prompt="""
1921
You are a polished, professional demo assistant. Keep responses to 1-2 sentences.
2022
2123
Tool guidance:
24+
- Flights: call search_flights to show flight cards with a pre-built schema.
25+
- Dashboards & rich UI: call generate_a2ui to create dashboard UIs with metrics,
26+
charts, tables, and cards. It handles rendering automatically.
2227
- Charts: call query_data first, then render with the chart component.
2328
- Todos: enable app mode first, then manage todos.
29+
- A2UI actions: when you see a log_a2ui_event result (e.g. "view_details"),
30+
respond with a brief confirmation. The UI already updated on the frontend.
2431
""",
2532
)
2633

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
Dynamic A2UI tool: LLM-generated UI from conversation context.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import json
8+
from typing import Any
9+
10+
from copilotkit import a2ui
11+
from langchain.tools import ToolRuntime, tool
12+
from langchain_core.messages import SystemMessage
13+
from langchain_core.tools import tool as lc_tool
14+
from langchain_openai import ChatOpenAI
15+
16+
CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog"
17+
18+
19+
@lc_tool
20+
def render_a2ui(
21+
surfaceId: str,
22+
catalogId: str,
23+
components: list[dict],
24+
data: dict | None = None,
25+
) -> str:
26+
"""Render a dynamic A2UI v0.9 surface."""
27+
return "rendered"
28+
29+
30+
@tool()
31+
def generate_a2ui(runtime: ToolRuntime[Any]) -> str:
32+
"""Generate dynamic A2UI components based on the conversation."""
33+
messages = runtime.state["messages"][:-1]
34+
context_entries = runtime.state.get("copilotkit", {}).get("context", [])
35+
context_text = "\n\n".join(
36+
entry.get("value", "")
37+
for entry in context_entries
38+
if isinstance(entry, dict) and entry.get("value")
39+
)
40+
41+
model = ChatOpenAI(model="gpt-4.1")
42+
model_with_tool = model.bind_tools([render_a2ui], tool_choice="render_a2ui")
43+
response = model_with_tool.invoke([SystemMessage(content=context_text), *messages])
44+
45+
if not response.tool_calls:
46+
return json.dumps({"error": "LLM did not call render_a2ui"})
47+
48+
args = response.tool_calls[0]["args"]
49+
surface_id = args.get("surfaceId", "dynamic-surface")
50+
catalog_id = args.get("catalogId", CUSTOM_CATALOG_ID)
51+
components = args.get("components", [])
52+
data = args.get("data", {})
53+
54+
operations = [
55+
a2ui.create_surface(surface_id, catalog_id=catalog_id),
56+
a2ui.update_components(surface_id, components),
57+
]
58+
if data:
59+
operations.append(a2ui.update_data_model(surface_id, data))
60+
61+
return a2ui.render(operations=operations)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Fixed-schema A2UI tool: flight search results.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from pathlib import Path
8+
from typing import TypedDict
9+
10+
from copilotkit import a2ui
11+
from langchain.tools import tool
12+
13+
CATALOG_ID = "copilotkit://app-dashboard-catalog"
14+
SURFACE_ID = "flight-search-results"
15+
FLIGHT_SCHEMA = a2ui.load_schema(
16+
Path(__file__).parent / "a2ui" / "schemas" / "flight_schema.json"
17+
)
18+
19+
20+
class Flight(TypedDict):
21+
id: str
22+
airline: str
23+
airlineLogo: str
24+
flightNumber: str
25+
origin: str
26+
destination: str
27+
date: str
28+
departureTime: str
29+
arrivalTime: str
30+
duration: str
31+
status: str
32+
statusIcon: str
33+
price: str
34+
35+
36+
@tool
37+
def search_flights(flights: list[Flight]) -> str:
38+
"""Search for flights and display the results as rich cards. Return exactly 2 flights.
39+
40+
Each flight must have: id, airline, airlineLogo, flightNumber, origin,
41+
destination, date, departureTime, arrivalTime, duration, status, statusIcon,
42+
and price.
43+
"""
44+
return a2ui.render(
45+
operations=[
46+
a2ui.create_surface(SURFACE_ID, catalog_id=CATALOG_ID),
47+
a2ui.update_components(SURFACE_ID, FLIGHT_SCHEMA),
48+
a2ui.update_data_model(SURFACE_ID, {"flights": flights}),
49+
],
50+
)

examples/integrations/langgraph-python-threads/apps/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"dependencies": {
1111
"@ag-ui/client": "0.0.52",
12+
"@copilotkit/a2ui-renderer": "1.56.2",
1213
"@copilotkit/react-core": "1.56.2",
1314
"@copilotkit/react-ui": "1.56.2",
1415
"@hono/node-server": "^1.19.14",

examples/integrations/langgraph-python-threads/apps/app/src/App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ExampleCanvas } from "@/components/example-canvas";
66
import { ThreadsDrawer } from "@/components/threads-drawer";
77
import { ThemeProvider } from "@/hooks/use-theme";
88
import { useExampleSuggestions, useGenerativeUIExamples } from "@/hooks";
9+
import { demonstrationCatalog } from "@/declarative-generative-ui/renderers";
910
import styles from "@/components/threads-drawer/threads-drawer.module.css";
1011

1112
const runtimeUrl = "/api/copilotkit";
@@ -42,7 +43,12 @@ function HomePage() {
4243
export default function App() {
4344
return (
4445
<ThemeProvider>
45-
<CopilotKitProvider runtimeUrl={runtimeUrl}>
46+
<CopilotKitProvider
47+
runtimeUrl={runtimeUrl}
48+
a2ui={{ catalog: demonstrationCatalog }}
49+
openGenerativeUI={{}}
50+
useSingleEndpoint={false}
51+
>
4652
<HomePage />
4753
</CopilotKitProvider>
4854
</ThemeProvider>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { z } from "zod";
2+
3+
const DynString = z.union([z.string(), z.object({ path: z.string() })]);
4+
5+
export const demonstrationCatalogDefinitions = {
6+
Title: {
7+
description: "A heading. Use for section titles and page headers.",
8+
props: z.object({
9+
text: z.string(),
10+
level: z.string().optional(),
11+
}),
12+
},
13+
Row: {
14+
description: "Horizontal layout container.",
15+
props: z.object({
16+
gap: z.number().optional(),
17+
align: z.string().optional(),
18+
justify: z.string().optional(),
19+
children: z.union([
20+
z.array(z.string()),
21+
z.object({ componentId: z.string(), path: z.string() }),
22+
]),
23+
}),
24+
},
25+
Column: {
26+
description: "Vertical layout container.",
27+
props: z.object({
28+
gap: z.number().optional(),
29+
align: z.string().optional(),
30+
children: z.union([
31+
z.array(z.string()),
32+
z.object({ componentId: z.string(), path: z.string() }),
33+
]),
34+
}),
35+
},
36+
DashboardCard: {
37+
description:
38+
"A card container with title and optional subtitle. Has a 'child' slot for content.",
39+
props: z.object({
40+
title: z.string(),
41+
subtitle: z.string().optional(),
42+
child: z.string().optional(),
43+
}),
44+
},
45+
Metric: {
46+
description: "A key metric display with label, value, and optional trend indicator.",
47+
props: z.object({
48+
label: z.string(),
49+
value: z.string(),
50+
trend: z.enum(["up", "down", "neutral"]).optional(),
51+
trendValue: z.string().optional(),
52+
}),
53+
},
54+
PieChart: {
55+
description: "A pie or donut chart.",
56+
props: z.object({
57+
data: z.array(
58+
z.object({
59+
label: z.string(),
60+
value: z.number(),
61+
color: z.string().optional(),
62+
}),
63+
),
64+
innerRadius: z.number().optional(),
65+
}),
66+
},
67+
BarChart: {
68+
description: "A bar chart.",
69+
props: z.object({
70+
data: z.array(z.object({ label: z.string(), value: z.number() })),
71+
color: z.string().optional(),
72+
}),
73+
},
74+
Badge: {
75+
description: "A small status badge.",
76+
props: z.object({
77+
text: z.string(),
78+
variant: z
79+
.enum(["success", "warning", "error", "info", "neutral"])
80+
.optional(),
81+
}),
82+
},
83+
DataTable: {
84+
description: "A data table with columns and rows.",
85+
props: z.object({
86+
columns: z.array(z.object({ key: z.string(), label: z.string() })),
87+
rows: z.array(z.record(z.any())),
88+
}),
89+
},
90+
Button: {
91+
description: "An interactive button with an action event.",
92+
props: z.object({
93+
child: z.string(),
94+
variant: z.enum(["primary", "secondary", "ghost"]).optional(),
95+
action: z
96+
.union([
97+
z.object({
98+
event: z.object({
99+
name: z.string(),
100+
context: z.record(z.any()).optional(),
101+
}),
102+
}),
103+
z.null(),
104+
])
105+
.optional(),
106+
}),
107+
},
108+
FlightCard: {
109+
description: "A rich flight result card.",
110+
props: z.object({
111+
airline: DynString,
112+
airlineLogo: DynString,
113+
flightNumber: DynString,
114+
origin: DynString,
115+
destination: DynString,
116+
date: DynString,
117+
departureTime: DynString,
118+
arrivalTime: DynString,
119+
duration: DynString,
120+
status: DynString,
121+
statusColor: DynString.optional(),
122+
price: DynString,
123+
action: z
124+
.union([
125+
z.object({
126+
event: z.object({
127+
name: z.string(),
128+
context: z.record(z.any()).optional(),
129+
}),
130+
}),
131+
z.null(),
132+
])
133+
.optional(),
134+
}),
135+
},
136+
};
137+
138+
export type DemonstrationCatalogDefinitions =
139+
typeof demonstrationCatalogDefinitions;

0 commit comments

Comments
 (0)