Skip to content

Commit 8ad38cf

Browse files
committed
feat(showcase/langgraph-python): Generative UI tool-rendering family
Five demos exercising the per-tool / catch-all / agent-state rendering patterns. The three tool-rendering cells share the tool_rendering_agent graph; they differ only in how the frontend renders the same tool calls. - tool-rendering (Tool Rendering - Specific) — per-tool useRenderTool for get_weather + search_flights, plus a useDefaultRenderTool wildcard for everything else. - tool-rendering-default-catchall (Tool Rendering - Default) — single shadcn-styled wildcard via useDefaultRenderTool. Without registering *some* renderer the runtime has no `*` entry and tool calls render invisibly; this demo shows the minimum-viable shape. - tool-rendering-custom-catchall (Tool Rendering - Custom Default) — same single-wildcard shape, branded with a custom card. Backend system prompt (src/agents/tool_rendering_agent.py) defaults to ONE tool per user question. Chaining is opt-in via a "Chain tools" suggestion that triggers an explicit-ask exception in the prompt — the previous default-on-chaining generated extra unsolicited tool-call cards on every turn. - gen-ui-tool-based (Generative UI: useComponent) — useComponent for render_bar_chart + render_pie_chart with Zod schemas; backend has tools=[] and the runtime injects the tools. - gen-ui-agent (Generative UI: Agent State) — agent-state-driven step list. The Python graph plans steps via a `set_steps` tool that returns Command(update={"steps": …}); the frontend reads via useAgent({updates: [OnStateChanged]}) + a custom MessageList that renders steps inside CopilotChat's messageView.children slot. Removed: src/app/demos/{tool-rendering,gen-ui-agent}/agent.py — TODO stubs; real graphs live in src/agents/.
1 parent 9e40b1b commit 8ad38cf

24 files changed

Lines changed: 619 additions & 278 deletions

File tree

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

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,47 +19,37 @@
1919
from langchain_openai import ChatOpenAI
2020
from copilotkit import CopilotKitMiddleware
2121

22-
# Multi-tool chaining prompt.
22+
# One-tool-per-question prompt.
2323
#
24-
# The goal of this demo is to surface MULTIPLE tool-call cards per turn so
25-
# the rendering patterns (per-tool + catch-all) get exercised visibly. The
26-
# prompt nudges the model toward an explore-then-enrich pattern (e.g.
27-
# `get_weather("Tokyo")` -> `search_flights(..., "Tokyo")`) without forcing
28-
# a rigid recipe: we describe the *habit*, not a chain.
24+
# This backend serves the tool-rendering demos, whose JOB is to show the
25+
# rendering patterns (per-tool, catch-all, default fallback). One tool
26+
# call per user turn is enough to demonstrate them — chained calls just
27+
# clutter the chat and surprise users.
2928
SYSTEM_PROMPT = (
3029
"You are a helpful travel & lifestyle concierge. You have mock tools "
31-
"for weather, flights, stock prices, and dice rolls - they all return "
32-
"fake data, so call them liberally.\n\n"
33-
"Your habit is to CHAIN tools when one answer naturally invites another. "
34-
"For a single user question, call at least TWO tools in succession when "
35-
"the topic allows before composing your final reply. Examples of "
36-
"helpful chains you should default to:\n"
37-
" - 'What's the weather in Tokyo?' -> call get_weather('Tokyo'), then "
38-
"call search_flights(origin='SFO', destination='Tokyo') so the user "
39-
"also sees how to get there.\n"
40-
" - 'How is AAPL doing?' -> call get_stock_price('AAPL'), then call "
41-
"get_stock_price on a related ticker (e.g. 'MSFT' or 'GOOGL') for "
42-
"comparison.\n"
43-
" - 'Roll a d20' -> call roll_dice(20), then call roll_dice again with "
44-
"a different number of sides so the user sees a contrast.\n"
45-
" - 'Find flights from SFO to JFK' -> call search_flights, then call "
46-
"get_weather on the destination city.\n\n"
47-
"Only skip chaining when the user has clearly asked for a single, "
48-
"atomic answer and more tool calls would feel intrusive. Never "
49-
"fabricate data that a tool could provide."
30+
"for weather, flights, stock prices, and dice rolls — they all return "
31+
"fake data.\n\n"
32+
"Routing rules:\n"
33+
" - Weather questions → call `get_weather` with the location.\n"
34+
" - Flight questions → call `search_flights` with origin and "
35+
"destination (default origin to 'SFO' if the user only names a "
36+
"destination).\n"
37+
" - Stock questions → call `get_stock_price` with the ticker.\n"
38+
" - Dice rolls → call `roll_dice` with the requested sides.\n"
39+
" - Anything else → reply in plain text.\n\n"
40+
"By default, call exactly ONE tool per user question and do NOT chain "
41+
"tools or fetch related data the user didn't ask for. The ONLY "
42+
"exception is when the user explicitly asks you to chain or call "
43+
"multiple tools in a single turn — then call each tool the user "
44+
"requested. After tools return, write one short sentence summarizing "
45+
"the result. Never fabricate data a tool could provide."
5046
)
5147

