Skip to content

Commit 8ffc94e

Browse files
tylerslatonclaude
andcommitted
feat(showcase/google-adk/gen-ui-agent): port frontend from langgraph-python
Copy LP gen-ui-agent demo verbatim: page.tsx now uses InlineAgentStateCard in messageView.children (subscribing to live state via useAgent v2 + UseAgentUpdate.OnStateChanged), plus the supporting InlineAgentStateCard.tsx, message-list-with-state.tsx, suggestions.ts, and README.md. Update the ADK gen_ui_agent to emit the same Step shape the LP frontend reads — {id, title, status} with the three-state lifecycle pending -> in_progress -> completed, driven by a six-call sequence so the card animates through every step. Without this, the card would render empty titles and never show the in_progress marker. Delete the orphan agent.py stub from the demo dir — the real backend agent lives in src/agents/gen_ui_agent.py and is wired through the shared registry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 070dcd7 commit 8ffc94e

7 files changed

Lines changed: 314 additions & 237 deletions

File tree

showcase/integrations/google-adk/src/agents/gen_ui_agent.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Agent backing the Agentic Generative UI (gen-ui-agent) demo.
22
3-
The agent plans a task as a list of steps and walks each step pending ->
4-
completed via a `set_steps` tool that writes into ADK session state under
5-
`steps`. The frontend reads `agent.state.steps` and renders a TaskProgress
6-
card. Mirrors langgraph-python's gen_ui_agent.py.
3+
The agent plans a task as a list of steps and walks each step
4+
pending -> in_progress -> completed via a `set_steps` tool that writes into
5+
ADK session state under `steps`. The frontend reads `agent.state.steps` and
6+
renders a live InlineAgentStateCard. Mirrors langgraph-python's
7+
gen_ui_agent.py — each step is `{id, title, status}` with status one of
8+
"pending", "in_progress", or "completed".
79
"""
810

911
from __future__ import annotations
@@ -18,24 +20,35 @@ def set_steps(tool_context: ToolContext, steps: list[dict]) -> dict:
1820
"""Publish the current plan + step statuses.
1921
2022
Call this every time a step transitions (including the first enumeration
21-
of all steps). Each step is an object with `description` and `status`
22-
where status is one of "pending" or "completed".
23+
of all steps). Each step is an object with `id`, `title`, and `status`
24+
where status is one of "pending", "in_progress", or "completed".
2325
"""
2426
tool_context.state["steps"] = steps
2527
return {"status": "ok", "step_count": len(steps)}
2628

2729

2830
_INSTRUCTION = (
29-
"You are an agentic planner. For each user request:\n"
30-
"(1) Plan exactly the requested number of concrete steps, then call "
31-
"`set_steps` ONCE with all of them set to status='pending'.\n"
32-
"(2) For each step in order, simulate the work briefly, then call "
33-
"`set_steps` with that step (and all earlier steps) flipped to "
34-
"status='completed'. Always pass the FULL list of steps every call.\n"
35-
"(3) After every step is completed, respond conversationally with a "
36-
"short summary.\n"
37-
"Never call set_steps in parallel — wait for one call to return before "
38-
"the next."
31+
"You are an agentic planner. For each user request, follow this exact "
32+
"sequence:\n"
33+
"1. Plan exactly 3 concrete steps and call `set_steps` ONCE with all "
34+
'three steps at status="pending". Each step is an object with `id` '
35+
"(short unique string like \"s1\", \"s2\", \"s3\"), `title` (concise "
36+
"description of the step), and `status`.\n"
37+
'2. Step 1: call `set_steps` with step 1 at status="in_progress", '
38+
'then call `set_steps` again with step 1 at status="completed".\n'
39+
'3. Step 2: call `set_steps` with step 2 at status="in_progress", '
40+
'then call `set_steps` again with step 2 at status="completed".\n'
41+
'4. Step 3: call `set_steps` with step 3 at status="in_progress", '
42+
'then call `set_steps` again with step 3 at status="completed".\n'
43+
"5. Send ONE final conversational assistant message summarizing the "
44+
"plan, then stop. Do not call any more tools after step 3 is "
45+
"completed.\n"
46+
"\n"
47+
"Always pass the FULL list of all three steps every call (preserving "
48+
"each step's `id` and `title` across calls; only `status` changes). "
49+
"Never call set_steps in parallel — always wait for one call to "
50+
"return before the next. After all three steps are completed you MUST "
51+
"send a final assistant message and terminate."
3952
)
4053

4154
gen_ui_agent = LlmAgent(
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"use client";
2+
3+
import React from "react";
4+
5+
/**
6+
* Step shape matches the `Step` TypedDict emitted by the Python deep agent's
7+
* custom `set_steps` tool (see `src/agents/gen_ui_agent.py`).
8+
* Status transitions: pending -> in_progress -> completed.
9+
*/
10+
export type Step = {
11+
id: string;
12+
title: string;
13+
status: "pending" | "in_progress" | "completed";
14+
};
15+
16+
export function InlineAgentStateCard({
17+
steps,
18+
status,
19+
}: {
20+
steps: Step[];
21+
status: "inProgress" | "complete";
22+
}) {
23+
const total = steps.length;
24+
const done = steps.filter((s) => s.status === "completed").length;
25+
const headline =
26+
status === "complete" || (total > 0 && done === total)
27+
? `All ${total} steps complete`
28+
: total > 0
29+
? `Step ${Math.min(done + 1, total)} of ${total}`
30+
: "Planning…";
31+
32+
return (
33+
<div
34+
data-testid="agent-state-card"
35+
className="my-3 mx-4 rounded-2xl border border-[#DBDBE5] bg-white p-4 shadow-sm"
36+
>
37+
<div className="flex items-center gap-2">
38+
{status === "inProgress" && done < total ? (
39+
<SpinnerIcon />
40+
) : (
41+
<CheckIcon />
42+
)}
43+
<span className="text-sm font-semibold text-[#010507]">{headline}</span>
44+
</div>
45+
46+
{steps.length > 0 && (
47+
<ol className="mt-3 space-y-2">
48+
{steps.map((step, idx) => (
49+
<li
50+
key={step.id ?? idx}
51+
data-testid="agent-step"
52+
data-status={step.status}
53+
className="flex items-start gap-3"
54+
>
55+
<StepMarker status={step.status} index={idx} />
56+
<span
57+
className={
58+
"text-xs leading-5 " +
59+
(step.status === "completed"
60+
? "text-[#838389] line-through"
61+
: step.status === "in_progress"
62+
? "text-[#010507] font-medium"
63+
: "text-[#57575B]")
64+
}
65+
>
66+
{step.title}
67+
</span>
68+
</li>
69+
))}
70+
</ol>
71+
)}
72+
</div>
73+
);
74+
}
75+
76+
function StepMarker({
77+
status,
78+
index,
79+
}: {
80+
status: Step["status"];
81+
index: number;
82+
}) {
83+
if (status === "completed") {
84+
return (
85+
<span className="mt-0.5 inline-flex h-5 w-5 flex-none items-center justify-center rounded-full bg-[#85ECCE] text-[#010507]">
86+
<svg
87+
className="h-3 w-3"
88+
fill="none"
89+
stroke="currentColor"
90+
viewBox="0 0 24 24"
91+
>
92+
<path
93+
strokeLinecap="round"
94+
strokeLinejoin="round"
95+
strokeWidth={3}
96+
d="M5 13l4 4L19 7"
97+
/>
98+
</svg>
99+
</span>
100+
);
101+
}
102+
if (status === "in_progress") {
103+
return (
104+
<span className="mt-0.5 inline-flex h-5 w-5 flex-none items-center justify-center rounded-full bg-[#BEC2FF] text-[#010507]">
105+
<svg
106+
className="h-3 w-3 animate-spin"
107+
xmlns="http://www.w3.org/2000/svg"
108+
fill="none"
109+
viewBox="0 0 24 24"
110+
>
111+
<circle
112+
className="opacity-30"
113+
cx="12"
114+
cy="12"
115+
r="10"
116+
stroke="currentColor"
117+
strokeWidth="4"
118+
/>
119+
<path
120+
className="opacity-90"
121+
fill="currentColor"
122+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
123+
/>
124+
</svg>
125+
</span>
126+
);
127+
}
128+
return (
129+
<span className="mt-0.5 inline-flex h-5 w-5 flex-none items-center justify-center rounded-full border border-[#DBDBE5] bg-white text-[10px] font-semibold text-[#838389]">
130+
{index + 1}
131+
</span>
132+
);
133+
}
134+
135+
function SpinnerIcon() {
136+
return (
137+
<svg
138+
className="w-4 h-4 animate-spin text-[#010507]"
139+
xmlns="http://www.w3.org/2000/svg"
140+
fill="none"
141+
viewBox="0 0 24 24"
142+
>
143+
<circle
144+
className="opacity-25"
145+
cx="12"
146+
cy="12"
147+
r="10"
148+
stroke="currentColor"
149+
strokeWidth="4"
150+
/>
151+
<path
152+
className="opacity-75"
153+
fill="currentColor"
154+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
155+
/>
156+
</svg>
157+
);
158+
}
159+
160+
function CheckIcon() {
161+
return (
162+
<svg
163+
className="w-4 h-4 text-[#189370]"
164+
fill="none"
165+
stroke="currentColor"
166+
viewBox="0 0 24 24"
167+
>
168+
<path
169+
strokeLinecap="round"
170+
strokeLinejoin="round"
171+
strokeWidth={3}
172+
d="M5 13l4 4L19 7"
173+
/>
174+
</svg>
175+
);
176+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Agentic Generative UI
2+
3+
The agent renders custom UI as it works through long-running tasks, streaming
4+
status updates and intermediate results into the chat.
5+
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.
8+
9+
The canonical description lives in the showcase manifest; this README is just
10+
a developer note alongside the demo source.

showcase/integrations/google-adk/src/app/demos/gen-ui-agent/agent.py

Lines changed: 0 additions & 5 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+
}

0 commit comments

Comments
 (0)