Skip to content

Commit 4c8a2ef

Browse files
committed
fix(showcase): align built-in-agent D5 wiring with LGP fixture tool names
Seven D5 features fail because built-in-agent demo pages use different tool names and hook types than the LangGraph-Python reference agent that the D5 fixtures were recorded against. - tool-rendering: switch from useComponent("weather") to useRenderTool("get_weather") with correct {parameters, result, status} shape - gen-ui-tool-based: rename useComponent("haiku") to useComponent("generate_haiku"), fix HaikuCard to accept args as direct props instead of {status, result, parameters} - hitl-in-chat: create /demos/hitl-in-chat route (was missing); reuses TimePickerCard from hitl-in-chat-booking with book_call tool - shared-state: add set_notes server tool to baseServerTools so the fixture's set_notes call has a backend handler - subagents: rename delegate_to_planner/researcher to research_agent/writing_agent/critique_agent matching LGP tool names - subagents page: update useComponent registrations and DelegationCard for the renamed tools
1 parent f5c97ae commit 4c8a2ef

6 files changed

Lines changed: 228 additions & 50 deletions

File tree

showcase/integrations/built-in-agent/src/app/demos/gen-ui-tool-based/page.tsx

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,80 @@ export default function GenUiToolBased() {
1616

1717
function Demo() {
1818
useComponent({
19-
name: "haiku",
19+
name: "generate_haiku",
2020
render: HaikuCard,
2121
});
2222

2323
return (
2424
<main className="p-8">
2525
<h1 className="text-2xl font-semibold mb-4">Tool-Based Generative UI</h1>
2626
<p className="text-sm opacity-70 mb-6">
27-
Try: &ldquo;Write me a haiku about morning dew.&rdquo; The agent calls
27+
Try: &ldquo;Write me a haiku about nature.&rdquo; The agent calls
2828
the
29-
<code className="mx-1 px-1 bg-gray-100 rounded">haiku</code>
29+
<code className="mx-1 px-1 bg-gray-100 rounded">generate_haiku</code>
3030
tool and the result renders inline as a typed card.
3131
</p>
3232
<CopilotChat />
3333
</main>
3434
);
3535
}
3636

37+
/**
38+
* HaikuCard — rendered by `useComponent({ name: "generate_haiku" })`.
39+
*
40+
* `useComponent` passes tool-call arguments directly as React props (via
41+
* `useFrontendTool`'s `render: ({ args }) => <Component {...args} />`).
42+
* The D5 fixture sends: { japanese, english, image_name, gradient }.
43+
*/
3744
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3845
function HaikuCard(props: any) {
39-
const { status, result, parameters } = props;
46+
const japaneseLines: string[] = props.japanese ?? [];
47+
const englishLines: string[] = props.english ?? props.lines ?? [];
48+
const gradient: string | undefined = props.gradient;
49+
const topic: string | undefined = props.topic;
4050

41-
if (status !== "complete" || !result) {
51+
const hasContent =
52+
japaneseLines.length > 0 || englishLines.length > 0;
53+
54+
if (!hasContent) {
4255
return (
43-
<div className="border rounded p-3 my-2 opacity-70 text-sm">
44-
Composing haiku
45-
{parameters?.topic ? ` about ${parameters.topic}…` : "…"}
56+
<div
57+
data-testid="haiku-card"
58+
className="border rounded p-3 my-2 opacity-70 text-sm"
59+
>
60+
Composing haiku{topic ? ` about ${topic}` : ""}
4661
</div>
4762
);
4863
}
4964

50-
let data: { topic?: string; lines?: string[] } = {};
51-
try {
52-
data = JSON.parse(result);
53-
} catch {
54-
// leave empty on parse failure
55-
}
56-
const lines = data.lines ?? [];
57-
5865
return (
59-
<div className="border rounded p-4 my-2 bg-amber-50">
60-
<div className="font-medium mb-2">Haiku — {data.topic ?? ""}</div>
66+
<div
67+
data-testid="haiku-card"
68+
className="border rounded p-4 my-2 bg-amber-50"
69+
style={gradient ? { background: gradient } : undefined}
70+
>
71+
<div className="font-medium mb-2">
72+
Haiku{topic ? ` — ${topic}` : ""}
73+
</div>
74+
{japaneseLines.length > 0 && (
75+
<div className="mb-2">
76+
{japaneseLines.map((line: string, i: number) => (
77+
<div
78+
key={i}
79+
data-testid="haiku-japanese-line"
80+
className="text-lg leading-relaxed"
81+
>
82+
{line}
83+
</div>
84+
))}
85+
</div>
86+
)}
6187
<div className="font-serif italic whitespace-pre-line text-lg leading-relaxed">
62-
{lines.join("\n")}
88+
{englishLines.map((line: string, i: number) => (
89+
<div key={i} data-testid="haiku-english-line">
90+
{line}
91+
</div>
92+
))}
6393
</div>
6494
</div>
6595
);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"use client";
2+
3+
/**
4+
* In-Chat HITL (book_call) demo.
5+
*
6+
* Mirrors the hitl-in-chat-booking page at `/demos/hitl-in-chat-booking`
7+
* but exposed at the canonical `/demos/hitl-in-chat` route that the D5
8+
* harness expects. The `book_call` tool presents a TimePickerCard with
9+
* fixed time slots; the user picks one, and the agent acknowledges.
10+
*/
11+
12+
import {
13+
CopilotKitProvider,
14+
CopilotChat,
15+
useHumanInTheLoop,
16+
} from "@copilotkit/react-core/v2";
17+
import { z } from "zod";
18+
import { TimePickerCard, TimeSlot } from "../hitl-in-chat-booking/time-picker-card";
19+
20+
const DEFAULT_SLOTS: TimeSlot[] = [
21+
{ label: "Tomorrow 10:00 AM", iso: "2026-04-30T10:00:00-07:00" },
22+
{ label: "Tomorrow 2:00 PM", iso: "2026-04-30T14:00:00-07:00" },
23+
{ label: "Monday 9:00 AM", iso: "2026-05-04T09:00:00-07:00" },
24+
{ label: "Monday 3:30 PM", iso: "2026-05-04T15:30:00-07:00" },
25+
];
26+
27+
export default function HitlInChat() {
28+
return (
29+
<CopilotKitProvider runtimeUrl="/api/copilotkit" useSingleEndpoint>
30+
<Demo />
31+
</CopilotKitProvider>
32+
);
33+
}
34+
35+
function Demo() {
36+
useHumanInTheLoop({
37+
name: "book_call",
38+
description:
39+
"Ask the user to pick a time slot for a call. The picker UI presents fixed candidate slots; the user's choice is returned to the agent.",
40+
parameters: z.object({
41+
topic: z
42+
.string()
43+
.describe("What the call is about (e.g. 'Intro with sales')"),
44+
attendee: z
45+
.string()
46+
.describe("Who the call is with (e.g. 'Alice from Sales')"),
47+
}),
48+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
49+
render: ({ args, status, respond }: any) => (
50+
<TimePickerCard
51+
topic={args?.topic ?? "a call"}
52+
attendee={args?.attendee}
53+
slots={DEFAULT_SLOTS}
54+
status={status}
55+
onSubmit={(result) => respond?.(result)}
56+
/>
57+
),
58+
});
59+
60+
return (
61+
<main className="p-8">
62+
<h1 className="text-2xl font-semibold mb-4">In-Chat Booking (HITL)</h1>
63+
<p className="text-sm opacity-70 mb-6">
64+
Try: &ldquo;Book a 30-minute onboarding call for Alice.&rdquo; The
65+
agent renders an inline time picker; pick a slot to confirm.
66+
</p>
67+
<CopilotChat />
68+
</main>
69+
);
70+
}

showcase/integrations/built-in-agent/src/app/demos/subagents/page.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ export default function Subagents() {
1616

1717
function Demo() {
1818
useComponent({
19-
name: "delegate_to_planner",
19+
name: "research_agent",
2020
render: DelegationCard,
2121
});
2222
useComponent({
23-
name: "delegate_to_researcher",
23+
name: "writing_agent",
24+
render: DelegationCard,
25+
});
26+
useComponent({
27+
name: "critique_agent",
2428
render: DelegationCard,
2529
});
2630

@@ -29,11 +33,14 @@ function Demo() {
2933
<h1 className="text-2xl font-semibold mb-4">Sub-Agents</h1>
3034
<p className="text-sm opacity-70 mb-6">
3135
The main agent delegates tasks to subagents via{" "}
32-
<code className="px-1 bg-gray-100 rounded">delegate_to_planner</code>
36+
<code className="px-1 bg-gray-100 rounded">research_agent</code>
37+
{" / "}
38+
<code className="px-1 bg-gray-100 rounded">writing_agent</code>
3339
{" / "}
34-
<code className="px-1 bg-gray-100 rounded">delegate_to_researcher</code>
40+
<code className="px-1 bg-gray-100 rounded">critique_agent</code>
3541
. Each delegation runs a nested <code>chat()</code> with its own system
36-
prompt. Try: &ldquo;Plan a 2-day trip to Tokyo with key sights.&rdquo;
42+
prompt. Try: &ldquo;Research the benefits of remote work and draft a
43+
one-paragraph summary.&rdquo;
3744
</p>
3845
<CopilotChat />
3946
</main>
@@ -44,7 +51,9 @@ function Demo() {
4451
function DelegationCard(props: any) {
4552
const { name, status, parameters, result } = props;
4653
const role =
47-
typeof name === "string" ? name.replace(/^delegate_to_/, "") : "subagent";
54+
typeof name === "string"
55+
? name.replace(/_agent$/, "").replace(/_/g, " ")
56+
: "subagent";
4857

4958
let parsed: { role?: string; text?: string } = {};
5059
if (status === "complete" && typeof result === "string") {

showcase/integrations/built-in-agent/src/app/demos/tool-rendering/page.tsx

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import {
44
CopilotKitProvider,
55
CopilotChat,
6-
useComponent,
6+
useRenderTool,
77
useDefaultRenderTool,
88
} from "@copilotkit/react-core/v2";
9+
import { z } from "zod";
910

1011
export default function ToolRendering() {
1112
return (
@@ -17,9 +18,18 @@ export default function ToolRendering() {
1718

1819
function Demo() {
1920
// @region[render-weather-tool]
20-
useComponent({
21-
name: "weather",
22-
render: WeatherCard,
21+
useRenderTool({
22+
name: "get_weather",
23+
parameters: z.object({ location: z.string() }),
24+
render: ({ parameters, result, status }) => {
25+
return (
26+
<WeatherCard
27+
loading={status !== "complete"}
28+
parameters={parameters}
29+
result={result}
30+
/>
31+
);
32+
},
2333
});
2434
// @endregion[render-weather-tool]
2535

@@ -34,7 +44,7 @@ function Demo() {
3444
<h1 className="text-2xl font-semibold mb-4">Tool Rendering</h1>
3545
<p className="text-sm opacity-70 mb-6">
3646
Try: &ldquo;What&apos;s the weather in Tokyo?&rdquo; The
37-
<code className="mx-1 px-1 bg-gray-100 rounded">weather</code>tool
47+
<code className="mx-1 px-1 bg-gray-100 rounded">get_weather</code>tool
3848
renders with a custom card; everything else falls back to a generic
3949
renderer.
4050
</p>
@@ -43,44 +53,69 @@ function Demo() {
4353
);
4454
}
4555

46-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
47-
function WeatherCard(props: any) {
48-
const { status, result } = props;
49-
if (status !== "complete" || !result) {
56+
/**
57+
* WeatherCard — rendered via `useRenderTool({ name: "get_weather" })`.
58+
*
59+
* Receives `loading`, `parameters` (tool args), and `result` (tool output).
60+
* The server tool returns: { city, temperature, humidity, wind_speed, conditions }.
61+
*/
62+
function WeatherCard({
63+
loading,
64+
parameters,
65+
result,
66+
}: {
67+
loading: boolean;
68+
parameters?: { location?: string };
69+
result?: unknown;
70+
}) {
71+
if (loading || !result) {
5072
return (
5173
<div className="border rounded p-3 my-2 opacity-70 text-sm">
5274
Fetching weather…
5375
</div>
5476
);
5577
}
78+
5679
let data: {
5780
city?: string;
81+
temperature?: number;
5882
tempF?: number;
5983
condition?: string;
84+
conditions?: string;
6085
humidity?: number;
86+
wind_speed?: number;
6187
} = {};
6288
try {
63-
data = JSON.parse(result);
89+
data =
90+
typeof result === "string" ? JSON.parse(result) : (result as typeof data);
6491
} catch {
6592
// Leave data empty on parse failure
6693
}
67-
const humidityPct =
68-
data.humidity != null ? `${Math.round(data.humidity * 100)}%` : "—";
94+
const city = data.city ?? parameters?.location ?? "—";
95+
const temp = data.temperature ?? data.tempF;
96+
const condition = data.conditions ?? data.condition;
97+
const humidityVal = data.humidity;
98+
const humidityStr =
99+
humidityVal != null
100+
? humidityVal < 1
101+
? `${Math.round(humidityVal * 100)}%`
102+
: `${humidityVal}%`
103+
: "—";
69104
return (
70-
<div className="border rounded p-3 my-2">
71-
<div className="font-medium">Weather in {data.city ?? "—"}</div>
105+
<div data-testid="weather-card" className="border rounded p-3 my-2">
106+
<div className="font-medium">Weather in {city}</div>
72107
<div className="grid grid-cols-3 gap-2 text-sm mt-2">
73108
<div>
74109
<div className="opacity-60">Temp</div>
75-
<div>{data.tempF != null ? `${data.tempF}°F` : "—"}</div>
110+
<div>{temp != null ? `${temp}°F` : "—"}</div>
76111
</div>
77112
<div>
78113
<div className="opacity-60">Condition</div>
79-
<div>{data.condition ?? "—"}</div>
114+
<div>{condition ?? "—"}</div>
80115
</div>
81116
<div>
82117
<div className="opacity-60">Humidity</div>
83-
<div>{humidityPct}</div>
118+
<div>{humidityStr}</div>
84119
</div>
85120
</div>
86121
</div>

showcase/integrations/built-in-agent/src/lib/factory/server-tools.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,30 @@ export const rollDiceTool = toolDefinition({
119119
result: Math.floor(Math.random() * Math.max(2, sides)) + 1,
120120
}));
121121

122+
// Tool for the shared-state-read-write demo. The `set_notes` tool
123+
// updates the `notes` slot in shared state, mirroring the LangGraph
124+
// Python reference agent's `set_notes` tool. The actual state mutation
125+
// happens client-side when the tool result is returned; here we just
126+
// echo the notes back so the runtime/frontend can handle it.
127+
export const setNotesTool = toolDefinition({
128+
name: "set_notes",
129+
description:
130+
"Replace the notes array in shared state with the full updated list. " +
131+
"Use this tool whenever the user asks you to 'remember' something, or " +
132+
"when you have an observation about the user worth surfacing in the " +
133+
"UI's notes panel. Always pass the FULL notes list (existing notes + " +
134+
"any new ones), not a diff. Keep each note short (< 120 chars).",
135+
inputSchema: z.object({
136+
notes: z.array(z.string()).describe("The complete updated list of notes"),
137+
}),
138+
}).server(async ({ notes }) => ({ success: true, notes }));
139+
122140
export const baseServerTools = [
123141
weatherTool,
124142
haikuTool,
125143
getWeatherTool,
126144
searchFlightsTool,
127145
getStockPriceTool,
128146
rollDiceTool,
147+
setNotesTool,
129148
] as const;

0 commit comments

Comments
 (0)