5248

5349
# @region[weather-tool-backend]
5450
@tool
5551
def get_weather(location: str) -> dict:
56-
"""Get the current weather for a given location.
57-
58-
Useful on its own for weather questions, and a great companion to
59-
`search_flights` - always consider checking the weather at a
60-
destination the user is flying to, and checking flights to any
61-
city whose weather the user has just asked about.
62-
"""
52+
"""Get the current weather for a given location."""
6353
return {
6454
"city": location,
6555
"temperature": 68,
@@ -72,13 +62,7 @@ def get_weather(location: str) -> dict:
7262

7363
@tool
7464
def search_flights(origin: str, destination: str) -> dict:
75-
"""Search mock flights from an origin airport to a destination airport.
76-
77-
Pairs naturally with `get_weather`: after searching flights, check
78-
the weather at the destination so the user can plan. When the user
79-
mentions a city without a matching origin, default the origin to
80-
'SFO'.
81-
"""
65+
"""Search mock flights from an origin airport to a destination airport."""
8266
return {
8367
"origin": origin,
8468
"destination": destination,
@@ -110,12 +94,7 @@ def search_flights(origin: str, destination: str) -> dict:
11094

11195
@tool
11296
def get_stock_price(ticker: str) -> dict:
113-
"""Get a mock current price for a stock ticker.
114-
115-
When the user asks about a single ticker, consider also pulling a
116-
related ticker for context (e.g. if they ask about 'AAPL', also
117-
fetch 'MSFT' or 'GOOGL' so the reply can compare).
118-
"""
97+
"""Get a mock current price for a stock ticker."""
11998
return {
12099
"ticker": ticker.upper(),
121100
"price_usd": round(100 + randint(0, 400) + randint(0, 99) / 100, 2),
@@ -125,12 +104,7 @@ def get_stock_price(ticker: str) -> dict:
125104

126105
@tool
127106
def roll_dice(sides: int = 6) -> dict:
128-
"""Roll a single die with the given number of sides.
129-
130-
When the user asks for a roll, consider rolling twice with different
131-
numbers of sides so the reply can show a contrast (e.g. a d6 AND a
132-
d20).
133-
"""
107+
"""Roll a single die with the given number of sides."""
134108
return {"sides": sides, "result": randint(1, max(2, sides))}
135109

136110

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
# Agentic Generative UI
22

3-
## What This Demo Shows
3+
The agent renders custom UI as it works through long-running tasks, streaming
4+
status updates and intermediate results into the chat.
45

5-
Long-running agent tasks with generated UI
6+
Frontend uses `useAgentRender` to map agent-emitted UI types to React
7+
components, so the agent has full control over what appears in the transcript.
68

7-
## How to Interact
8-
9-
Try asking your Copilot to:
10-
11-
- "What's the weather like in San Francisco?"
12-
- "Show me current conditions in multiple cities"
13-
- "What should I wear in Tokyo today?"
14-
15-
The agent generates custom UI components in the chat stream itself, rendering weather cards and interactive elements directly as part of its response.
16-
17-
## Technical Details
18-
19-
- **Agent-driven Generative UI** differs from tool-based — the agent directly emits UI components in the response stream
20-
- The agent's response includes structured data that CopilotKit renders as React components inline with the chat
21-
- Unlike tool-based gen UI, this approach gives the agent full control over when and how UI appears
22-
- The frontend registers renderers that map agent output types to React components
9+
The canonical description lives in the showcase manifest; this README is just
10+
a developer note alongside the demo source.

showcase/integrations/langgraph-python/src/app/demos/gen-ui-agent/agent.py

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { InlineAgentStateCard, type Step } from "./InlineAgentStateCard";
5+
6+
export function MessageListWithState({
7+
messageElements,
8+
interruptElement,
9+
steps,
10+
status,
11+
}: {
12+
messageElements: React.ReactNode;
13+
interruptElement: React.ReactNode;
14+
steps: Step[];
15+
status: "inProgress" | "complete";
16+
}) {
17+
return (
18+
<div data-testid="copilot-message-list" className="flex flex-col">
19+
{messageElements}
20+
{steps.length > 0 && (
21+
<InlineAgentStateCard steps={steps} status={status} />
22+
)}
23+
{interruptElement}
24+
</div>
25+
);
26+
}

showcase/integrations/langgraph-python/src/app/demos/gen-ui-agent/page.tsx

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"use client";
22

33
import React from "react";
4-
import { CopilotKit } from "@copilotkit/react-core";
54
import {
65
CopilotChat,
6+
CopilotKit,
77
useAgent,
88
UseAgentUpdate,
9-
useConfigureSuggestions,
109
} from "@copilotkit/react-core/v2";
11-
import { InlineAgentStateCard } from "./InlineAgentStateCard";
1210
import type { Step } from "./InlineAgentStateCard";
11+
import { MessageListWithState } from "./message-list-with-state";
12+
import { useSuggestions } from "./suggestions";
1313

1414
/**
1515
* Agentic Generative UI — In-Chat State Rendering
@@ -51,24 +51,7 @@ function Chat() {
5151
updates: [UseAgentUpdate.OnStateChanged],
5252
});
5353

54-
useConfigureSuggestions({
55-
suggestions: [
56-
{
57-
title: "Plan a product launch",
58-
message: "Plan a product launch for a new mobile app.",
59-
},
60-
{
61-
title: "Organize a team offsite",
62-
message: "Organize a three-day engineering team offsite.",
63-
},
64-
{
65-
title: "Research a competitor",
66-
message:
67-
"Research our top competitor and summarize their strengths and weaknesses.",
68-
},
69-
],
70-
available: "always",
71-
});
54+
useSuggestions();
7255

7356
const steps = (agent.state as AgentState | undefined)?.steps ?? [];
7457
const status = agent.isRunning ? "inProgress" : "complete";
@@ -79,13 +62,12 @@ function Chat() {
7962
className="h-full rounded-2xl"
8063
messageView={{
8164
children: ({ messageElements, interruptElement }) => (
82-
<div data-testid="copilot-message-list" className="flex flex-col">
83-
{messageElements}
84-
{steps.length > 0 && (
85-
<InlineAgentStateCard steps={steps} status={status} />
86-
)}
87-
{interruptElement}
88-
</div>
65+
<MessageListWithState
66+
messageElements={messageElements}
67+
interruptElement={interruptElement}
68+
steps={steps}
69+
status={status}
70+
/>
8971
),
9072
}}
9173
/>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client";
2+
3+
import { useConfigureSuggestions } from "@copilotkit/react-core/v2";
4+
5+
export function useSuggestions() {
6+
useConfigureSuggestions({
7+
suggestions: [
8+
{
9+
title: "Plan a product launch",
10+
message: "Plan a product launch for a new mobile app.",
11+
},
12+
{
13+
title: "Organize a team offsite",
14+
message: "Organize a three-day engineering team offsite.",
15+
},
16+
{
17+
title: "Research a competitor",
18+
message:
19+
"Research our top competitor and summarize their strengths and weaknesses.",
20+
},
21+
],
22+
available: "always",
23+
});
24+
}
Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,10 @@
11
# Tool-Based Generative UI
22

3-
## What This Demo Shows
3+
The agent calls a backend tool that returns structured data; the frontend
4+
renders that tool result as a custom React component instead of plain text.
45

5-
Agent uses tools to trigger UI generation
6+
`useRenderTool` maps each tool name to a renderer that receives `args`,
7+
`result`, and `status`, so the UI can show loading and complete states.
68

7-
## How to Interact
8-
9-
Try asking your Copilot to:
10-
11-
- "What's the weather forecast for this week in San Francisco?"
12-
- "Show me the weather in Paris"
13-
- "Compare the weather in Tokyo and London"
14-
15-
The agent generates structured data via tools, and the frontend renders it as rich UI components.
16-
17-
## Technical Details
18-
19-
What's happening technically:
20-
21-
- **Generative UI** means the agent's tool calls produce structured data that the frontend renders as custom React components
22-
- Unlike plain text responses, the agent returns tool results with typed parameters (city, temperature, conditions)
23-
- `useRenderTool` maps each tool name to a React component, so `get_weather` renders a weather card with icons, temperature displays, and forecast details
24-
- The agent decides when to call the tool based on context — it can mix tool-based UI generation with regular text responses
25-
- This pattern enables agents to create dynamic, data-driven interfaces on demand
9+
The canonical description lives in the showcase manifest; this README is just
10+
a developer note alongside the demo source.

showcase/integrations/langgraph-python/src/app/demos/gen-ui-tool-based/page.tsx

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"use client";
22

33
import React from "react";
4-
import { CopilotKit } from "@copilotkit/react-core";
54
import {
65
CopilotChat,
6+
CopilotKit,
77
useComponent,
8-
useConfigureSuggestions,
98
} from "@copilotkit/react-core/v2";
109
import { BarChart, barChartPropsSchema } from "./bar-chart";
1110
import { PieChart, pieChartPropsSchema } from "./pie-chart";
11+
import { useSuggestions } from "./suggestions";
1212

1313
export default function ControlledGenUiDemo() {
1414
return (
@@ -37,23 +37,7 @@ function Chat() {
3737
});
3838
// @endregion[pie-chart-renderer]
3939

40-
useConfigureSuggestions({
41-
suggestions: [
42-
{
43-
title: "Sales bar chart",
44-
message: "Show me a bar chart of quarterly sales for Q1, Q2, Q3, Q4.",
45-
},
46-
{
47-
title: "Traffic pie chart",
48-
message: "Show me a pie chart of website traffic by source.",
49-
},
50-
{
51-
title: "Market share",
52-
message: "Show a pie chart of smartphone market share by brand.",
53-
},
54-
],
55-
available: "always",
56-
});
40+
useSuggestions();
5741

5842
return (
5943
<div className="flex justify-center items-center h-screen w-full">
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use client";
2+
3+
import { useConfigureSuggestions } from "@copilotkit/react-core/v2";
4+
5+
export function useSuggestions() {
6+
useConfigureSuggestions({
7+
suggestions: [
8+
{
9+
title: "Sales bar chart",
10+
message: "Show me a bar chart of quarterly sales for Q1, Q2, Q3, Q4.",
11+
},
12+
{
13+
title: "Traffic pie chart",
14+
message: "Show me a pie chart of website traffic by source.",
15+
},
16+
{
17+
title: "Market share",
18+
message: "Show a pie chart of smartphone market share by brand.",
19+
},
20+
],
21+
available: "always",
22+
});
23+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from "react";
2+
3+
// Inline-cloned shadcn/ui <Badge />. Plain Tailwind classes — no `cn()`,
4+
// no `cva`. Local to this demo only.
5+
6+
type Variant = "default" | "secondary" | "outline" | "success" | "warning";
7+
8+
const variantClasses: Record<Variant, string> = {
9+
default: "border-transparent bg-neutral-900 text-neutral-50",
10+
secondary: "border-transparent bg-neutral-100 text-neutral-700",
11+
outline: "border-neutral-200 text-neutral-900",
12+
success: "border-transparent bg-emerald-100 text-emerald-700",
13+
warning: "border-transparent bg-amber-100 text-amber-700",
14+
};
15+
16+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
17+
variant?: Variant;
18+
}
19+
20+
export function Badge({
21+
className = "",
22+
variant = "default",
23+
...props
24+
}: BadgeProps) {
25+
return (
26+
<div
27+
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider transition-colors ${variantClasses[variant]} ${className}`}
28+
{...props}
29+
/>
30+
);
31+
}

0 commit comments

Comments
 (0)