Skip to content

Commit b2e3ac5

Browse files
committed
docs(showcase): a2ui-fixed-schema cross-framework parity via WhenFrameworkHas
Closes the architectural-divergence gap for a2ui-fixed-schema across 4 frameworks. Pairs with PDX-68 — the canonical docs page now renders the correct code + prose per framework idiom. Code regions added: - spring-ai DisplayFlightTool.java: wraps inline FLIGHT_SCHEMA with @region[backend-schema-json-load] - ms-agent-dotnet A2uiFixedSchemaAgent.cs: same name wrapping the inline C# FlightSchema array - mastra src/mastra/tools/index.ts: wraps generateA2uiTool with @region[backend-render-operations] (LLM-driven path) - strands src/agents/agent.py: same on the generate_a2ui @tool MDX (fixed-schema.mdx) restructure: - Intro neutralized; new 3-bullet rundown of which frameworks fall into schema-loading / schema-inline / llm-driven - 'How it works' step 1 reworded to be framework-neutral - Steps 4 + 5 split into three <WhenFrameworkHas a2ui_pattern=...> gates: schema-loading → 'Load the schema JSON at startup' + render ops schema-inline → 'Define the schema inline' + render ops llm-driven → single 'Generate the schema dynamically' step Spot-checked on dev server: - /langgraph-python/.../fixed-schema → shows schema-loading section only - /spring-ai/.../fixed-schema → shows schema-inline section only - /mastra/.../fixed-schema → shows llm-driven section only - /crewai-crews/.../fixed-schema → shows schema-loading section only
1 parent 7699e95 commit b2e3ac5

5 files changed

Lines changed: 111 additions & 19 deletions

File tree

showcase/integrations/mastra/src/mastra/tools/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ export const searchFlightsTool = createTool({
145145
execute: async ({ flights }) => JSON.stringify(searchFlightsImpl(flights)),
146146
});
147147

148+
// @region[backend-render-operations]
149+
// The `generate-a2ui` tool runs a secondary LLM call with a forced
150+
// `render_a2ui` tool, then converts that tool call's args into the
151+
// A2UI `a2ui_operations` container that the middleware forwards to
152+
// the frontend renderer. Mastra returns the operations as a JSON
153+
// string from the tool body; the catalog
154+
// (`copilotkit://generative-catalog`) resolves component names to
155+
// React renderers on the client.
148156
export const generateA2uiTool = createTool({
149157
id: "generate-a2ui",
150158
description: "Generate dynamic A2UI surface components",
@@ -197,3 +205,4 @@ export const generateA2uiTool = createTool({
197205
);
198206
},
199207
});
208+
// @endregion[backend-render-operations]

showcase/integrations/ms-agent-dotnet/agent/A2uiFixedSchemaAgent.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,14 @@ public AIAgent Create()
6969
]);
7070
}
7171

72-
// The fixed-schema flight component tree. Frontend renders this via a
73-
// registered catalog (`copilotkit://flight-fixed-catalog`). Matches the
74-
// LangGraph reference at src/agents/a2ui_schemas/flight_schema.json.
72+
// @region[backend-schema-json-load]
73+
// The fixed-schema flight component tree. .NET doesn't ship a
74+
// JSON-loading helper analogous to LangGraph Python's
75+
// `a2ui.load_schema(...)`, so the schema is declared inline as a
76+
// C# array — equivalent to deserialising a `flight_schema.json`
77+
// file at startup. Matches the LangGraph reference at
78+
// `src/agents/a2ui_schemas/flight_schema.json`. Frontend renders
79+
// this via the registered catalog (`copilotkit://flight-fixed-catalog`).
7580
private static readonly object[] FlightSchema = new object[]
7681
{
7782
new { id = "root", component = "Card", child = "content" },
@@ -121,6 +126,7 @@ public AIAgent Create()
121126
},
122127
new { id = "bookButtonLabel", component = "Text", text = "Book flight" },
123128
};
129+
// @endregion[backend-schema-json-load]
124130

125131
// @region[backend-render-operations]
126132
[Description("Show a flight card for the given trip. Use short airport codes (e.g. SFO, JFK) for origin/destination and a price string like $289.")]

showcase/integrations/spring-ai/src/main/java/com/copilotkit/showcase/springai/tools/DisplayFlightTool.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ public class DisplayFlightTool implements Function<DisplayFlightTool.Request, St
2525

