Skip to content

Commit 82fdfef

Browse files
committed
feat: generate dsl i/o a2ui
1 parent c1704f6 commit 82fdfef

15 files changed

Lines changed: 1263 additions & 998 deletions

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

Lines changed: 26 additions & 582 deletions
Large diffs are not rendered by default.
Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,17 @@
11
import { Agent } from '@mastra/core/agent';
22
import { Memory } from '@mastra/memory';
33

4-
import {
5-
addCustomCatalogInstructions,
6-
renderA2uiTool,
7-
} from '../../../../libs/ag-ui-server/index.js';
8-
import { aggregateDataTool } from '../tools/aggregate-data.js';
9-
import { findBookedFlightsTool } from '../tools/find-booked-flights.js';
10-
import { renderChartTool } from '../tools/render-chart.js';
11-
import { renderFlightChartTool } from '../tools/render-flight-chart.js';
12-
import { searchFlightsTool } from '../tools/search-flights.js';
13-
import { searchHotelsTool } from '../tools/search-hotels.js';
14-
import { searchRentalCarsTool } from '../tools/search-rental-cars.js';
15-
import { weatherForecastTool } from '../tools/weather-forecast.js';
4+
import { renderDashboardTool } from '../tools/render-dashboard.js';
165
import { dashboardAgentPrompt } from './dashboard-agent.prompt.js';
176

187
export const dashboardAgent = new Agent({
198
id: 'dashboardAgent',
209
name: 'Flight42 Dashboard Composer',
21-
instructions: addCustomCatalogInstructions({
22-
systemInstructions: dashboardAgentPrompt,
23-
log: false,
24-
}),
10+
instructions: dashboardAgentPrompt,
2511
model: 'openai/gpt-5.3-chat-latest',
26-
tools: {
27-
searchFlightsTool,
28-
aggregateDataTool,
29-
weatherForecastTool,
30-
findBookedFlightsTool,
31-
renderChartTool,
32-
renderFlightChartTool,
33-
searchRentalCarsTool,
34-
searchHotelsTool,
35-
renderA2uiTool,
36-
},
37-
// A typical dashboard request issues many tool calls before the final
38-
// `renderA2uiTool`: searchFlights, several aggregateData runs, one or two
39-
// renderChart calls, optionally weatherForecast and findBookedFlights, and
40-
// finally renderA2uiTool. Mastra's default step limit is 5, which makes
41-
// the agent stop right before the rendering step and produces an empty
42-
// dashboard. `defaultOptions` is the option bag for the new
43-
// `agent.stream()` / `agent.generate()` APIs in Mastra >= 1.x; the
44-
// `*Legacy` variants only apply to `streamLegacy()` / `generateLegacy()`
45-
// and are silently ignored by the AG-UI route.
46-
defaultOptions: { maxSteps: 20 },
12+
tools: { renderDashboardTool },
13+
// The agent's only job is to issue ONE renderDashboard tool call,
14+
// so a small step budget is plenty.
15+
defaultOptions: { maxSteps: 3 },
4716
memory: new Memory(),
4817
});

ai-server/src/mastra/agents/dashboard-data-agent.prompt.ts

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

ai-server/src/mastra/agents/dashboard-data-agent.ts

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

ai-server/src/mastra/cache/dashboard-cache.ts

Lines changed: 24 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,32 @@ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
33
import { dirname, join, resolve } from 'node:path';
44
import { fileURLToPath } from 'node:url';
55

6+
import {
7+
type DashboardSpec,
8+
dashboardSpecSchema,
9+
} from '../dashboard-dsl/dashboard-spec.js';
10+
611
// File-system based cache for the dashboard agent. Resolves to
712
// `<repo>/ai-server/cache/` relative to this source file so the location is
813
// stable regardless of which directory `mastra dev` is launched from.
14+
//
15+
// Since the move to the dashboard DSL we cache only the parsed
16+
// `DashboardSpec`. A2UI structural ops are recompiled deterministically
17+
// from the spec by `compileDashboard` on every refresh, and the data
18+
// ops are reproduced fresh from the live data sources. The .json
19+
// extension is new on purpose so old `.a2ui.txt` files (which used a
20+
// different layout) no longer collide.
921
const SOURCE_DIR = dirname(fileURLToPath(import.meta.url));
1022
const CACHE_DIR = resolve(SOURCE_DIR, '../../../cache');
11-
const FILE_SUFFIX = '.a2ui.txt';
23+
const FILE_SUFFIX = '.dashboard.json';
1224

