@dbx-tools/appkit-mastra 0.1.4 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/src/agents.d.ts +1 -1
- package/dist/src/agents.js +43 -19
- package/dist/src/chart.d.ts +170 -0
- package/dist/src/chart.js +491 -0
- package/dist/src/config.d.ts +13 -0
- package/dist/src/genie.d.ts +36 -14
- package/dist/src/genie.js +434 -75
- package/dist/src/history.d.ts +67 -0
- package/dist/src/history.js +172 -0
- package/dist/src/memory.js +15 -2
- package/dist/src/model.js +18 -14
- package/dist/src/plugin.d.ts +11 -1
- package/dist/src/plugin.js +28 -2
- package/dist/src/processors/strip-stale-charts.d.ts +29 -0
- package/dist/src/processors/strip-stale-charts.js +96 -0
- package/dist/src/server.d.ts +4 -0
- package/dist/src/server.js +59 -45
- package/dist/src/serving.js +19 -2
- package/dist/src/tools/email.d.ts +74 -0
- package/dist/src/tools/email.js +122 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.ts +2 -0
- package/package.json +21 -25
- package/src/agents.ts +46 -21
- package/src/chart.ts +593 -0
- package/src/config.ts +13 -0
- package/src/genie.ts +499 -102
- package/src/history.ts +210 -0
- package/src/memory.ts +19 -2
- package/src/model.ts +18 -13
- package/src/plugin.ts +30 -2
- package/src/processors/strip-stale-charts.ts +105 -0
- package/src/server.ts +76 -51
- package/src/serving.ts +21 -2
- package/src/tools/email.ts +147 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart-rendering primitives.
|
|
3
|
+
*
|
|
4
|
+
* Three surfaces, one shared brain:
|
|
5
|
+
*
|
|
6
|
+
* - {@link buildRenderDataTool}: a Mastra tool the model calls
|
|
7
|
+
* ("here is a dataset, render it as a chart"). The tool's
|
|
8
|
+
* `execute` emits a `kind: "chart"` event with the raw rows to
|
|
9
|
+
* `ctx.writer` synchronously, kicks off the chart-planner agent,
|
|
10
|
+
* and `await`s the planner promise before returning so the
|
|
11
|
+
* planner's latency is attributed to this tool's trace span.
|
|
12
|
+
* The LLM-bound output is just `{ chartId }`, so its context
|
|
13
|
+
* stays flat regardless of dataset size.
|
|
14
|
+
*
|
|
15
|
+
* - {@link emitChartWithPlanning}: the underlying helper that both
|
|
16
|
+
* `render_data` and Genie's `drainGenieStream` call. Mints the
|
|
17
|
+
* `chartId`, fires the dataset event immediately, runs the
|
|
18
|
+
* planner in the background, and returns `{ chartId,
|
|
19
|
+
* plannerPromise }` so callers can choose to await for trace
|
|
20
|
+
* shape or fire-and-forget.
|
|
21
|
+
*
|
|
22
|
+
* - {@link runChartPlanner}: the chart-planner Agent + ECOption
|
|
23
|
+
* expansion as a plain async function. Used internally by
|
|
24
|
+
* {@link emitChartWithPlanning}; producers shouldn't reach for
|
|
25
|
+
* it directly so chart events keep a single wire-format
|
|
26
|
+
* contract.
|
|
27
|
+
*
|
|
28
|
+
* The model wires the chart into its reply by emitting the marker
|
|
29
|
+
* `[[chart:<chartId>]]` on its own line in markdown. The chat
|
|
30
|
+
* client splits the assistant text on these markers and drops a
|
|
31
|
+
* `<ChartSlot>` in at the position the model placed it; the slot
|
|
32
|
+
* shows a skeleton until the second `kind: "chart"` event (with
|
|
33
|
+
* the resolved `EChartsOption`) arrives, then swaps in the
|
|
34
|
+
* rendered Echarts visualisation.
|
|
35
|
+
*/
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
37
|
+
import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
|
|
38
|
+
import { Agent } from "@mastra/core/agent";
|
|
39
|
+
import { createTool } from "@mastra/core/tools";
|
|
40
|
+
import { z } from "zod";
|
|
41
|
+
import { ModelTier, modelForTier, buildModel } from "./model.js";
|
|
42
|
+
/**
|
|
43
|
+
* Module-level logger tagged `[mastra/chart]`. Uses the shared
|
|
44
|
+
* {@link logUtils.logger} so calls below `LOG_LEVEL` are
|
|
45
|
+
* discarded for free. Default `LOG_LEVEL` is `info`; flip to
|
|
46
|
+
* `debug` to see the per-chart timeline (`emit:start` →
|
|
47
|
+
* `write:ok(data)` → `planner:done` → `write:ok(option)`).
|
|
48
|
+
*/
|
|
49
|
+
const log = logUtils.logger("mastra/chart");
|
|
50
|
+
/**
|
|
51
|
+
* Compact, model-friendly representation of an Echarts spec. The
|
|
52
|
+
* planner agent emits this; {@link planToEchartsOption} expands it
|
|
53
|
+
* into a real `EChartsOption` JSON. Two layers because letting the
|
|
54
|
+
* model fill in a fully-typed `EChartsOption` is brittle (hundreds
|
|
55
|
+
* of optional fields, deep unions, version-dependent shapes). A
|
|
56
|
+
* small "chart plan" schema is much more reliable for a fast model
|
|
57
|
+
* and keeps animation / tooltip / styling defaults consistent
|
|
58
|
+
* across charts.
|
|
59
|
+
*/
|
|
60
|
+
const chartPlanSchema = z.object({
|
|
61
|
+
chartType: z
|
|
62
|
+
.enum(["bar", "line", "area", "scatter", "pie"])
|
|
63
|
+
.describe(stringUtils.toDescription `
|
|
64
|
+
The chart shape that best matches the data and intent. Use
|
|
65
|
+
\`bar\` for category-vs-value comparisons, \`line\` for
|
|
66
|
+
trends over an ordered axis, \`area\` for stacked-trend
|
|
67
|
+
emphasis, \`scatter\` for two-numeric-axis correlations,
|
|
68
|
+
\`pie\` for parts-of-a-whole when categories are few.
|
|
69
|
+
`),
|
|
70
|
+
title: z.string().optional().describe(stringUtils.toDescription `
|
|
71
|
+
Short title shown above the chart. Optional; defaults to the
|
|
72
|
+
\`title\` argument the caller passed in.
|
|
73
|
+
`),
|
|
74
|
+
xAxisLabel: z.string().optional().describe(stringUtils.toDescription `
|
|
75
|
+
Axis label below the chart. Used for bar / line / area /
|
|
76
|
+
scatter; ignored for pie.
|
|
77
|
+
`),
|
|
78
|
+
yAxisLabel: z.string().optional().describe(stringUtils.toDescription `
|
|
79
|
+
Axis label to the left of the chart. Used for bar / line /
|
|
80
|
+
area / scatter; ignored for pie.
|
|
81
|
+
`),
|
|
82
|
+
categories: z
|
|
83
|
+
.array(z.string())
|
|
84
|
+
.optional()
|
|
85
|
+
.describe(stringUtils.toDescription `
|
|
86
|
+
X-axis category labels for \`bar\` / \`line\` / \`area\`
|
|
87
|
+
charts (one per data point in each series). Omit for
|
|
88
|
+
\`scatter\` (uses [x, y] tuples) and \`pie\` (each slice
|
|
89
|
+
carries its own \`name\`).
|
|
90
|
+
`),
|
|
91
|
+
series: z
|
|
92
|
+
.array(z.object({
|
|
93
|
+
name: z.string().describe(stringUtils.toDescription `
|
|
94
|
+
Legend name for this series.
|
|
95
|
+
`),
|
|
96
|
+
data: z
|
|
97
|
+
.array(z.union([
|
|
98
|
+
z.number(),
|
|
99
|
+
z.tuple([z.number(), z.number()]),
|
|
100
|
+
z.object({
|
|
101
|
+
name: z.string(),
|
|
102
|
+
value: z.number(),
|
|
103
|
+
}),
|
|
104
|
+
]))
|
|
105
|
+
.describe(stringUtils.toDescription `
|
|
106
|
+
Data points. For \`bar\` / \`line\` / \`area\`, an
|
|
107
|
+
array of numbers aligned to \`categories\`. For
|
|
108
|
+
\`scatter\`, an array of \`[x, y]\` numeric tuples.
|
|
109
|
+
For \`pie\`, an array of \`{name, value}\` objects.
|
|
110
|
+
`),
|
|
111
|
+
}))
|
|
112
|
+
.min(1)
|
|
113
|
+
.describe(stringUtils.toDescription `
|
|
114
|
+
One or more series to plot. Pie charts use exactly one
|
|
115
|
+
series; bar/line/area can stack multiple series sharing
|
|
116
|
+
the same \`categories\` axis.
|
|
117
|
+
`),
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* System prompt for the inner chart-planning agent. Tuned for a
|
|
121
|
+
* fast-tier model (Haiku, GPT-5-mini, Gemini Flash Lite).
|
|
122
|
+
*/
|
|
123
|
+
const CHART_PLANNER_INSTRUCTIONS = stringUtils.toDescription `
|
|
124
|
+
You design Apache Echarts visualizations. The user gives you a
|
|
125
|
+
tabular dataset (rows of objects) plus a title and an optional
|
|
126
|
+
description of the intent. You produce a small chart plan
|
|
127
|
+
(chart type, axis labels, categories, series) that best
|
|
128
|
+
conveys the data.
|
|
129
|
+
|
|
130
|
+
Decision guide:
|
|
131
|
+
|
|
132
|
+
- bar: comparing a numeric value across a small/medium set of
|
|
133
|
+
discrete categories (top-N, ranking, group-by).
|
|
134
|
+
- line: ordered-axis trend (time series, sequence).
|
|
135
|
+
- area: same as line but emphasises magnitude or stacked
|
|
136
|
+
composition.
|
|
137
|
+
- scatter: two numeric axes, correlation between fields.
|
|
138
|
+
- pie: parts of a whole when 2-7 categories sum to a
|
|
139
|
+
meaningful total.
|
|
140
|
+
|
|
141
|
+
When in doubt between bar and line, prefer bar for unordered
|
|
142
|
+
categories and line for ordered ones (dates, time buckets,
|
|
143
|
+
ranks). Never pick pie for more than 7 slices.
|
|
144
|
+
|
|
145
|
+
For bar / line / area: pick one column as the category axis
|
|
146
|
+
(usually the only string-valued column) and one or more
|
|
147
|
+
numeric columns as series. Sort categories by the primary
|
|
148
|
+
series value descending unless the data is naturally ordered
|
|
149
|
+
(dates, ranks).
|
|
150
|
+
|
|
151
|
+
For pie: pick the category column for slice names and one
|
|
152
|
+
numeric column for slice values. Emit a single series.
|
|
153
|
+
|
|
154
|
+
For scatter: pick two numeric columns and emit \`[x, y]\`
|
|
155
|
+
tuples in a single series.
|
|
156
|
+
|
|
157
|
+
Keep series names human-readable (use the column name; title
|
|
158
|
+
case it lightly if needed). Keep titles concise; do not
|
|
159
|
+
repeat the user's title in xAxisLabel / yAxisLabel.
|
|
160
|
+
`;
|
|
161
|
+
/**
|
|
162
|
+
* Lazily-constructed inner agent shared across all calls in this
|
|
163
|
+
* process. The agent is stateless (no memory, no tools) so a
|
|
164
|
+
* single instance per plugin config is safe; model resolution
|
|
165
|
+
* still happens per-call against the live `requestContext`, so
|
|
166
|
+
* OBO auth stays user-scoped.
|
|
167
|
+
*/
|
|
168
|
+
function createChartPlannerAgent(config) {
|
|
169
|
+
return new Agent({
|
|
170
|
+
id: "render_chart_planner",
|
|
171
|
+
name: "Chart Planner",
|
|
172
|
+
description: "Picks chart type and axis encodings for a dataset.",
|
|
173
|
+
instructions: CHART_PLANNER_INSTRUCTIONS,
|
|
174
|
+
model: ({ requestContext }) => buildModel(config, requestContext, {
|
|
175
|
+
modelId: modelForTier(ModelTier.Fast),
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Module-level cache: one chart-planner agent per plugin config
|
|
181
|
+
* instance. Keyed on the config object identity since each plugin
|
|
182
|
+
* mount provides its own resolver / fallbacks. Re-used across
|
|
183
|
+
* tool invocations and the render-chart HTTP route.
|
|
184
|
+
*/
|
|
185
|
+
const _plannerByConfig = new WeakMap();
|
|
186
|
+
function getPlannerAgent(config) {
|
|
187
|
+
let agent = _plannerByConfig.get(config);
|
|
188
|
+
if (!agent) {
|
|
189
|
+
agent = createChartPlannerAgent(config);
|
|
190
|
+
_plannerByConfig.set(config, agent);
|
|
191
|
+
}
|
|
192
|
+
return agent;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Run the chart planner against the given dataset and return a
|
|
196
|
+
* full Echarts `EChartsOption` JSON. Used by
|
|
197
|
+
* {@link emitChartWithPlanning}; tools and producers shouldn't
|
|
198
|
+
* call this directly (use the helper instead so chart events
|
|
199
|
+
* follow the same wire-format contract everywhere).
|
|
200
|
+
*/
|
|
201
|
+
export async function runChartPlanner(opts) {
|
|
202
|
+
const { config, requestContext, title, description, data } = opts;
|
|
203
|
+
const planner = getPlannerAgent(config);
|
|
204
|
+
const prompt = [
|
|
205
|
+
`Title: ${title}`,
|
|
206
|
+
description ? `Intent: ${description}` : null,
|
|
207
|
+
"",
|
|
208
|
+
"Dataset (JSON, one row per object):",
|
|
209
|
+
"```json",
|
|
210
|
+
JSON.stringify(data, null, 2),
|
|
211
|
+
"```",
|
|
212
|
+
]
|
|
213
|
+
.filter((line) => line !== null)
|
|
214
|
+
.join("\n");
|
|
215
|
+
const result = await planner.generate(prompt, {
|
|
216
|
+
structuredOutput: { schema: chartPlanSchema },
|
|
217
|
+
...(requestContext ? { requestContext } : {}),
|
|
218
|
+
});
|
|
219
|
+
const plan = result.object;
|
|
220
|
+
const option = planToEchartsOption(plan, title);
|
|
221
|
+
return { option, chartType: plan.chartType };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Shared chart-emission primitive used by both the `render_data`
|
|
225
|
+
* tool and Genie's `drainGenieStream`. Keeps both producers on
|
|
226
|
+
* one wire-format contract so the chat client only ever has to
|
|
227
|
+
* understand a single chart event shape.
|
|
228
|
+
*
|
|
229
|
+
* Behaviour:
|
|
230
|
+
*
|
|
231
|
+
* 1. Generates a short `chartId` (8 hex chars).
|
|
232
|
+
* 2. Immediately emits `{ kind: "chart", chartId, title,
|
|
233
|
+
* description?, data }` via the writer so the chat client can
|
|
234
|
+
* mount its `<ChartSlot>` with the rows in hand.
|
|
235
|
+
* 3. Kicks off the chart-planner agent in the background. On
|
|
236
|
+
* success, emits a second `{ kind: "chart", chartId, option }`
|
|
237
|
+
* event - same `chartId`, just the spec - so the client merges
|
|
238
|
+
* the two into one rendered chart. On failure, no follow-up
|
|
239
|
+
* event fires; the client falls back to whatever it can do
|
|
240
|
+
* with the dataset alone (typically a "render failed" frame
|
|
241
|
+
* after the parent tool finishes).
|
|
242
|
+
*
|
|
243
|
+
* Returns `chartId` synchronously so the caller can include it in
|
|
244
|
+
* the tool result (model uses it in `[[chart:<chartId>]]`
|
|
245
|
+
* markers), and `plannerPromise` so the caller can choose
|
|
246
|
+
* trace-spanning vs. snappy-return semantics.
|
|
247
|
+
*/
|
|
248
|
+
export async function emitChartWithPlanning(opts) {
|
|
249
|
+
const { writer, config, requestContext, title, description, data } = opts;
|
|
250
|
+
// Short, marker-friendly id. The LLM types this verbatim into
|
|
251
|
+
// `[[chart:<id>]]`; an 8-hex-char prefix is unique within a
|
|
252
|
+
// single assistant turn (collision odds ~1 in 4 billion) and
|
|
253
|
+
// much less error-prone for the model to reproduce.
|
|
254
|
+
const chartId = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
255
|
+
log.debug("emit:start", {
|
|
256
|
+
chartId,
|
|
257
|
+
title,
|
|
258
|
+
rows: data.length,
|
|
259
|
+
columns: data[0] ? Object.keys(data[0]) : [],
|
|
260
|
+
hasWriter: writer !== undefined,
|
|
261
|
+
});
|
|
262
|
+
// Initial event: rows + metadata, no option yet. The client
|
|
263
|
+
// mounts a chart slot that shows a skeleton until the option
|
|
264
|
+
// event arrives (or until the parent tool finishes without
|
|
265
|
+
// one, in which case it falls back).
|
|
266
|
+
await safeWrite(writer, chartId, "data", {
|
|
267
|
+
kind: "chart",
|
|
268
|
+
chartId,
|
|
269
|
+
title,
|
|
270
|
+
...(description ? { description } : {}),
|
|
271
|
+
data,
|
|
272
|
+
});
|
|
273
|
+
// Background planner. Awaitable for trace observability via the
|
|
274
|
+
// returned `plannerPromise`; safe to ignore for pure
|
|
275
|
+
// fire-and-forget. Failures are intentionally swallowed (only
|
|
276
|
+
// logged): the dataset event already landed, so the client has
|
|
277
|
+
// enough to surface a fallback.
|
|
278
|
+
const plannerPromise = (async () => {
|
|
279
|
+
const startedAt = Date.now();
|
|
280
|
+
try {
|
|
281
|
+
const { option, chartType } = await runChartPlanner({
|
|
282
|
+
config,
|
|
283
|
+
...(requestContext ? { requestContext } : {}),
|
|
284
|
+
title,
|
|
285
|
+
...(description ? { description } : {}),
|
|
286
|
+
data,
|
|
287
|
+
});
|
|
288
|
+
log.debug("planner:done", {
|
|
289
|
+
chartId,
|
|
290
|
+
chartType,
|
|
291
|
+
elapsedMs: Date.now() - startedAt,
|
|
292
|
+
});
|
|
293
|
+
await safeWrite(writer, chartId, "option", { kind: "chart", chartId, option });
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
// No follow-up event on failure. The client treats a
|
|
297
|
+
// dataset-only chart slot as "render failed" once the
|
|
298
|
+
// parent tool's status flips to done. Surface as a `warn`
|
|
299
|
+
// so the failure is visible at the default log level
|
|
300
|
+
// without being mistaken for a fatal error.
|
|
301
|
+
log.warn("planner:error", {
|
|
302
|
+
chartId,
|
|
303
|
+
elapsedMs: Date.now() - startedAt,
|
|
304
|
+
error: err instanceof Error ? err.message : String(err),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
})();
|
|
308
|
+
return { chartId, plannerPromise };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Best-effort writer.write. Failures are logged at `warn` (a
|
|
312
|
+
* persistently-closed writer is the most likely culprit when
|
|
313
|
+
* chart events go missing client-side) but swallowed so a closed
|
|
314
|
+
* downstream stream (cancelled request, client navigated away)
|
|
315
|
+
* can't take a tool down.
|
|
316
|
+
*/
|
|
317
|
+
async function safeWrite(writer, chartId, phase, chunk) {
|
|
318
|
+
if (!writer) {
|
|
319
|
+
log.debug("write:no-writer", { chartId, phase });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
await writer.write(chunk);
|
|
324
|
+
log.debug("write:ok", { chartId, phase });
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
log.warn("write:error", {
|
|
328
|
+
chartId,
|
|
329
|
+
phase,
|
|
330
|
+
error: err instanceof Error ? err.message : String(err),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const renderDataInputSchema = z.object({
|
|
335
|
+
title: z.string().describe(stringUtils.toDescription `
|
|
336
|
+
Title shown above the rendered chart. Use a concise
|
|
337
|
+
sentence-case label (e.g. "Top 10 SKUs by On-Hand Units").
|
|
338
|
+
`),
|
|
339
|
+
description: z.string().optional().describe(stringUtils.toDescription `
|
|
340
|
+
Optional one-line intent describing what insight the chart
|
|
341
|
+
should convey (e.g. "highlight the steep drop-off after
|
|
342
|
+
position 5", "compare quarterly revenue across regions").
|
|
343
|
+
The chart-planner reads this when picking the chart type and
|
|
344
|
+
axis encodings; the user does not see it directly.
|
|
345
|
+
`),
|
|
346
|
+
data: z
|
|
347
|
+
.array(z.record(z.string(), z.unknown()))
|
|
348
|
+
.min(1)
|
|
349
|
+
.describe(stringUtils.toDescription `
|
|
350
|
+
Tabular dataset to chart. One object per row, keyed by
|
|
351
|
+
column name. Values may be strings, numbers, booleans, or
|
|
352
|
+
null. The chart-planner decides which columns are
|
|
353
|
+
categories vs. numeric series. Cap at a few hundred rows
|
|
354
|
+
for legibility; sample / aggregate larger datasets first.
|
|
355
|
+
`),
|
|
356
|
+
});
|
|
357
|
+
const renderDataOutputSchema = z.object({
|
|
358
|
+
chartId: z.string().describe(stringUtils.toDescription `
|
|
359
|
+
Identifier of the queued chart. To position the chart in
|
|
360
|
+
your reply, embed the marker \`[[chart:<chartId>]]\` on its
|
|
361
|
+
own line where the chart should appear; the client renders
|
|
362
|
+
it inline.
|
|
363
|
+
`),
|
|
364
|
+
});
|
|
365
|
+
/**
|
|
366
|
+
* Build the `render_data` tool bound to the given plugin config.
|
|
367
|
+
*
|
|
368
|
+
* The tool is a thin wrapper around {@link emitChartWithPlanning}:
|
|
369
|
+
* a single `kind: "chart"` writer event ships the raw rows to
|
|
370
|
+
* the client immediately, the chart-planner agent runs alongside
|
|
371
|
+
* (so the calling LLM stays unblocked while the planner thinks),
|
|
372
|
+
* and a follow-up `kind: "chart"` event with the resolved
|
|
373
|
+
* `EChartsOption` lands when it's ready. The tool's `execute`
|
|
374
|
+
* awaits the planner promise so the planner work shows up under
|
|
375
|
+
* the tool's trace span; the LLM still gets back just
|
|
376
|
+
* `{ chartId }`, so its context stays small regardless of dataset
|
|
377
|
+
* size.
|
|
378
|
+
*/
|
|
379
|
+
export function buildRenderDataTool(config) {
|
|
380
|
+
return createTool({
|
|
381
|
+
id: "render_data",
|
|
382
|
+
description: stringUtils.toDescription `
|
|
383
|
+
Submit a tabular dataset for inline rendering as a chart in
|
|
384
|
+
the user's view. Pass a title, the raw rows (array of
|
|
385
|
+
objects keyed by column name), and an optional one-line
|
|
386
|
+
description of the insight to highlight. Returns a short
|
|
387
|
+
\`chartId\`; the chart renders inline at the position you
|
|
388
|
+
embed the matching \`[[chart:<chartId>]]\` marker.
|
|
389
|
+
|
|
390
|
+
Placement contract: embed \`[[chart:<chartId>]]\` on its own
|
|
391
|
+
line (blank lines above and below) wherever you want the
|
|
392
|
+
chart to appear in your reply. The client shows a skeleton
|
|
393
|
+
at that spot until the chart is ready, then swaps in the
|
|
394
|
+
rendered Echarts visualization. You can call
|
|
395
|
+
\`render_data\` multiple times in the same turn (the tool
|
|
396
|
+
is parallel-safe) and interleave the markers with prose so
|
|
397
|
+
each chart sits next to its commentary. A chart whose
|
|
398
|
+
marker is omitted falls through to the end of your reply
|
|
399
|
+
as a fallback - safe but less polished.
|
|
400
|
+
|
|
401
|
+
Use whenever a SQL row set, API response, or hand-built
|
|
402
|
+
dataset would land better as a picture than as a list or
|
|
403
|
+
table. Cap input at a few hundred rows; sample or
|
|
404
|
+
aggregate larger datasets first.
|
|
405
|
+
`,
|
|
406
|
+
inputSchema: renderDataInputSchema,
|
|
407
|
+
outputSchema: renderDataOutputSchema,
|
|
408
|
+
execute: async (input, ctx) => {
|
|
409
|
+
const { title, description, data } = input;
|
|
410
|
+
const writer = ctx?.writer;
|
|
411
|
+
const requestContext = ctx
|
|
412
|
+
?.requestContext;
|
|
413
|
+
const { chartId, plannerPromise } = await emitChartWithPlanning({
|
|
414
|
+
...(writer ? { writer } : {}),
|
|
415
|
+
config,
|
|
416
|
+
...(requestContext ? { requestContext } : {}),
|
|
417
|
+
title,
|
|
418
|
+
...(description ? { description } : {}),
|
|
419
|
+
data,
|
|
420
|
+
});
|
|
421
|
+
// Await the planner so its latency is attributed to this
|
|
422
|
+
// tool's trace span. The promise itself swallows planner
|
|
423
|
+
// failures, so this never throws.
|
|
424
|
+
await plannerPromise;
|
|
425
|
+
return { chartId };
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Expand a {@link ChartPlan} into a full Echarts `EChartsOption`
|
|
431
|
+
* JSON. Centralized here so the planner agent only fills in the
|
|
432
|
+
* compact plan shape; tooltip / animation / color / grid defaults
|
|
433
|
+
* stay consistent across charts and are easy to tune without
|
|
434
|
+
* retraining model behaviour.
|
|
435
|
+
*/
|
|
436
|
+
function planToEchartsOption(plan, fallbackTitle) {
|
|
437
|
+
const baseTitle = plan.title ?? fallbackTitle;
|
|
438
|
+
const grid = { left: 48, right: 24, top: 56, bottom: 48, containLabel: true };
|
|
439
|
+
if (plan.chartType === "pie") {
|
|
440
|
+
return {
|
|
441
|
+
title: { text: baseTitle, left: "center" },
|
|
442
|
+
tooltip: { trigger: "item" },
|
|
443
|
+
legend: { bottom: 0 },
|
|
444
|
+
series: [
|
|
445
|
+
{
|
|
446
|
+
name: plan.series[0]?.name ?? baseTitle,
|
|
447
|
+
type: "pie",
|
|
448
|
+
radius: ["35%", "65%"],
|
|
449
|
+
data: plan.series[0]?.data ?? [],
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (plan.chartType === "scatter") {
|
|
455
|
+
return {
|
|
456
|
+
title: { text: baseTitle, left: "center" },
|
|
457
|
+
tooltip: { trigger: "item" },
|
|
458
|
+
legend: { bottom: 0 },
|
|
459
|
+
grid,
|
|
460
|
+
xAxis: { type: "value", name: plan.xAxisLabel },
|
|
461
|
+
yAxis: { type: "value", name: plan.yAxisLabel },
|
|
462
|
+
series: plan.series.map((s) => ({
|
|
463
|
+
name: s.name,
|
|
464
|
+
type: "scatter",
|
|
465
|
+
data: s.data,
|
|
466
|
+
})),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
// bar / line / area share the same axis layout.
|
|
470
|
+
const isArea = plan.chartType === "area";
|
|
471
|
+
const seriesType = plan.chartType === "bar" ? "bar" : "line";
|
|
472
|
+
return {
|
|
473
|
+
title: { text: baseTitle, left: "center" },
|
|
474
|
+
tooltip: { trigger: "axis" },
|
|
475
|
+
legend: { bottom: 0 },
|
|
476
|
+
grid,
|
|
477
|
+
xAxis: {
|
|
478
|
+
type: "category",
|
|
479
|
+
data: plan.categories ?? [],
|
|
480
|
+
name: plan.xAxisLabel,
|
|
481
|
+
},
|
|
482
|
+
yAxis: { type: "value", name: plan.yAxisLabel },
|
|
483
|
+
series: plan.series.map((s) => ({
|
|
484
|
+
name: s.name,
|
|
485
|
+
type: seriesType,
|
|
486
|
+
data: s.data,
|
|
487
|
+
smooth: seriesType === "line",
|
|
488
|
+
...(isArea ? { areaStyle: {} } : {}),
|
|
489
|
+
})),
|
|
490
|
+
};
|
|
491
|
+
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -152,6 +152,19 @@ export interface MastraPluginConfig extends BasePluginConfig {
|
|
|
152
152
|
* or to add custom endpoints in front of the public catalogue.
|
|
153
153
|
*/
|
|
154
154
|
defaultModelFallbacks?: readonly string[];
|
|
155
|
+
/**
|
|
156
|
+
* When `true` (default), every agent gets a built-in input
|
|
157
|
+
* processor that strips `chartId` fields from prior assistant
|
|
158
|
+
* tool-invocation results before they reach the model. This
|
|
159
|
+
* prevents the model from reusing turn-scoped chartIds it sees
|
|
160
|
+
* in memory recall (which would leave `[[chart:<id>]]` markers
|
|
161
|
+
* pointing at writer events that no longer exist).
|
|
162
|
+
*
|
|
163
|
+
* Set to `false` to opt out - useful if a non-default agent
|
|
164
|
+
* needs full visibility into prior chartIds (e.g. an audit
|
|
165
|
+
* agent reasoning about chart lineage).
|
|
166
|
+
*/
|
|
167
|
+
stripStaleCharts?: boolean;
|
|
155
168
|
/**
|
|
156
169
|
* Style guardrails appended to every agent's `instructions` to curb
|
|
157
170
|
* common LLM-isms (em dashes, emojis, sycophantic openers, throwaway
|
package/dist/src/genie.d.ts
CHANGED
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
* upstream change in `@databricks/appkit` flows in automatically.
|
|
12
12
|
*
|
|
13
13
|
* As Genie streams its long-running events (`FETCHING_METADATA` →
|
|
14
|
-
* `ASKING_AI` → `EXECUTING_QUERY` → `COMPLETED`, plus SQL
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* `ASKING_AI` → `EXECUTING_QUERY` → `COMPLETED`, plus SQL text and
|
|
15
|
+
* follow-ups in `message_result.attachments`), the tool forwards a
|
|
16
|
+
* normalised {@link GenieProgress} discriminated union out through
|
|
17
|
+
* `ctx.writer` so the client can render an incremental loading pill.
|
|
18
|
+
* Row payloads from `query_result` are intentionally discarded - the
|
|
19
|
+
* LLM never sees rows, and charts come from the separate
|
|
20
|
+
* `render_data` tool when the model decides one is useful.
|
|
20
21
|
*/
|
|
21
22
|
import { genie } from "@databricks/appkit";
|
|
22
23
|
import { createTool } from "@mastra/core/tools";
|
|
24
|
+
import type { MastraPluginConfig } from "./config.js";
|
|
23
25
|
/** Live AppKit `GeniePlugin` instance. */
|
|
24
26
|
export type GeniePluginInstance = InstanceType<ReturnType<typeof genie>["plugin"]>;
|
|
25
27
|
/** Full `exports()` shape of the AppKit `genie` plugin. */
|
|
@@ -33,10 +35,19 @@ export type GenieStreamEvent = ReturnType<GenieExports["sendMessage"]> extends A
|
|
|
33
35
|
/** Conversation history returned by `genie.exports().getConversation`. */
|
|
34
36
|
export type GenieConversation = Awaited<ReturnType<GenieExports["getConversation"]>>;
|
|
35
37
|
/**
|
|
36
|
-
* Normalised progress event surfaced to the UI as a Mastra
|
|
37
|
-
* chunk.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* Normalised progress event surfaced to the UI as a Mastra
|
|
39
|
+
* `tool-output` chunk. Loading pill events (`started`, `status`,
|
|
40
|
+
* `sql`, `suggested`, `error`) are pure UI metadata and never reach
|
|
41
|
+
* the LLM.
|
|
42
|
+
*
|
|
43
|
+
* The `chart` variant is the wire shape emitted by
|
|
44
|
+
* {@link emitChartWithPlanning} (used by both this Genie
|
|
45
|
+
* draining loop and the system-level `render_data` tool). All
|
|
46
|
+
* fields except `chartId` are optional because two events per
|
|
47
|
+
* chartId arrive on the wire: the first carries the rows
|
|
48
|
+
* (`title` + `description?` + `data`); the second, on planner
|
|
49
|
+
* success, carries just the resolved Echarts spec (`option`).
|
|
50
|
+
* The host UI's `<ChartSlot>` merges them by `chartId`.
|
|
40
51
|
*/
|
|
41
52
|
export type GenieProgress = {
|
|
42
53
|
kind: "started";
|
|
@@ -54,9 +65,12 @@ export type GenieProgress = {
|
|
|
54
65
|
description?: string;
|
|
55
66
|
statementId?: string;
|
|
56
67
|
} | {
|
|
57
|
-
kind: "
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
kind: "chart";
|
|
69
|
+
chartId: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
data?: Array<Record<string, unknown>>;
|
|
73
|
+
option?: Record<string, unknown>;
|
|
60
74
|
} | {
|
|
61
75
|
kind: "text";
|
|
62
76
|
content: string;
|
|
@@ -79,10 +93,16 @@ export declare function defaultGenieToolName(alias: string): string;
|
|
|
79
93
|
* Build one `sendMessage` tool per configured Genie alias plus a single
|
|
80
94
|
* `getConversation` tool. Returns a record keyed by tool id, ready to
|
|
81
95
|
* spread into an `Agent`'s `tools` map.
|
|
96
|
+
*
|
|
97
|
+
* `config` must be the active plugin config; Genie's
|
|
98
|
+
* `query_result` events are routed through
|
|
99
|
+
* {@link emitChartWithPlanning} which uses it to resolve the
|
|
100
|
+
* chart-planner's model.
|
|
82
101
|
*/
|
|
83
102
|
export declare function buildGenieTools(opts: {
|
|
84
103
|
aliases: string[];
|
|
85
104
|
exports: GenieExports;
|
|
105
|
+
config: MastraPluginConfig;
|
|
86
106
|
signal?: AbortSignal;
|
|
87
107
|
}): Record<string, ReturnType<typeof createTool>>;
|
|
88
108
|
/**
|
|
@@ -104,6 +124,8 @@ export declare function buildGenieTools(opts: {
|
|
|
104
124
|
* all-or-nothing bundle. Wire `only` / `except` / `prefix` / `rename`
|
|
105
125
|
* later if a caller needs them.
|
|
106
126
|
*/
|
|
107
|
-
export declare function buildGenieProvider(plugin: GeniePluginInstance
|
|
127
|
+
export declare function buildGenieProvider(plugin: GeniePluginInstance, opts: {
|
|
128
|
+
config: MastraPluginConfig;
|
|
129
|
+
}): {
|
|
108
130
|
toolkit(opts?: unknown): Record<string, ReturnType<typeof createTool>>;
|
|
109
131
|
};
|