Skip to content

Commit 47cf518

Browse files
authored
refactor(runtime): Support durable compaction of threads (CopilotKit#4130)
<!-- Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. **Please PLEASE reach out to us first before starting any significant work on new or existing features.** By the time you've gotten here, you're looking at creating a pull request so hopefully we're not too late. We love community contributions! That said, we want to make sure we're all on the same page before you start. Investing a lot of time and effort just to find out it doesn't align with the upstream project feels awful, and we don't want that to happen. It also helps to make sure the work you're planning isn't already in progress. As described in our contributing guide, please file an issue first: https://github.com/ag-ui-protocol/ag-ui/issues Or, reach out to us on Discord: https://discord.com/invite/6dffbvGU3D You can learn more about contributing to copilotkit here: https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md Happy contributing! --> ## What does this PR do? (Describe the changes introduced in this PR) ## Related PRs and Issues - (Direct link to related PR or issue, if relevant) ## Checklist - [ ] I have read the [Contribution Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md) - [ ] If the PR changes or adds functionality, I have updated the relevant documentation
2 parents 1889c1a + cf6d8f4 commit 47cf518

21 files changed

Lines changed: 1829 additions & 103 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: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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:
47+
"A key metric display with label, value, and optional trend indicator.",
48+
props: z.object({
49+
label: z.string(),
50+
value: z.string(),
51+
trend: z.enum(["up", "down", "neutral"]).optional(),
52+
trendValue: z.string().optional(),
53+
}),
54+
},
55+
PieChart: {
56+
description: "A pie or donut chart.",
57+
props: z.object({
58+
data: z.array(
59+
z.object({
60+
label: z.string(),
61+
value: z.number(),
62+
color: z.string().optional(),
63+
}),
64+
),
65+
innerRadius: z.number().optional(),
66+
}),
67+
},
68+
BarChart: {
69+
description: "A bar chart.",
70+
props: z.object({
71+
data: z.array(z.object({ label: z.string(), value: z.number() })),
72+
color: z.string().optional(),
73+
}),
74+
},
75+
Badge: {
76+
description: "A small status badge.",
77+
props: z.object({
78+
text: z.string(),
79+
variant: z
80+
.enum(["success", "warning", "error", "info", "neutral"])
81+
.optional(),
82+
}),
83+
},
84+
DataTable: {
85+
description: "A data table with columns and rows.",
86+
props: z.object({
87+
columns: z.array(z.object({ key: z.string(), label: z.string() })),
88+
rows: z.array(z.record(z.any())),
89+
}),
90+
},
91+
Button: {
92+
description: "An interactive button with an action event.",
93+
props: z.object({
94+
child: z.string(),
95+
variant: z.enum(["primary", "secondary", "ghost"]).optional(),
96+
action: z
97+
.union([
98+
z.object({
99+
event: z.object({
100+
name: z.string(),
101+
context: z.record(z.any()).optional(),
102+
}),
103+
}),
104+
z.null(),
105+
])
106+
.optional(),
107+
}),
108+
},
109+
FlightCard: {
110+
description: "A rich flight result card.",
111+
props: z.object({
112+
airline: DynString,
113+
airlineLogo: DynString,
114+
flightNumber: DynString,
115+
origin: DynString,
116+
destination: DynString,
117+
date: DynString,
118+
departureTime: DynString,
119+
arrivalTime: DynString,
120+
duration: DynString,
121+
status: DynString,
122+
statusColor: DynString.optional(),
123+
price: DynString,
124+
action: z
125+
.union([
126+
z.object({
127+
event: z.object({
128+
name: z.string(),
129+
context: z.record(z.any()).optional(),
130+
}),
131+
}),
132+
z.null(),
133+
])
134+
.optional(),
135+
}),
136+
},
137+
};
138+
139+
export type DemonstrationCatalogDefinitions =
140+
typeof demonstrationCatalogDefinitions;

0 commit comments

Comments
 (0)