2626
public record Request(String origin, String destination, String airline, String price) {}
2727

28+
// @region[backend-schema-json-load]
29+
// The fixed-schema flight component tree. Spring AI doesn't ship a
30+
// JSON-loading helper analogous to `a2ui.load_schema(...)`, so the
31+
// schema is declared inline as a Java constant — equivalent to
32+
// deserialising a `flight_schema.json` file at startup. The frontend
33+
// catalog (`copilotkit://flight-fixed-catalog`) resolves each
34+
// `component` name to a React renderer.
2835
private static final List<Map<String, Object>> FLIGHT_SCHEMA = List.of(
2936
Map.of("id", "root", "component", "Card", "child", "content"),
3037
Map.of("id", "content", "component", "Column",
@@ -58,6 +65,7 @@ public record Request(String origin, String destination, String airline, String
5865
)),
5966
Map.of("id", "bookButtonLabel", "component", "Text", "text", "Book flight")
6067
);
68+
// @endregion[backend-schema-json-load]
6169

6270
@Override
6371
public String apply(Request request) {

showcase/integrations/strands/src/agents/agent.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ def search_flights(flights: list[dict]):
155155
return json.dumps(result)
156156

157157

158+
# @region[backend-render-operations]
159+
# The `generate_a2ui` tool runs a secondary LLM call with a forced
160+
# `render_a2ui` tool, then converts that tool call's args into the
161+
# A2UI `a2ui_operations` container via
162+
# `build_a2ui_operations_from_tool_call`. The ag_ui_strands middleware
163+
# detects the container in the tool result and forwards the ops to
164+
# the frontend, which resolves component names through the registered
165+
# catalog (`copilotkit://generative-catalog`).
158166
@tool
159167
def generate_a2ui(context: str) -> str:
160168
"""Generate dynamic A2UI components based on the conversation.
@@ -273,6 +281,7 @@ def generate_a2ui(context: str) -> str:
273281

274282
result = build_a2ui_operations_from_tool_call(args)
275283
return json.dumps(result)
284+
# @endregion[backend-render-operations]
276285

277286

278287
@tool

showcase/shell-docs/src/content/docs/generative-ui/a2ui/fixed-schema.mdx

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,34 @@ snippet_cell: a2ui-fixed-schema
88

99
In the fixed-schema approach, you design the UI schema once (by hand,
1010
or using the [A2UI Composer](https://a2ui-composer.ag-ui.com/)) and
11-
save it as JSON next to your agent. The agent tool only provides the
12-
*data*; the surface appears instantly when the tool returns because
13-
nothing has to be generated at runtime.
11+
keep it on the agent side. The agent tool only provides the *data*;
12+
the surface appears instantly when the tool returns because nothing
13+
has to be generated at runtime.
14+
15+
How the schema is *delivered* to the runtime is the only thing that
16+
varies between integrations:
17+
18+
- **Schema-loading** (langgraph-python, langgraph-typescript,
19+
langgraph-fastapi, llamaindex, crewai-crews, pydantic-ai,
20+
ms-agent-python, google-adk) — the schema is saved as a `.json`
21+
file next to the agent and loaded once at startup.
22+
- **Schema-inline** (spring-ai, ms-agent-dotnet) — the schema is
23+
declared inline as a typed literal in source. The host language
24+
doesn't ship a `load_schema` JSON loader, so the structure is
25+
compiled in directly.
26+
- **LLM-driven** (mastra, strands) — the agent runs a secondary LLM
27+
call to produce the operations container per-request. The catalog
28+
is still fixed; the schema is generated on demand.
1429

1530
Ask about a flight and the agent renders a fully structured card from a pre-defined schema:
1631

1732
<InlineDemo demo="a2ui-fixed-schema" />
1833

1934
## How it works
2035

21-
1. The schema is loaded from a JSON file at startup via
22-
`a2ui.load_schema(...)`, a thin `json.load` wrapper.
36+
1. The schema is made available to the agent — either loaded from a
37+
JSON file at startup, declared inline, or generated per-request,
38+
depending on the integration.
2339
2. The agent's `display_flight` tool receives data from the primary LLM
2440
(origin / destination / airline / price).
2541
3. The tool returns `a2ui.render(...)` with `createSurface` +
@@ -28,7 +44,7 @@ Ask about a flight and the agent renders a fully structured card from a pre-defi
2844
renders the surface using the matching 5-component client catalog
2945
(Title, Airport, Arrow, AirlineBadge, PriceTag, plus the built-ins).
3046

31-
## Schemas as JSON: compositional trees
47+
## Compositional schemas
3248

3349
The example below ships a flight card assembled compositionally from
3450
small sub-components rather than one monolithic `FlightCard`:
@@ -42,7 +58,8 @@ Card
4258
└─ Button (Book)
4359
```
4460

45-
That tree lives in `backend/schemas/flight_schema.json`. Components
61+
That tree lives backend-side — as a JSON file, an inline literal, or
62+
a per-request LLM output, depending on the integration. Components
4663
without data bindings (like `Title` or `Arrow`) carry their value
4764
inline; components bound to the LLM's data (like `Airport`) reference
4865
fields via JSON Pointer paths such as `{ "path": "/origin" }`. The
@@ -86,28 +103,71 @@ renderers with CopilotKit's built-ins so the schema can reference
86103
<Snippet region="catalog-creation" />
87104
</Step>
88105

106+
<WhenFrameworkHas flag="a2ui_pattern" equals="schema-loading">
89107
<Step>
90108
### Load the schema JSON at startup
91109

92-
`a2ui.load_schema(path)` is a thin `json.load` wrapper that parses the
93-
schema file once at module-import time. The sibling `booked_schema.json`
94-
is kept ready for the button-click "booked" optimistic swap (see the
95-
note on action handlers below):
110+
`a2ui.load_schema(path)` (or the framework's equivalent thin `json.load`
111+
wrapper) parses the schema file once at module-import time. The
112+
sibling `booked_schema.json` is kept ready for the button-click
113+
"booked" optimistic swap (see the note on action handlers below):
96114

97115
<Snippet region="backend-schema-json-load" />
98116
</Step>
99117

100118
<Step>
101119
### Return render operations from the tool
102120

103-
The `display_flight` tool returns `a2ui.render(operations=[…])`. The
104-
A2UI middleware detects the operations container in the tool result
105-
and forwards it to the frontend renderer. The LLM only generates the
106-
four data fields (`origin`, `destination`, `airline`, `price`); the
107-
schema does the rest:
121+
The agent tool returns `a2ui.render(operations=[…])`. The A2UI
122+
middleware detects the operations container in the tool result and
123+
forwards it to the frontend renderer. The LLM only generates the four
124+
data fields (`origin`, `destination`, `airline`, `price`); the schema
125+
does the rest:
108126

109127
<Snippet region="backend-render-operations" />
110128
</Step>
129+
</WhenFrameworkHas>
130+
131+
<WhenFrameworkHas flag="a2ui_pattern" equals="schema-inline">
132+
<Step>
133+
### Define the schema inline
134+
135+
Spring AI / .NET don't ship a `load_schema` JSON helper, so the
136+
component tree is declared inline as a typed literal in source —
137+
equivalent to deserialising a `flight_schema.json` but compiled into
138+
the agent class. The structure is identical to the JSON form; only
139+
the surface syntax changes:
140+
141+
<Snippet region="backend-schema-json-load" />
142+
</Step>
143+
144+
<Step>
145+
### Return render operations from the tool
146+
147+
The agent tool builds the same `createSurface` + `updateComponents` +
148+
`updateDataModel` operations container and returns it. The A2UI
149+
middleware detects the operations in the tool result and forwards
150+
them to the frontend renderer; the LLM only supplies the four data
151+
fields:
152+
153+
<Snippet region="backend-render-operations" />
154+
</Step>
155+
</WhenFrameworkHas>
156+
157+
<WhenFrameworkHas flag="a2ui_pattern" equals="llm-driven">
158+
<Step>
159+
### Generate the schema dynamically
160+
161+
Mastra and Strands take a different route: the agent tool runs a
162+
*secondary* LLM call with a forced tool choice that produces the
163+
operations container per-request. The frontend catalog is still fixed
164+
(same `Title`/`Airport`/`Arrow`/`AirlineBadge`/`PriceTag` primitives),
165+
but the schema is built on the fly. Schema construction and render
166+
emission happen in the same tool call:
167+
168+
<Snippet region="backend-render-operations" />
169+
</Step>
170+
</WhenFrameworkHas>
111171
</Steps>
112172

113173
## Why compositional beats monolithic

0 commit comments

Comments
 (0)