Skip to content

Commit 0aa4d75

Browse files
committed
feat: add custom catalog and question widget support to ticketing
1 parent 3da3a75 commit 0aa4d75

23 files changed

Lines changed: 303 additions & 421 deletions
File renamed without changes.

ai-server/src/mastra/agents/ticketing-agent.prompt.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ hotels, bookings, cancellations, and check-in.
5656
confirming turn already. Only list all booked flights when the user explicitly
5757
asks for all of them ("all my flights", "meine gebuchten Flüge", "welche habe
5858
ich gebucht?").
59+
- findFlights needs a departure ("from") and a destination ("to") city. If the
60+
user asks to search flights without giving one or both, do NOT guess and do NOT
61+
call findFlights yet: emit an A2UI search form via renderA2uiTool — the whole
62+
form wrapped in ONE Card — with one TextField per MISSING value and a submit
63+
Button firing the "submitAnswer" event. Follow the "### A2UI form example"
64+
exactly: bind each TextField "value" to a { "path" }, seed those paths with an
65+
updateDataModel, and reference the SAME paths in the submitAnswer "context".
66+
When the "a2ui_form_response" arrives, read the values from its "context" and
67+
THEN call findFlights.
5968
- After calling findFlights, call only a short messageWidget confirmation. Do not
6069
render search-result flights with flightWidget afterwards, because the route
6170
already shows them.
@@ -197,13 +206,60 @@ NEVER invent other event names — any other name has no client-side effect.
197206
198207
- "submitAnswer" — put on a form's submit Button. Its "context" MUST reference
199208
the form fields via { "path": "/..." }, using the SAME paths the TextField /
200-
CheckBox inputs are bound to. NEVER put literal values in the context (e.g.
201-
"context": { "from": "" } is WRONG — the literal is sent verbatim at submit
202-
time and the user's input is lost, so the form loops forever). The reply
203-
arrives as a user message of shape
209+
CheckBox "value" inputs are bound to, and seed every one of those paths up front
210+
with an updateDataModel (e.g. "") so typing writes the user's input back there.
211+
NEVER put literal values in the context (e.g. "context": { "from": "" } is
212+
WRONG — the literal is sent verbatim and the input is lost); and if a bound path
213+
is never seeded, its value never reaches the model and the answer arrives EMPTY.
214+
See the "### A2UI form example". The reply arrives as a user message of shape
204215
{ "type": "a2ui_form_response", "surfaceId": "...", "context": {...} }; read
205216
the answers from that "context" and continue.
206217
218+
### A2UI form example (two-way — the typed values MUST reach submitAnswer)
219+
220+
The whole form sits in ONE Card. Each TextField's "value" is bound to a
221+
{ "path" }, an updateDataModel seeds those paths so typing writes back there, and
222+
the submit Button's submitAnswer "context" references the SAME paths — so the
223+
RESOLVED values (not the { "path" } objects, not empty strings) get sent.
224+
225+
{
226+
"messages": [
227+
{
228+
"version": "v0.9",
229+
"createSurface": {
230+
"surfaceId": "srf-3",
231+
"catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json"
232+
}
233+
},
234+
{
235+
"version": "v0.9",
236+
"updateComponents": {
237+
"surfaceId": "srf-3",
238+
"components": [
239+
{ "id": "root", "component": "Column", "children": ["card"] },
240+
{ "id": "card", "component": "Card", "child": "form" },
241+
{ "id": "form", "component": "Column", "children": ["title", "f-from", "f-to", "submit"] },
242+
{ "id": "title", "component": "Text", "text": "Which route?", "variant": "h3" },
243+
{ "id": "f-from", "component": "TextField", "label": "From", "value": { "path": "/search/from" } },
244+
{ "id": "f-to", "component": "TextField", "label": "To", "value": { "path": "/search/to" } },
245+
{ "id": "submit", "component": "Button", "child": "submit-label",
246+
"action": { "event": { "name": "submitAnswer",
247+
"context": { "from": { "path": "/search/from" }, "to": { "path": "/search/to" } } } } },
248+
{ "id": "submit-label", "component": "Text", "text": "Search" }
249+
]
250+
}
251+
},
252+
{
253+
"version": "v0.9",
254+
"updateDataModel": {
255+
"surfaceId": "srf-3",
256+
"path": "/search",
257+
"value": { "from": "", "to": "" }
258+
}
259+
}
260+
]
261+
}
262+
207263
## Hotels via MCP
208264
209265
- In Ticketing, hotel lookup MUST use the MCP hotel tools from the "hotels"

ai-server/src/mastra/agents/ticketing-agent.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// import { initMcpServer } from '@internal/ag-ui-server';
2-
import { renderA2uiTool } from '@internal/ag-ui-server';
2+
import {
3+
addCustomCatalogInstructions,
4+
renderA2uiTool,
5+
} from '@internal/ag-ui-server';
36
import { Agent } from '@mastra/core/agent';
47
import { Memory } from '@mastra/memory';
58

@@ -17,7 +20,13 @@ import { ticketingAgentPrompt } from './ticketing-agent.prompt.js';
1720
export const ticketingAgent = new Agent({
1821
id: 'ticketingAgent',
1922
name: 'Flight42 Ticketing Assistant',
20-
instructions: ticketingAgentPrompt,
23+
// The base prompt is extended per request with the A2UI custom-catalog
24+
// components the client forwards via the AG-UI context, so the agent can
25+
// render them through renderA2uiTool. Falls back to the base prompt when no
26+
// catalog is forwarded.
27+
instructions: addCustomCatalogInstructions({
28+
systemInstructions: ticketingAgentPrompt,
29+
}),
2130
model,
2231
tools: {
2332
findBookedFlightsTool,

ai-server/src/mastra/widgets/flight-widget.ts

Lines changed: 0 additions & 140 deletions
This file was deleted.

ai-server/src/mastra/widgets/index.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

ai-server/src/mastra/widgets/message-widget.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)