Skip to content

Commit e8eaace

Browse files
committed
Merge slot B5 into blitz/lgp-genuine-pass/integration
2 parents 4097b16 + 0539593 commit e8eaace

13 files changed

Lines changed: 937 additions & 311 deletions

File tree

packages/react-core/src/v2/hooks/use-default-render-tool.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function useDefaultRenderTool(
6363
);
6464
}
6565

66-
function DefaultToolCallRenderer({
66+
export function DefaultToolCallRenderer({
6767
name,
6868
parameters,
6969
status,
@@ -86,6 +86,11 @@ function DefaultToolCallRenderer({
8686

8787
return (
8888
<div
89+
data-testid="copilot-tool-render"
90+
data-tool-name={name}
91+
data-status={statusString}
92+
data-args={safeStringifyForAttr(parameters)}
93+
data-result={safeStringifyForAttr(result)}
8994
style={{
9095
marginTop: "8px",
9196
paddingBottom: "8px",
@@ -150,6 +155,7 @@ function DefaultToolCallRenderer({
150155
}}
151156
/>
152157
<span
158+
data-testid="copilot-tool-render-name"
153159
style={{
154160
fontSize: "13px",
155161
fontWeight: 600,
@@ -164,6 +170,7 @@ function DefaultToolCallRenderer({
164170
</div>
165171

166172
<span
173+
data-testid="copilot-tool-render-status"
167174
style={{
168175
display: "inline-flex",
169176
alignItems: "center",
@@ -252,3 +259,13 @@ function DefaultToolCallRenderer({
252259
</div>
253260
);
254261
}
262+
263+
function safeStringifyForAttr(value: unknown): string {
264+
if (value === undefined || value === null) return "";
265+
if (typeof value === "string") return value;
266+
try {
267+
return JSON.stringify(value);
268+
} catch {
269+
return String(value);
270+
}
271+
}

packages/react-core/src/v2/hooks/use-render-tool-call.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurati
66
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
77
import { partialJSONParse } from "@copilotkit/shared";
88
import { ReactToolCallRenderer } from "../types/react-tool-call-renderer";
9+
import { DefaultToolCallRenderer } from "./use-default-render-tool";
910

1011
export interface UseRenderToolCallProps {
1112
toolCall: ToolCall;
@@ -153,11 +154,12 @@ export function useRenderToolCall() {
153154
exactMatches[0] ||
154155
renderToolCalls.find((rc) => rc.name === "*");
155156

156-
if (!renderConfig) {
157-
return null;
158-
}
159-
160-
const RenderComponent = renderConfig.render;
157+
// Fall back to the framework's built-in default tool-call renderer
158+
// when neither a per-tool nor a wildcard renderer has been
159+
// registered. This makes "zero custom renderers" demos paint tool
160+
// calls out-of-the-box instead of going invisible.
161+
const RenderComponent = (renderConfig?.render ??
162+
defaultToolCallRenderAdapter) as ReactToolCallRenderer<unknown>["render"];
161163
const isExecuting = executingToolCallIds.has(toolCall.id);
162164

163165
// Use the memoized ToolCallRenderer component to prevent unnecessary re-renders
@@ -176,3 +178,31 @@ export function useRenderToolCall() {
176178

177179
return renderToolCall;
178180
}
181+
182+
// Adapter that bridges the ReactToolCallRenderer signature
183+
// (`{ name, args, status, result, toolCallId }`) to the
184+
// `DefaultToolCallRenderer` signature (`{ name, parameters, status,
185+
// result }`) so the latter can be used as a zero-config fallback when
186+
// no `*` renderer is registered.
187+
function defaultToolCallRenderAdapter(props: {
188+
name: string;
189+
args: unknown;
190+
status: ToolCallStatus;
191+
result: string | undefined;
192+
toolCallId: string;
193+
}): React.ReactElement {
194+
const status =
195+
props.status === ToolCallStatus.Complete
196+
? "complete"
197+
: props.status === ToolCallStatus.Executing
198+
? "executing"
199+
: "inProgress";
200+
return (
201+
<DefaultToolCallRenderer
202+
name={props.name}
203+
parameters={props.args}
204+
status={status}
205+
result={props.result}
206+
/>
207+
);
208+
}

showcase/aimock/d5-all.json

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,207 @@
11
{
22
"_comment": "Bundled D5 (e2e-deep) fixtures. Source files: showcase/harness/fixtures/d5/*.json (auto-merged). Uses hasToolResult matching for multi-turn disambiguation. Loaded by aimock before feature-parity.json for match precedence. The first 7 entries below are the open-gen-ui (4) and open-gen-ui-advanced (3) pill fixtures — they emit deterministic generateSandboxedUi tool calls and intentionally appear before everything else so they win first-match-wins ordering against the showcase-assistant 'hi' catch-all in feature-parity.json. Verbatim pill messages are owned by langgraph-python/src/app/demos/open-gen-ui/suggestions.ts and open-gen-ui-advanced/suggestions.ts. Source-of-truth: showcase/harness/fixtures/d5/gen-ui-open.json.",
33
"fixtures": [
4+
{
5+
"_comment": "tool-rendering pill: Chain tools — emit 3 tool calls in one assistant turn (get_weather Tokyo + search_flights SFO->Tokyo + roll_d20=11). MUST appear before the bare 'weather in Tokyo' fixture below; substring match would otherwise leak into this prompt.",
6+
"match": {
7+
"userMessage": "Chain a few tools in this single turn",
8+
"hasToolResult": false
9+
},
10+
"response": {
11+
"toolCalls": [
12+
{
13+
"id": "call_tr_chain_weather_001",
14+
"name": "get_weather",
15+
"arguments": "{\"location\":\"Tokyo\"}"
16+
},
17+
{
18+
"id": "call_tr_chain_flights_001",
19+
"name": "search_flights",
20+
"arguments": "{\"origin\":\"SFO\",\"destination\":\"Tokyo\"}"
21+
},
22+
{
23+
"id": "call_tr_chain_roll_001",
24+
"name": "roll_d20",
25+
"arguments": "{\"value\":11}"
26+
}
27+
]
28+
}
29+
},
30+
{
31+
"match": {
32+
"userMessage": "Chain a few tools in this single turn",
33+
"hasToolResult": true
34+
},
35+
"response": {
36+
"content": "Done — Tokyo is sunny, three flights found, and the d20 came up 11."
37+
}
38+
},
39+
{
40+
"_comment": "tool-rendering pill: Weather in SF — verbatim pill prompt, deterministic San Francisco values.",
41+
"match": {
42+
"userMessage": "What's the weather in San Francisco?",
43+
"hasToolResult": false
44+
},
45+
"response": {
46+
"toolCalls": [
47+
{
48+
"id": "call_tr_weather_sf_001",
49+
"name": "get_weather",
50+
"arguments": "{\"location\":\"San Francisco\"}"
51+
}
52+
]
53+
}
54+
},
55+
{
56+
"match": {
57+
"userMessage": "What's the weather in San Francisco?",
58+
"hasToolResult": true
59+
},
60+
"response": {
61+
"content": "San Francisco is currently 68°F and sunny with light winds."
62+
}
63+
},
64+
{
65+
"_comment": "tool-rendering pill: Find flights — verbatim pill prompt, dedicated search_flights fixture. MUST take precedence over the a2ui beautiful-chat fixture below (which uses the same tool name with non-flight-list args shape).",
66+
"match": {
67+
"userMessage": "Find flights from SFO to JFK.",
68+
"hasToolResult": false
69+
},
70+
"response": {
71+
"toolCalls": [
72+
{
73+
"id": "call_tr_flights_sfo_jfk_001",
74+
"name": "search_flights",
75+
"arguments": "{\"origin\":\"SFO\",\"destination\":\"JFK\"}"
76+
}
77+
]
78+
}
79+
},
80+
{
81+
"match": {
82+
"userMessage": "Find flights from SFO to JFK.",
83+
"hasToolResult": true
84+
},
85+
"response": {
86+
"content": "Three flights from SFO to JFK — United UA231 at 08:15 ($348), Delta DL412 at 11:20 ($312), and JetBlue B6722 at 17:05 ($289)."
87+
}
88+
},
89+
{
90+
"_comment": "tool-rendering pill: Stock price — verbatim pill prompt, AAPL with deterministic price/change.",
91+
"match": {
92+
"userMessage": "What's the current price of AAPL?",
93+
"hasToolResult": false
94+
},
95+
"response": {
96+
"toolCalls": [
97+
{
98+
"id": "call_tr_stock_aapl_001",
99+
"name": "get_stock_price",
100+
"arguments": "{\"ticker\":\"AAPL\"}"
101+
}
102+
]
103+
}
104+
},
105+
{
106+
"match": {
107+
"userMessage": "What's the current price of AAPL?",
108+
"hasToolResult": true
109+
},
110+
"response": {
111+
"content": "AAPL is trading at $338.37, down 2.96% on the day."
112+
}
113+
},
114+
{
115+
"_comment": "tool-rendering pill: Roll a d20 — exactly 5 sequential roll_d20 calls returning [7, 14, 3, 19, 20]. Disambiguated by turnIndex (count of assistant messages so far) so the sequence is stateless across test runs.",
116+
"match": {
117+
"userMessage": "Roll a 20-sided die.",
118+
"turnIndex": 0,
119+
"hasToolResult": false
120+
},
121+
"response": {
122+
"toolCalls": [
123+
{
124+
"id": "call_tr_d20_seq_001",
125+
"name": "roll_d20",
126+
"arguments": "{\"value\":7}"
127+
}
128+
]
129+
}
130+
},
131+
{
132+
"match": {
133+
"userMessage": "Roll a 20-sided die.",
134+
"turnIndex": 1,
135+
"hasToolResult": true
136+
},
137+
"response": {
138+
"toolCalls": [
139+
{
140+
"id": "call_tr_d20_seq_002",
141+
"name": "roll_d20",
142+
"arguments": "{\"value\":14}"
143+
}
144+
]
145+
}
146+
},
147+
{
148+
"match": {
149+
"userMessage": "Roll a 20-sided die.",
150+
"turnIndex": 2,
151+
"hasToolResult": true
152+
},
153+
"response": {
154+
"toolCalls": [
155+
{
156+
"id": "call_tr_d20_seq_003",
157+
"name": "roll_d20",
158+
"arguments": "{\"value\":3}"
159+
}
160+
]
161+
}
162+
},
163+
{
164+
"match": {
165+
"userMessage": "Roll a 20-sided die.",
166+
"turnIndex": 3,
167+
"hasToolResult": true
168+
},
169+
"response": {
170+
"toolCalls": [
171+
{
172+
"id": "call_tr_d20_seq_004",
173+
"name": "roll_d20",
174+
"arguments": "{\"value\":19}"
175+
}
176+
]
177+
}
178+
},
179+
{
180+
"match": {
181+
"userMessage": "Roll a 20-sided die.",
182+
"turnIndex": 4,
183+
"hasToolResult": true
184+
},
185+
"response": {
186+
"toolCalls": [
187+
{
188+
"id": "call_tr_d20_seq_005",
189+
"name": "roll_d20",
190+
"arguments": "{\"value\":20}"
191+
}
192+
]
193+
}
194+
},
195+
{
196+
"match": {
197+
"userMessage": "Roll a 20-sided die.",
198+
"turnIndex": 5,
199+
"hasToolResult": true
200+
},
201+
"response": {
202+
"content": "Rolled the d20 five times — landed on 20 on the final roll."
203+
}
204+
},
4205
{
5206
"match": {
6207
"userMessage": "3D axis visualization (model airplane)"

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
from langchain_openai import ChatOpenAI
2020
from copilotkit import CopilotKitMiddleware
2121

22-
# One-tool-per-question prompt.
22+
# Multi-tool-per-question prompt.
2323
#
2424
# 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.
25+
# rendering patterns (per-tool, catch-all, default fallback). The agent
26+
# may call multiple tools per turn when the user asks for them. The
27+
# `roll_d20` tool accepts a deterministic `value` parameter so the
28+
# aimock fixtures can script the exact dice sequence the e2e tests
29+
# assert against.
2830
SYSTEM_PROMPT = (
2931
"You are a helpful travel & lifestyle concierge. You have mock tools "
3032
"for weather, flights, stock prices, and dice rolls — they all return "
@@ -35,14 +37,12 @@
3537
"destination (default origin to 'SFO' if the user only names a "
3638
"destination).\n"
3739
" - Stock questions → call `get_stock_price` with the ticker.\n"
38-
" - Dice rolls → call `roll_dice` with the requested sides.\n"
40+
" - d20 rolls → call `roll_d20` (it returns a deterministic value).\n"
3941
" - 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."
42+
"When the user asks you to chain or call multiple tools in a single "
43+
"turn, call each tool they requested. After tools return, write one "
44+
"short sentence summarizing the result. Never fabricate data a tool "
45+
"could provide."
4646
)
4747

4848

@@ -103,16 +103,23 @@ def get_stock_price(ticker: str) -> dict:
103103

104104

105105
@tool
106-
def roll_dice(sides: int = 6) -> dict:
107-
"""Roll a single die with the given number of sides."""
108-
return {"sides": sides, "result": randint(1, max(2, sides))}
106+
def roll_d20(value: int = 0) -> dict:
107+
"""Roll a 20-sided die.
108+
109+
The `value` argument lets the LLM (or aimock fixture) script a
110+
deterministic roll for testing — the tool simply echoes it back as
111+
the result. When called without `value` (or with 0), the tool
112+
returns a random natural d20 roll.
113+
"""
114+
rolled = value if isinstance(value, int) and 1 <= value <= 20 else randint(1, 20)
115+
return {"sides": 20, "value": rolled, "result": rolled}
109116

110117

111118
model = ChatOpenAI(model="gpt-4o-mini")
112119

113120
graph = create_agent(
114121
model=model,
115-
tools=[get_weather, search_flights, get_stock_price, roll_dice],
122+
tools=[get_weather, search_flights, get_stock_price, roll_d20],
116123
middleware=[CopilotKitMiddleware()],
117124
system_prompt=SYSTEM_PROMPT,
118125
)

0 commit comments

Comments
 (0)