Skip to content

Commit fbbacca

Browse files
committed
feat(agno): port a2ui-fixed-schema demo
Adds a dedicated A2UI fixed-schema cell for the Agno integration: backend `a2ui_fixed_agent.py` ships `flight_schema.json` + `booked_schema.json` and a single `display_flight` tool that emits an `a2ui_operations` container directly (no secondary LLM). Frontend ports `page.tsx` + Zod definitions / renderers / catalog from the langgraph-python reference. Dedicated runtime route at `/api/copilotkit-a2ui-fixed-schema` runs the A2UI middleware with `injectA2UITool: false` so the agent owns its rendering tool.
1 parent 025c9a2 commit fbbacca

12 files changed

Lines changed: 699 additions & 4 deletions

File tree

showcase/integrations/agno/PARITY_NOTES.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ See `manifest.yaml` for the authoritative list.
8989
+ `byoc_json_render_agent.py` whose system prompt steers the LLM toward
9090
the json-render flat element-tree spec (`{ root, elements }`).
9191

92+
### Sixth pass (A2UI fixed schema)
93+
94+
- `a2ui-fixed-schema` — dedicated `/api/copilotkit-a2ui-fixed-schema`
95+
runtime with `injectA2UITool: false`. New `a2ui_fixed_agent.py`
96+
mounted at `/a2ui-fixed-schema/agui` ships
97+
`flight_schema.json` + `booked_schema.json` and a single
98+
`display_flight` tool that emits an `a2ui_operations` container
99+
*directly* — no secondary LLM call — so the LLM only fills in data
100+
(origin/destination/airline/price). `booked_schema.json` is shipped
101+
as a sibling for when the SDK exposes per-button action handlers
102+
for fixed-schema surfaces.
103+
92104
### Third pass (state + multi-agent recovery)
93105

94106
- `shared-state-read-write` — bidirectional shared state with the UI
@@ -153,10 +165,6 @@ follow-up parity pass rather than faked in.
153165
an Agno agent
154166
- `agent-config` — dedicated `/api/copilotkit-agent-config` runtime with typed
155167
config forwarding; needs Agno dynamic-system-prompt wiring per-request
156-
- `declarative-gen-ui` (A2UI dynamic) — dedicated runtime + frontend A2UI
157-
catalog; the existing Agno `main` agent already exposes `generate_a2ui`,
158-
but the declarative-gen-ui cell expects a different runtime surface
159-
- `a2ui-fixed-schema` — dedicated runtime + fixed-schema catalog
160168
- `mcp-apps` — requires Agno MCP client/server wiring; Agno has
161169
`agno.tools.mcp.MCPTools` but integration with the AGUI adapter's
162170
activity-message surface wasn't verified

showcase/integrations/agno/manifest.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ features:
5757
- headless-complete
5858
- auth
5959
- beautiful-chat
60+
- a2ui-fixed-schema
6061
not_supported_features:
6162
- gen-ui-interrupt
6263
- interrupt-headless
@@ -491,6 +492,23 @@ demos:
491492
- src/app/demos/byoc-json-render/charts/bar-chart.tsx
492493
- src/app/demos/byoc-json-render/charts/pie-chart.tsx
493494
- src/app/api/copilotkit/route.ts
495+
- id: a2ui-fixed-schema
496+
name: "A2UI Fixed Schema"
497+
description: Schema is authored ahead of time as JSON; the agent only streams data into the data model. `display_flight` emits an a2ui_operations container directly with no secondary LLM call
498+
tags:
499+
- generative-ui
500+
- a2ui
501+
route: /demos/a2ui-fixed-schema
502+
animated_preview_url:
503+
highlight:
504+
- src/agents/a2ui_fixed_agent.py
505+
- src/agents/a2ui_schemas/flight_schema.json
506+
- src/agents/a2ui_schemas/booked_schema.json
507+
- src/app/demos/a2ui-fixed-schema/page.tsx
508+
- src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts
509+
- src/app/demos/a2ui-fixed-schema/a2ui/definitions.ts
510+
- src/app/demos/a2ui-fixed-schema/a2ui/renderers.tsx
511+
- src/app/api/copilotkit-a2ui-fixed-schema/route.ts
494512
managed_platform:
495513
name: Agent OS
496514
url: https://os.agno.com

showcase/integrations/agno/src/agent_server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from starlette.middleware.base import BaseHTTPMiddleware
4343
from starlette.responses import JSONResponse
4444