1325
interface RequestMessage {
1426
readonly role: string;
1527
readonly content?: unknown;
1628
}
1729

1830
export interface DashboardCacheEntry {
19-
/**
20-
* Surface-shaping operations: `createSurface` + `updateComponents`.
21-
* Re-emitted as-is on every cache hit so the renderer rebuilds the
22-
* component tree.
23-
*/
24-
structural: unknown[];
25-
/**
26-
* Initial `updateDataModel` operations from the original turn. Used as
27-
* a template for the delta-refresh agent: it sees these paths and
28-
* re-emits them with fresh values.
29-
*/
30-
dataModel: unknown[];
31-
/**
32-
* The surface id used by `structural` and `dataModel`. Convenience
33-
* cache; could also be derived from the operations list.
34-
*/
35-
surfaceId: string;
36-
}
37-
38-
interface MaybeOperation {
39-
readonly createSurface?: { readonly surfaceId?: unknown };
40-
readonly updateComponents?: { readonly surfaceId?: unknown };
41-
readonly updateDataModel?: unknown;
42-
readonly deleteSurface?: unknown;
31+
spec: DashboardSpec;
4332
}
4433

4534
export function computeDashboardRequestHash(
@@ -82,12 +71,9 @@ export async function readDashboardCache(
8271

8372
export async function writeDashboardCache(
8473
hash: string,
85-
operations: readonly unknown[],
86-
): Promise<DashboardCacheEntry | null> {
87-
const entry = splitA2uiOperations(operations);
88-
if (!entry) {
89-
return null;
90-
}
74+
spec: DashboardSpec,
75+
): Promise<DashboardCacheEntry> {
76+
const entry: DashboardCacheEntry = { spec };
9177
await mkdir(CACHE_DIR, { recursive: true });
9278
await writeFile(
9379
getCacheFilePath(hash),
@@ -97,66 +83,19 @@ export async function writeDashboardCache(
9783
return entry;
9884
}
9985

100-
/**
101-
* Splits a fresh A2UI operations array into the structural part
102-
* (`createSurface` + `updateComponents`) and the initial data-model
103-
* part (`updateDataModel`). Returns `null` when the operations don't
104-
* contain a `createSurface` (in which case caching makes no sense).
105-
*/
106-
export function splitA2uiOperations(
107-
operations: readonly unknown[],
108-
): DashboardCacheEntry | null {
109-
const structural: unknown[] = [];
110-
const dataModel: unknown[] = [];
111-
let surfaceId: string | null = null;
112-
113-
for (const op of operations) {
114-
if (!op || typeof op !== 'object') {
115-
continue;
116-
}
117-
const candidate = op as MaybeOperation;
118-
if (candidate.createSurface) {
119-
structural.push(op);
120-
const id = candidate.createSurface.surfaceId;
121-
if (typeof id === 'string') {
122-
surfaceId = id;
123-
}
124-
continue;
125-
}
126-
if (candidate.updateComponents) {
127-
structural.push(op);
128-
continue;
129-
}
130-
if (candidate.updateDataModel) {
131-
dataModel.push(op);
132-
continue;
133-
}
134-
}
135-
136-
if (!surfaceId || structural.length === 0) {
137-
return null;
138-
}
139-
140-
return { structural, dataModel, surfaceId };
141-
}
142-
14386
function toCacheEntry(value: unknown): DashboardCacheEntry | null {
14487
if (!value || typeof value !== 'object') {
14588
return null;
14689
}
147-
const candidate = value as Partial<DashboardCacheEntry>;
148-
if (
149-
!Array.isArray(candidate.structural) ||
150-
!Array.isArray(candidate.dataModel) ||
151-
typeof candidate.surfaceId !== 'string'
152-
) {
90+
const candidate = value as { spec?: unknown };
91+
if (!candidate.spec) {
92+
return null;
93+
}
94+
const result = dashboardSpecSchema.safeParse(candidate.spec);
95+
if (!result.success) {
15396
return null;
15497
}
155-
return {
156-
structural: candidate.structural,
157-
dataModel: candidate.dataModel,
158-
surfaceId: candidate.surfaceId,
159-
};
98+
return { spec: result.data };
16099
}
161100

162101
function getCacheFilePath(hash: string): string {

0 commit comments

Comments
 (0)