Skip to content

Commit ca22e38

Browse files
committed
Merge remote-tracking branch 'origin/main' into alem/d5-fixture-fixes-bucket-a
# Conflicts: # showcase/aimock/d5-all.json
2 parents 22a8446 + 3aa64b0 commit ca22e38

15 files changed

Lines changed: 2401 additions & 2238 deletions

File tree

docs/components/layout/navbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const Navbar = ({ pageTree }: NavbarProps) => {
8484

8585
const handleTalkToEngineersClick = () => {
8686
posthog?.capture("talk_to_us_clicked", { location: "docs_nav" });
87-
window.location.href = "https://copilotkit.ai/contact-us";
87+
window.location.href = "https://copilotkit.ai/talk-to-an-engineer";
8888
};
8989

9090
// Read sessionStorage on client only to avoid hydration mismatch (tab-specific)

showcase/aimock/d5-all.json

Lines changed: 2149 additions & 2146 deletions
Large diffs are not rendered by default.

showcase/aimock/feature-parity.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@
88
"content": "Here's your sales dashboard \u2014 total revenue is $1.2M (+12% MoM), 342 new customers, and 4.2% conversion. The pie shows revenue split by category and the bar chart tracks monthly sales."
99
}
1010
},
11+
{
12+
"match": {
13+
"userMessage": "with total revenue, new customers, and conversion rate metrics",
14+
"toolName": "_design_a2ui_surface"
15+
},
16+
"response": {
17+
"toolCalls": [
18+
{
19+
"id": "call_fp_design_a2ui_surface_sales_dashboard_001",
20+
"name": "_design_a2ui_surface",
21+
"arguments": "{\"surfaceId\":\"beautiful-chat-sales-dashboard\",\"catalogId\":\"copilotkit://app-dashboard-catalog\",\"components\":[{\"id\":\"root\",\"component\":\"Column\",\"children\":[\"row-metrics\",\"row-charts\"],\"gap\":16},{\"id\":\"row-metrics\",\"component\":\"Row\",\"children\":[\"card-revenue\",\"card-customers\",\"card-conversion\"],\"gap\":16},{\"id\":\"card-revenue\",\"component\":\"DashboardCard\",\"title\":\"Total Revenue\",\"child\":\"metric-revenue\"},{\"id\":\"metric-revenue\",\"component\":\"Metric\",\"label\":\"Total Revenue\",\"value\":\"$1.2M\",\"trend\":\"up\",\"trendValue\":\"+12% MoM\"},{\"id\":\"card-customers\",\"component\":\"DashboardCard\",\"title\":\"New Customers\",\"child\":\"metric-customers\"},{\"id\":\"metric-customers\",\"component\":\"Metric\",\"label\":\"New Customers\",\"value\":\"342\",\"trend\":\"up\",\"trendValue\":\"+8% MoM\"},{\"id\":\"card-conversion\",\"component\":\"DashboardCard\",\"title\":\"Conversion Rate\",\"child\":\"metric-conversion\"},{\"id\":\"metric-conversion\",\"component\":\"Metric\",\"label\":\"Conversion Rate\",\"value\":\"4.2%\",\"trend\":\"neutral\"},{\"id\":\"row-charts\",\"component\":\"Row\",\"children\":[\"card-pie\",\"card-bar\"],\"gap\":16},{\"id\":\"card-pie\",\"component\":\"DashboardCard\",\"title\":\"Revenue by Category\",\"child\":\"pie-revenue\"},{\"id\":\"pie-revenue\",\"component\":\"PieChart\",\"data\":[{\"label\":\"Electronics\",\"value\":42000},{\"label\":\"Clothing\",\"value\":28000},{\"label\":\"Food\",\"value\":18000},{\"label\":\"Books\",\"value\":12000}]},{\"id\":\"card-bar\",\"component\":\"DashboardCard\",\"title\":\"Monthly Sales\",\"child\":\"bar-monthly\"},{\"id\":\"bar-monthly\",\"component\":\"BarChart\",\"data\":[{\"label\":\"Jan\",\"value\":50000},{\"label\":\"Feb\",\"value\":62000},{\"label\":\"Mar\",\"value\":58000},{\"label\":\"Apr\",\"value\":71000},{\"label\":\"May\",\"value\":80000},{\"label\":\"Jun\",\"value\":92000}]}]}"
22+
}
23+
]
24+
}
25+
},
1126
{
1227
"match": {
1328
"userMessage": "with total revenue, new customers, and conversion rate metrics",

showcase/harness/src/probes/scripts/_beautiful-chat-shared.ts

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -239,36 +239,38 @@ export async function assertToggleTheme(page: ConversationPage): Promise<void> {
239239
);
240240
}
241241
try {
242-
await waiter.waitForFunction(
243-
() => {
244-
const win = globalThis as unknown as {
245-
document: {
246-
documentElement: { classList: { contains(s: string): boolean } };
247-
body: { textContent: string | null };
242+
await waiter
243+
.waitForFunction(
244+
() => {
245+
const win = globalThis as unknown as {
246+
document: {
247+
documentElement: { classList: { contains(s: string): boolean } };
248+
body: { textContent: string | null };
249+
};
248250
};
249-
};
250-
const isDark =
251-
win.document.documentElement.classList.contains("dark");
252-
const text = (win.document.body.textContent ?? "").toLowerCase();
253-
const themeToggledRendered = text.includes("theme toggled");
254-
// Track (a): class flipped from the initial reading.
255-
// Track (b): the agent's confirmation text landed.
256-
// We have to read `initiallyDark` via the closure trick — the
257-
// runner's structural `waitForFunction` doesn't expose
258-
// Playwright's optional `arg` parameter, so we split the
259-
// closure capture into two specialised branches at registration
260-
// time (see the if/else below) and unify the OR here.
261-
// Because we can't smuggle `initiallyDark` into the page
262-
// closure, the OR is rewritten as two separate predicates
263-
// outside this function.
264-
return isDark !== isDark || themeToggledRendered; // overwritten below
265-
},
266-
{ timeout: 30_000 },
267-
).catch(() => {
268-
// Predicate above is intentionally never satisfied — it's just
269-
// a placeholder so the type-check is happy. Real waiting happens
270-
// in the branched call below.
271-
});
251+
const isDark =
252+
win.document.documentElement.classList.contains("dark");
253+
const text = (win.document.body.textContent ?? "").toLowerCase();
254+
const themeToggledRendered = text.includes("theme toggled");
255+
// Track (a): class flipped from the initial reading.
256+
// Track (b): the agent's confirmation text landed.
257+
// We have to read `initiallyDark` via the closure trick — the
258+
// runner's structural `waitForFunction` doesn't expose
259+
// Playwright's optional `arg` parameter, so we split the
260+
// closure capture into two specialised branches at registration
261+
// time (see the if/else below) and unify the OR here.
262+
// Because we can't smuggle `initiallyDark` into the page
263+
// closure, the OR is rewritten as two separate predicates
264+
// outside this function.
265+
return isDark !== isDark || themeToggledRendered; // overwritten below
266+
},
267+
{ timeout: 30_000 },
268+
)
269+
.catch(() => {
270+
// Predicate above is intentionally never satisfied — it's just
271+
// a placeholder so the type-check is happy. Real waiting happens
272+
// in the branched call below.
273+
});
272274
// Real wait — branched on initiallyDark so the closure can capture
273275
// it without crossing the page boundary.
274276
if (initiallyDark) {
@@ -284,9 +286,7 @@ export async function assertToggleTheme(page: ConversationPage): Promise<void> {
284286
};
285287
const flipped =
286288
!win.document.documentElement.classList.contains("dark");
287-
const themeToggledRendered = (
288-
win.document.body.textContent ?? ""
289-
)
289+
const themeToggledRendered = (win.document.body.textContent ?? "")
290290
.toLowerCase()
291291
.includes("theme toggled");
292292
return flipped || themeToggledRendered;
@@ -306,9 +306,7 @@ export async function assertToggleTheme(page: ConversationPage): Promise<void> {
306306
};
307307
const flipped =
308308
win.document.documentElement.classList.contains("dark");
309-
const themeToggledRendered = (
310-
win.document.body.textContent ?? ""
311-
)
309+
const themeToggledRendered = (win.document.body.textContent ?? "")
312310
.toLowerCase()
313311
.includes("theme toggled");
314312
return flipped || themeToggledRendered;

showcase/harness/src/probes/scripts/d5-gen-ui-interrupt.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ export function buildInterruptAssertion(
9292
// assistant content for the schedule_meeting tool result.
9393
const scheduledNarration =
9494
text.includes("scheduled") || text.includes("confirmed");
95-
const sample =
96-
(win.document.body.textContent ?? "").slice(-200).replace(/\s+/g, " ").trim();
95+
const sample = (win.document.body.textContent ?? "")
96+
.slice(-200)
97+
.replace(/\s+/g, " ")
98+
.trim();
9799
return { pickedTestid, bookedBadge, scheduledNarration, sample };
98100
});
99101
if (

showcase/harness/src/probes/scripts/d5-reasoning-display.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export const REASONING_SELECTORS = [
5858
'[data-testid="reasoning-content"]',
5959
'[data-testid="reasoning-default"]',
6060
'[data-message-role="reasoning"]',
61-
'text=Thought for',
62-
'text=Thinking…',
61+
"text=Thought for",
62+
"text=Thinking…",
6363
] as const;
6464

6565
async function hasReasoningMessage(page: Page): Promise<boolean> {

showcase/integrations/langgraph-python/src/agents/a2ui_dynamic.py

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
`examples/integrations/langgraph-python/agent/src/a2ui_dynamic_schema.py`):
66
77
- The agent binds an explicit `generate_a2ui` tool. When called, `generate_a2ui`
8-
invokes a secondary LLM bound to `render_a2ui` (tool_choice forced) with the
9-
registered client catalog injected as `copilotkit.context`.
8+
invokes a secondary LLM bound to `_design_a2ui_surface` (tool_choice forced)
9+
with the registered client catalog injected as `copilotkit.context`. The
10+
internal tool is intentionally NOT named `render_a2ui` to avoid the A2UI
11+
middleware's default tool-call intercept (`a2uiToolNames`).
1012
- The tool result returns an `a2ui_operations` container which the A2UI
1113
middleware detects in the tool-call result and forwards to the frontend
1214
renderer.
@@ -37,14 +39,22 @@
3739
CUSTOM_CATALOG_ID = "declarative-gen-ui-catalog"
3840

3941

42+
# Internal tool bound only to the secondary LLM inside `generate_a2ui` for
43+
# structured output. Intentionally NOT named `render_a2ui` because the A2UI
44+
# middleware default-intercepts tool calls by that name from the run's event
45+
# stream and synthesises ACTIVITY_SNAPSHOT events from the LLM's RAW streaming
46+
# args (catalogId + components, before our Python code can validate). That
47+
# bypass is what surfaced the "Cannot create component root without a type"
48+
# infinite-loop on the deployed declarative-gen-ui demo. Renaming sidesteps
49+
# the middleware's intercept list (`a2uiToolNames`).
4050
@lc_tool
41-
def render_a2ui(
51+
def _design_a2ui_surface(
4252
surfaceId: str,
4353
catalogId: str,
4454
components: list[dict],
4555
data: dict | None = None,
4656
) -> str:
47-
"""Render a dynamic A2UI v0.9 surface.
57+
"""Design a dynamic A2UI v0.9 surface.
4858
4959
Args:
5060
surfaceId: Unique surface identifier.
@@ -53,7 +63,26 @@ def render_a2ui(
5363
component must have id "root".
5464
data: Optional initial data model for the surface.
5565
"""
56-
return "rendered"
66+
return "designed"
67+
68+
69+
_GENERATE_A2UI_PROMPT_HEADER = f"""\
70+
You are designing a dynamic A2UI v0.9 surface. Call the `_design_a2ui_surface`
71+
tool with a flat component array.
72+
73+
Hard requirements (failing any of these breaks the renderer — be strict):
74+
- `catalogId` MUST be exactly: "{CUSTOM_CATALOG_ID}"
75+
- `surfaceId` is a short kebab-case identifier (e.g. "kpi-dashboard").
76+
- `components` is a FLAT array. Every entry MUST include both an `id` (unique
77+
string) AND a `component` (string — the catalog component name). The root
78+
entry MUST have `id: "root"` AND a valid `component` field — never emit
79+
a root entry without a component type.
80+
- Container components (Row, Column, Card) reference children by id via their
81+
`children` (array of strings) or `child` (single string) prop. Do NOT inline
82+
children objects. Define each child as its own entry in the flat array and
83+
reference its id.
84+
- Use only catalog component names listed in the schema below.
85+
"""
5786

5887

5988
@tool()
@@ -69,34 +98,55 @@ def generate_a2ui(runtime: ToolRuntime[Any]) -> str:
6998
# Pull the A2UI component schema + usage guidelines from the runtime's
7099
# `copilotkit.context` (the runtime injects them automatically when the
71100
# frontend registers a catalog via `<CopilotKit a2ui={{ catalog }}>`).
101+
# We prepend an explicit instruction header because the runtime context
102+
# alone leaves room for the LLM to hallucinate catalog IDs or emit a root
103+
# component without a `component` field — both surface as "Cannot create
104+
# component root without a type" infinite-loops in the renderer.
72105
context_entries = runtime.state.get("copilotkit", {}).get("context", [])
73106
context_text = "\n\n".join(
74107
entry.get("value", "")
75108
for entry in context_entries
76109
if isinstance(entry, dict) and entry.get("value")
77110
)
78111

112+
prompt = f"{_GENERATE_A2UI_PROMPT_HEADER}\n\n{context_text}".strip()
113+
79114
model = ChatOpenAI(model="gpt-4.1")
80115
model_with_tool = model.bind_tools(
81-
[render_a2ui],
82-
tool_choice="render_a2ui",
116+
[_design_a2ui_surface],
117+
tool_choice="_design_a2ui_surface",
83118
)
84119

85120
response = model_with_tool.invoke(
86-
[SystemMessage(content=context_text), *messages],
121+
[SystemMessage(content=prompt), *messages],
87122
)
88123

89124
if not response.tool_calls:
90-
return json.dumps({"error": "LLM did not call render_a2ui"})
125+
return json.dumps({"error": "LLM did not call _design_a2ui_surface"})
91126

92127
tool_call = response.tool_calls[0]
93128
args = tool_call["args"]
94129

95130
surface_id = args.get("surfaceId", "dynamic-surface")
96-
catalog_id = args.get("catalogId", CUSTOM_CATALOG_ID)
131+
# Force the canonical catalog ID — the secondary LLM has been observed
132+
# hallucinating IDs from sibling demos when context is sparse.
133+
catalog_id = CUSTOM_CATALOG_ID
97134
components = args.get("components", [])
98135
data = args.get("data", {})
99136

137+
# Defensive: every component must have an `id` AND a `component` field, or
138+
# the frontend renderer throws "Cannot create component <id> without a
139+
# type" and never recovers. Drop malformed entries so a valid surface
140+
# still renders.
141+
components = [
142+
c for c in components
143+
if isinstance(c, dict) and c.get("id") and c.get("component")
144+
]
145+
if not any(c.get("id") == "root" for c in components):
146+
return json.dumps({
147+
"error": "LLM produced no valid root component for the A2UI surface."
148+
})
149+
100150
ops = [
101151
a2ui.create_surface(surface_id, catalog_id=catalog_id),
102152
a2ui.update_components(surface_id, components),

showcase/integrations/langgraph-python/src/agents/a2ui_fixed.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def display_flight(origin: str, destination: str, airline: str, price: str) -> s
5353
5454
Use short airport codes (e.g. "SFO", "JFK") for origin/destination and a
5555
price string like "$289".
56+
57+
After this tool returns, the flight card is already rendered to the user
58+
via the A2UI surface — the JSON returned here is the surface descriptor
59+
the renderer consumes, NOT a status code. Do NOT call this tool again
60+
for the same flight (the user already sees the card). Reply with one
61+
short confirmation sentence and stop.
5662
"""
5763
# @region[backend-render-operations]
5864
# The A2UI middleware detects the `a2ui_operations` container in this
@@ -86,7 +92,10 @@ def display_flight(origin: str, destination: str, airline: str, price: str) -> s
8692
middleware=[CopilotKitMiddleware()],
8793
system_prompt=(
8894
"You help users find flights. When asked about a flight, call "
89-
"display_flight with origin, destination, airline, and price. "
90-
"Keep any chat reply to one short sentence."
95+
"`display_flight` exactly ONCE with origin, destination, airline, "
96+
"and price. The tool's JSON return value is an A2UI surface "
97+
"descriptor — the flight card is already rendered to the user; do "
98+
"NOT call `display_flight` again for the same trip. After the tool "
99+
"returns, reply with one short confirmation sentence and stop."
91100
),
92101
)

0 commit comments

Comments
 (0)