45+
from agents.a2ui_fixed_agent import agent as a2ui_fixed_agent
4546
from agents.agent_config_agent import (
4647
agent as agent_config_agent,
4748
build_agent as build_agent_config_agent,
@@ -277,6 +278,7 @@ async def _gen():
277278
agent_os = AgentOS(
278279
agents=[
279280
main_agent,
281+
a2ui_fixed_agent,
280282
agent_config_agent,
281283
byoc_hashbrown_agent,
282284
byoc_json_render_agent,
@@ -306,6 +308,10 @@ async def _gen():
306308
# BYOC: json-render — agent emits a json-render spec the frontend
307309
# renderer mounts against a Zod-validated catalog.
308310
AGUI(agent=byoc_json_render_agent, prefix="/byoc-json-render"),
311+
# A2UI fixed schema — agent's `display_flight` tool emits an
312+
# `a2ui_operations` container directly (no secondary LLM) bound to
313+
# the pre-authored `flight_schema.json`.
314+
AGUI(agent=a2ui_fixed_agent, prefix="/a2ui-fixed-schema"),
309315
],
310316
)
311317
app = agent_os.get_app()
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Agno agent for the Declarative Generative UI (A2UI Fixed Schema) demo.
2+
3+
Fixed-schema A2UI: the component tree (schema) is authored ahead of time
4+
as JSON and shipped with the backend. The agent only streams *data* into
5+
the data model at runtime via the `display_flight` tool. The frontend
6+
registers a matching catalog (see
7+
`src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts`).
8+
9+
Mirrors the langgraph-python `a2ui_fixed.py` reference. The dedicated
10+
runtime route at `api/copilotkit-a2ui-fixed-schema/route.ts` runs the
11+
A2UI middleware with `injectA2UITool: false` because the backend owns
12+
the rendering tool itself — it emits an `a2ui_operations` container
13+
directly without a secondary LLM call.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import json
19+
from pathlib import Path
20+
21+
from agno.agent.agent import Agent
22+
from agno.models.openai import OpenAIChat
23+
from agno.tools import tool
24+
from dotenv import load_dotenv
25+
26+
load_dotenv()
27+
28+
29+
CATALOG_ID = "copilotkit://flight-fixed-catalog"
30+
SURFACE_ID = "flight-fixed-schema"
31+
32+
_SCHEMAS_DIR = Path(__file__).parent / "a2ui_schemas"
33+
34+
35+
def _load_schema(filename: str) -> list[dict]:
36+
"""Load an A2UI fixed schema from the local schemas directory."""
37+
with open(_SCHEMAS_DIR / filename, "r", encoding="utf-8") as fh:
38+
return json.load(fh)
39+
40+
41+
FLIGHT_SCHEMA = _load_schema("flight_schema.json")
42+
# `booked_schema.json` is shipped alongside `flight_schema.json` so the
43+
# schema is ready to wire up once the SDK exposes per-button action handlers
44+
# for fixed-schema surfaces (matching the langgraph-python reference).
45+
BOOKED_SCHEMA = _load_schema("booked_schema.json")
46+
47+
48+
@tool
49+
def display_flight(origin: str, destination: str, airline: str, price: str):
50+
"""Show a flight card for the given trip.
51+
52+
Emits an `a2ui_operations` container directly — NO secondary LLM
53+
call. The runtime A2UI middleware detects the container in the tool
54+
result and forwards the surface to the frontend renderer. The
55+
frontend catalog resolves component names against the local React
56+
components.
57+
58+
Args:
59+
origin (str): Origin airport code (e.g. "SFO").
60+
destination (str): Destination airport code (e.g. "JFK").
61+
airline (str): Airline name (e.g. "United").
62+
price (str): Price string (e.g. "$289").
63+
64+
Returns:
65+
str: A2UI operations as JSON.
66+
"""
67+
operations = [
68+
{
69+
"type": "create_surface",
70+
"surfaceId": SURFACE_ID,
71+
"catalogId": CATALOG_ID,
72+
},
73+
{
74+
"type": "update_components",
75+
"surfaceId": SURFACE_ID,
76+
"components": FLIGHT_SCHEMA,
77+
},
78+
{
79+
"type": "update_data_model",
80+
"surfaceId": SURFACE_ID,
81+
"data": {
82+
"origin": origin,
83+
"destination": destination,
84+
"airline": airline,
85+
"price": price,
86+
},
87+
},
88+
]
89+
return json.dumps({"a2ui_operations": operations})
90+
91+
92+
SYSTEM_PROMPT = (
93+
"You help users find flights. When asked about a flight, call "
94+
"display_flight with origin (3-letter code), destination (3-letter "
95+
"code), airline, and price (e.g. '$289'). Keep any chat reply to one "
96+
"short sentence."
97+
)
98+
99+
100+
agent = Agent(
101+
model=OpenAIChat(id="gpt-4o-mini", timeout=120),
102+
tools=[display_flight],
103+
tool_call_limit=4,
104+
description=SYSTEM_PROMPT,
105+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"id": "root",
4+
"component": "Column",
5+
"gap": 8,
6+
"children": ["title", "detail"]
7+
},
8+
{
9+
"id": "title",
10+
"component": "Text",
11+
"text": { "path": "/title" },
12+
"variant": "h2"
13+
},
14+
{
15+
"id": "detail",
16+
"component": "Text",
17+
"text": { "path": "/detail" },
18+
"variant": "body"
19+
}
20+
]
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
[
2+
{
3+
"id": "root",
4+
"component": "Card",
5+
"child": "content"
6+
},
7+
{
8+
"id": "content",
9+
"component": "Column",
10+
"children": ["title", "route", "meta", "bookButton"]
11+
},
12+
{
13+
"id": "title",
14+
"component": "Title",
15+
"text": "Flight Details"
16+
},
17+
{
18+
"id": "route",
19+
"component": "Row",
20+
"justify": "spaceBetween",
21+
"align": "center",
22+
"children": ["from", "arrow", "to"]
23+
},
24+
{
25+
"id": "from",
26+
"component": "Airport",
27+
"code": { "path": "/origin" }
28+
},
29+
{
30+
"id": "arrow",
31+
"component": "Arrow"
32+
},
33+
{
34+
"id": "to",
35+
"component": "Airport",
36+
"code": { "path": "/destination" }
37+
},
38+
{
39+
"id": "meta",
40+
"component": "Row",
41+
"justify": "spaceBetween",
42+
"align": "center",
43+
"children": ["airline", "price"]
44+
},
45+
{
46+
"id": "airline",
47+
"component": "AirlineBadge",
48+
"name": { "path": "/airline" }
49+
},
50+
{
51+
"id": "price",
52+
"component": "PriceTag",
53+
"amount": { "path": "/price" }
54+
},
55+
{
56+
"id": "bookButton",
57+
"component": "Button",
58+
"variant": "primary",
59+
"child": "bookButtonLabel",
60+
"action": {
61+
"event": {
62+
"name": "book_flight",
63+
"context": {
64+
"origin": { "path": "/origin" },
65+
"destination": { "path": "/destination" },
66+
"airline": { "path": "/airline" },
67+
"price": { "path": "/price" }
68+
}
69+
}
70+
}
71+
},
72+
{
73+
"id": "bookButtonLabel",
74+
"component": "Text",
75+
"text": "Book flight"
76+
}
77+
]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Dedicated runtime for the A2UI — Fixed Schema cell. Splitting into its
2+
// own endpoint lets us set `a2ui.injectA2UITool: false` — the backend Agno
3+
// agent owns the `display_flight` tool which emits its own
4+
// `a2ui_operations` container directly in the tool result (no secondary
5+
// LLM call).
6+
//
7+
// Reference:
8+
// - showcase/integrations/langgraph-python/src/app/api/copilotkit-a2ui-fixed-schema/route.ts
9+
// - src/agents/a2ui_fixed_agent.py (the Agno backend)
10+
11+
import { NextRequest, NextResponse } from "next/server";
12+
import {
13+
CopilotRuntime,
14+
ExperimentalEmptyAdapter,
15+
copilotRuntimeNextJSAppRouterEndpoint,
16+
} from "@copilotkit/runtime";
17+
import { HttpAgent } from "@ag-ui/client";
18+
19+
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
20+
21+
const a2uiFixedSchemaAgent = new HttpAgent({
22+
url: `${AGENT_URL}/a2ui-fixed-schema/agui`,
23+
});
24+
25+
const runtime = new CopilotRuntime({
26+
// @ts-ignore -- see main route.ts
27+
agents: { "a2ui-fixed-schema": a2uiFixedSchemaAgent },
28+
a2ui: {
29+
// The backend agent emits its own `a2ui_operations` container inside
30+
// `display_flight` (see src/agents/a2ui_fixed_agent.py). We still run
31+
// the A2UI middleware so it detects the container in tool results and
32+
// forwards surfaces to the frontend — but we do NOT inject a runtime
33+
// `render_a2ui` tool on top of the agent's existing tools.
34+
injectA2UITool: false,
35+
},
36+
});
37+
38+
export const POST = async (req: NextRequest) => {
39+
try {
40+
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
41+
endpoint: "/api/copilotkit-a2ui-fixed-schema",
42+
serviceAdapter: new ExperimentalEmptyAdapter(),
43+
runtime,
44+
});
45+
return await handleRequest(req);
46+
} catch (error: unknown) {
47+
const e = error as { message?: string; stack?: string };
48+
return NextResponse.json(
49+
{ error: e.message, stack: e.stack },
50+
{ status: 500 },
51+
);
52+
}
53+
};

0 commit comments

Comments
 (0)