@dbx-tools/appkit-mastra 0.1.13 → 0.1.19
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 +109 -102
- package/dist/src/agents.d.ts +2 -2
- package/dist/src/agents.js +65 -14
- package/dist/src/chart.d.ts +39 -105
- package/dist/src/chart.js +177 -194
- package/dist/src/config.d.ts +104 -0
- package/dist/src/config.js +43 -0
- package/dist/src/genie.d.ts +169 -107
- package/dist/src/genie.js +983 -577
- package/dist/src/history.d.ts +31 -3
- package/dist/src/history.js +137 -31
- package/dist/src/memory.d.ts +4 -4
- package/dist/src/memory.js +2 -2
- package/dist/src/model.js +2 -2
- package/dist/src/observability.d.ts +56 -25
- package/dist/src/observability.js +70 -56
- package/dist/src/plugin.js +25 -15
- package/dist/src/processors/strip-stale-charts.js +1 -1
- package/dist/src/server.d.ts +12 -0
- package/dist/src/server.js +38 -2
- package/dist/src/serving.js +1 -1
- package/dist/src/tools/email.js +1 -1
- package/dist/src/writer.d.ts +23 -0
- package/dist/src/writer.js +37 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +21 -18
- package/src/agents.ts +72 -17
- package/src/chart.ts +205 -251
- package/src/config.ts +120 -0
- package/src/genie.ts +1183 -658
- package/src/history.ts +147 -33
- package/src/memory.ts +5 -5
- package/src/model.ts +3 -3
- package/src/observability.ts +94 -70
- package/src/plugin.ts +25 -15
- package/src/processors/strip-stale-charts.ts +1 -1
- package/src/server.ts +49 -2
- package/src/serving.ts +1 -1
- package/src/tools/email.ts +1 -1
- package/src/writer.ts +44 -0
package/dist/src/chart.js
CHANGED
|
@@ -1,44 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chart-rendering primitives.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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.
|
|
4
|
+
* Two surfaces, one shared brain:
|
|
21
5
|
*
|
|
22
6
|
* - {@link runChartPlanner}: the chart-planner Agent + ECOption
|
|
23
|
-
* expansion as a plain async function.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
7
|
+
* expansion as a plain async function. Takes a dataset and
|
|
8
|
+
* returns a promise that resolves to a full `EChartsOption`
|
|
9
|
+
* JSON plus the chosen `chartType`. No background work, no
|
|
10
|
+
* writer side-effects, no id allocation - callers stitch the
|
|
11
|
+
* result into whatever shape their producer needs.
|
|
12
|
+
*
|
|
13
|
+
* - {@link buildRenderDataTool}: a Mastra tool the model calls
|
|
14
|
+
* ("here is a dataset, render it as a chart"). Mints a short
|
|
15
|
+
* `chartId`, `await`s {@link runChartPlanner} so the planner
|
|
16
|
+
* latency is attributed to this tool's trace span, emits one
|
|
17
|
+
* `type: "chart"` writer event carrying the dataset + resolved
|
|
18
|
+
* `option`, and returns `{ chartId }` to the model. The
|
|
19
|
+
* LLM-bound output stays flat regardless of dataset size.
|
|
27
20
|
*
|
|
28
21
|
* The model wires the chart into its reply by emitting the marker
|
|
29
22
|
* `[[chart:<chartId>]]` on its own line in markdown. The chat
|
|
30
23
|
* client splits the assistant text on these markers and drops a
|
|
31
|
-
* `<ChartSlot>` in at the position the model placed it
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
24
|
+
* `<ChartSlot>` in at the position the model placed it. The slot
|
|
25
|
+
* resolves directly to the rendered Echarts visualisation - no
|
|
26
|
+
* skeleton state, because the option is in the same event as the
|
|
27
|
+
* dataset.
|
|
35
28
|
*/
|
|
36
|
-
import {
|
|
37
|
-
import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
|
|
29
|
+
import { commonUtils, logUtils, stringUtils } from "@dbx-tools/shared";
|
|
38
30
|
import { Agent } from "@mastra/core/agent";
|
|
39
31
|
import { createTool } from "@mastra/core/tools";
|
|
40
32
|
import { z } from "zod";
|
|
41
33
|
import { ModelTier, modelForTier, buildModel } from "./model.js";
|
|
34
|
+
import { safeWrite } from "./writer.js";
|
|
42
35
|
/**
|
|
43
36
|
* Module-level logger tagged `[mastra/chart]`. Uses the shared
|
|
44
37
|
* {@link logUtils.logger} so calls below `LOG_LEVEL` are
|
|
@@ -47,6 +40,75 @@ import { ModelTier, modelForTier, buildModel } from "./model.js";
|
|
|
47
40
|
* `write:ok(data)` → `planner:done` → `write:ok(option)`).
|
|
48
41
|
*/
|
|
49
42
|
const log = logUtils.logger("mastra/chart");
|
|
43
|
+
/**
|
|
44
|
+
* One series data point. Wide variant set so the planner agent can
|
|
45
|
+
* faithfully pass through whatever the SQL row set contained
|
|
46
|
+
* (numbers, stringified numbers, nulls for missing measurements,
|
|
47
|
+
* `[x, y]` tuples for scatter, `{name, value}` slices for pie)
|
|
48
|
+
* without the structured-output guard rejecting the whole plan.
|
|
49
|
+
*
|
|
50
|
+
* Three layers of tolerance:
|
|
51
|
+
*
|
|
52
|
+
* 1. {@link z.preprocess} normalizes wire shapes BEFORE union
|
|
53
|
+
* dispatch: stringified numbers parse to numbers, finite
|
|
54
|
+
* checks reject `NaN` / `Infinity`, 2-element arrays coerce
|
|
55
|
+
* tuple components, and `{value}` objects with missing /
|
|
56
|
+
* stringified `value` get coerced or rejected uniformly.
|
|
57
|
+
* Anything not handleable becomes `null`.
|
|
58
|
+
* 2. The union accepts `null` as a first-class variant. Echarts
|
|
59
|
+
* renders null as a gap on bar / line / area (which is the
|
|
60
|
+
* right visual signal for "missing reading"). Scatter and
|
|
61
|
+
* pie filter nulls in {@link planToEchartsOption} because
|
|
62
|
+
* Echarts crashes on null tuples / slices.
|
|
63
|
+
* 3. {@link z.union#catch} backstops the whole thing: if
|
|
64
|
+
* preprocess somehow produces a shape that still doesn't
|
|
65
|
+
* match any variant, the bad item becomes `null` instead of
|
|
66
|
+
* taking down the entire chart with a
|
|
67
|
+
* `Structured output validation failed` error.
|
|
68
|
+
*
|
|
69
|
+
* Net effect: a 200-row dataset with a few sparse/null/string
|
|
70
|
+
* values still produces a chart; only a totally-malformed planner
|
|
71
|
+
* response (no items at all) falls through to the table fallback.
|
|
72
|
+
*/
|
|
73
|
+
const chartDataPointSchema = z
|
|
74
|
+
.preprocess((v) => {
|
|
75
|
+
if (v === null || v === undefined)
|
|
76
|
+
return null;
|
|
77
|
+
if (typeof v === "number")
|
|
78
|
+
return Number.isFinite(v) ? v : null;
|
|
79
|
+
if (typeof v === "string") {
|
|
80
|
+
const n = Number(v);
|
|
81
|
+
return Number.isFinite(n) ? n : null;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(v) && v.length === 2) {
|
|
84
|
+
const x = typeof v[0] === "number" ? v[0] : Number(v[0]);
|
|
85
|
+
const y = typeof v[1] === "number" ? v[1] : Number(v[1]);
|
|
86
|
+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
|
|
87
|
+
}
|
|
88
|
+
if (typeof v === "object" && v !== null && "value" in v) {
|
|
89
|
+
const obj = v;
|
|
90
|
+
const val = typeof obj.value === "number" ? obj.value : Number(obj.value);
|
|
91
|
+
if (!Number.isFinite(val))
|
|
92
|
+
return null;
|
|
93
|
+
// Coerce numeric / boolean / nullish names to strings so a
|
|
94
|
+
// pie slice keyed on a year (`2024`) or category id is
|
|
95
|
+
// accepted without round-tripping through the catch arm.
|
|
96
|
+
const rawName = obj.name;
|
|
97
|
+
const name = typeof rawName === "string"
|
|
98
|
+
? rawName
|
|
99
|
+
: rawName == null
|
|
100
|
+
? ""
|
|
101
|
+
: String(rawName);
|
|
102
|
+
return { name, value: val };
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}, z.union([
|
|
106
|
+
z.number(),
|
|
107
|
+
z.null(),
|
|
108
|
+
z.tuple([z.number(), z.number()]),
|
|
109
|
+
z.object({ name: z.string(), value: z.number() }),
|
|
110
|
+
]))
|
|
111
|
+
.catch(null);
|
|
50
112
|
/**
|
|
51
113
|
* Compact, model-friendly representation of an Echarts spec. The
|
|
52
114
|
* planner agent emits this; {@link planToEchartsOption} expands it
|
|
@@ -58,8 +120,7 @@ const log = logUtils.logger("mastra/chart");
|
|
|
58
120
|
* across charts.
|
|
59
121
|
*/
|
|
60
122
|
const chartPlanSchema = z.object({
|
|
61
|
-
chartType: z
|
|
62
|
-
.enum(["bar", "line", "area", "scatter", "pie"])
|
|
123
|
+
chartType: z.enum(["bar", "line", "area", "scatter", "pie"])
|
|
63
124
|
.describe(stringUtils.toDescription `
|
|
64
125
|
The chart shape that best matches the data and intent. Use
|
|
65
126
|
\`bar\` for category-vs-value comparisons, \`line\` for
|
|
@@ -79,10 +140,7 @@ const chartPlanSchema = z.object({
|
|
|
79
140
|
Axis label to the left of the chart. Used for bar / line /
|
|
80
141
|
area / scatter; ignored for pie.
|
|
81
142
|
`),
|
|
82
|
-
categories: z
|
|
83
|
-
.array(z.string())
|
|
84
|
-
.optional()
|
|
85
|
-
.describe(stringUtils.toDescription `
|
|
143
|
+
categories: z.array(z.string()).optional().describe(stringUtils.toDescription `
|
|
86
144
|
X-axis category labels for \`bar\` / \`line\` / \`area\`
|
|
87
145
|
charts (one per data point in each series). Omit for
|
|
88
146
|
\`scatter\` (uses [x, y] tuples) and \`pie\` (each slice
|
|
@@ -93,24 +151,14 @@ const chartPlanSchema = z.object({
|
|
|
93
151
|
name: z.string().describe(stringUtils.toDescription `
|
|
94
152
|
Legend name for this series.
|
|
95
153
|
`),
|
|
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 `
|
|
154
|
+
data: z.array(chartDataPointSchema).describe(stringUtils.toDescription `
|
|
106
155
|
Data points. For \`bar\` / \`line\` / \`area\`, an
|
|
107
156
|
array of numbers aligned to \`categories\`. For
|
|
108
157
|
\`scatter\`, an array of \`[x, y]\` numeric tuples.
|
|
109
158
|
For \`pie\`, an array of \`{name, value}\` objects.
|
|
110
159
|
`),
|
|
111
160
|
}))
|
|
112
|
-
.min(1)
|
|
113
|
-
.describe(stringUtils.toDescription `
|
|
161
|
+
.min(1).describe(stringUtils.toDescription `
|
|
114
162
|
One or more series to plot. Pie charts use exactly one
|
|
115
163
|
series; bar/line/area can stack multiple series sharing
|
|
116
164
|
the same \`categories\` axis.
|
|
@@ -193,13 +241,14 @@ function getPlannerAgent(config) {
|
|
|
193
241
|
}
|
|
194
242
|
/**
|
|
195
243
|
* Run the chart planner against the given dataset and return a
|
|
196
|
-
* full Echarts `EChartsOption` JSON.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
244
|
+
* full Echarts `EChartsOption` JSON. Pure async function: no
|
|
245
|
+
* writer side-effects, no id minting, no background work.
|
|
246
|
+
* Producers (the `render_data` tool, the Genie agent,
|
|
247
|
+
* anything else that needs a chart) await this and stitch the
|
|
248
|
+
* result into whatever shape their wire contract needs.
|
|
200
249
|
*/
|
|
201
250
|
export async function runChartPlanner(opts) {
|
|
202
|
-
const { config, requestContext, title, description, data } = opts;
|
|
251
|
+
const { config, requestContext, title, description, data, signal } = opts;
|
|
203
252
|
const planner = getPlannerAgent(config);
|
|
204
253
|
const prompt = [
|
|
205
254
|
`Title: ${title}`,
|
|
@@ -215,122 +264,12 @@ export async function runChartPlanner(opts) {
|
|
|
215
264
|
const result = await planner.generate(prompt, {
|
|
216
265
|
structuredOutput: { schema: chartPlanSchema },
|
|
217
266
|
...(requestContext ? { requestContext } : {}),
|
|
267
|
+
...(signal ? { abortSignal: signal } : {}),
|
|
218
268
|
});
|
|
219
269
|
const plan = result.object;
|
|
220
270
|
const option = planToEchartsOption(plan, title);
|
|
221
271
|
return { option, chartType: plan.chartType };
|
|
222
272
|
}
|
|
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
273
|
const renderDataInputSchema = z.object({
|
|
335
274
|
title: z.string().describe(stringUtils.toDescription `
|
|
336
275
|
Title shown above the rendered chart. Use a concise
|
|
@@ -343,9 +282,7 @@ const renderDataInputSchema = z.object({
|
|
|
343
282
|
The chart-planner reads this when picking the chart type and
|
|
344
283
|
axis encodings; the user does not see it directly.
|
|
345
284
|
`),
|
|
346
|
-
data: z
|
|
347
|
-
.array(z.record(z.string(), z.unknown()))
|
|
348
|
-
.min(1)
|
|
285
|
+
data: z.array(z.record(z.string(), z.unknown())).min(1)
|
|
349
286
|
.describe(stringUtils.toDescription `
|
|
350
287
|
Tabular dataset to chart. One object per row, keyed by
|
|
351
288
|
column name. Values may be strings, numbers, booleans, or
|
|
@@ -365,16 +302,14 @@ const renderDataOutputSchema = z.object({
|
|
|
365
302
|
/**
|
|
366
303
|
* Build the `render_data` tool bound to the given plugin config.
|
|
367
304
|
*
|
|
368
|
-
* The tool
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* `{ chartId }`, so its context stays small regardless of dataset
|
|
377
|
-
* size.
|
|
305
|
+
* The tool awaits {@link runChartPlanner} so the planner's
|
|
306
|
+
* latency is attributed to this tool's trace span, then emits
|
|
307
|
+
* one `type: "chart"` writer event carrying the dataset and the
|
|
308
|
+
* resolved `EChartsOption`. The LLM-bound output is just
|
|
309
|
+
* `{ chartId }` so the model's context stays flat regardless of
|
|
310
|
+
* dataset size. Planner failures are caught and surfaced as a
|
|
311
|
+
* `type: "error"` writer event so the slot can fall back to
|
|
312
|
+
* "couldn't render chart" without taking the parent agent down.
|
|
378
313
|
*/
|
|
379
314
|
export function buildRenderDataTool(config) {
|
|
380
315
|
return createTool({
|
|
@@ -389,14 +324,14 @@ export function buildRenderDataTool(config) {
|
|
|
389
324
|
|
|
390
325
|
Placement contract: embed \`[[chart:<chartId>]]\` on its own
|
|
391
326
|
line (blank lines above and below) wherever you want the
|
|
392
|
-
chart to appear in your reply. The
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
327
|
+
chart to appear in your reply. The chart is fully resolved
|
|
328
|
+
by the time the tool returns, so it renders immediately at
|
|
329
|
+
that spot. You can call \`render_data\` multiple times in
|
|
330
|
+
the same turn (the tool is parallel-safe) and interleave
|
|
331
|
+
the markers with prose so each chart sits next to its
|
|
332
|
+
commentary. A chart whose marker is omitted falls through
|
|
333
|
+
to the end of your reply as a fallback - safe but less
|
|
334
|
+
polished.
|
|
400
335
|
|
|
401
336
|
Use whenever a SQL row set, API response, or hand-built
|
|
402
337
|
dataset would land better as a picture than as a list or
|
|
@@ -410,18 +345,58 @@ export function buildRenderDataTool(config) {
|
|
|
410
345
|
const writer = ctx?.writer;
|
|
411
346
|
const requestContext = ctx
|
|
412
347
|
?.requestContext;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
348
|
+
// Marker-friendly short id. The LLM types this verbatim
|
|
349
|
+
// into `[[chart:<id>]]`; 8 hex chars is unique within a
|
|
350
|
+
// single assistant turn and easy for the model to copy.
|
|
351
|
+
const chartId = commonUtils.shortId();
|
|
352
|
+
const startedAt = Date.now();
|
|
353
|
+
log.debug("render:start", {
|
|
354
|
+
chartId,
|
|
417
355
|
title,
|
|
418
|
-
|
|
419
|
-
data,
|
|
356
|
+
rows: data.length,
|
|
357
|
+
columns: data[0] ? Object.keys(data[0]) : [],
|
|
358
|
+
hasWriter: writer !== undefined,
|
|
420
359
|
});
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
360
|
+
try {
|
|
361
|
+
const { option, chartType } = await runChartPlanner({
|
|
362
|
+
config,
|
|
363
|
+
...(requestContext ? { requestContext } : {}),
|
|
364
|
+
title,
|
|
365
|
+
...(description ? { description } : {}),
|
|
366
|
+
data,
|
|
367
|
+
});
|
|
368
|
+
log.debug("render:done", {
|
|
369
|
+
chartId,
|
|
370
|
+
chartType,
|
|
371
|
+
elapsedMs: Date.now() - startedAt,
|
|
372
|
+
});
|
|
373
|
+
// Single chart event with everything resolved: dataset
|
|
374
|
+
// for the table-like fallback / hover, option for the
|
|
375
|
+
// actual render. Best-effort write so a closed
|
|
376
|
+
// downstream stream can't take the tool down.
|
|
377
|
+
await safeWrite(log, writer, {
|
|
378
|
+
type: "chart",
|
|
379
|
+
chartId,
|
|
380
|
+
title,
|
|
381
|
+
...(description ? { description } : {}),
|
|
382
|
+
data,
|
|
383
|
+
option,
|
|
384
|
+
}, { chartId });
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
log.warn("render:error", {
|
|
388
|
+
chartId,
|
|
389
|
+
elapsedMs: Date.now() - startedAt,
|
|
390
|
+
error: commonUtils.errorMessage(err),
|
|
391
|
+
});
|
|
392
|
+
// Surface as a writer-level error so the slot can
|
|
393
|
+
// transition to "couldn't render chart" without the
|
|
394
|
+
// parent agent surfacing a stack trace.
|
|
395
|
+
await safeWrite(log, writer, {
|
|
396
|
+
type: "error",
|
|
397
|
+
error: commonUtils.errorMessage(err),
|
|
398
|
+
}, { chartId });
|
|
399
|
+
}
|
|
425
400
|
return { chartId };
|
|
426
401
|
},
|
|
427
402
|
});
|
|
@@ -437,6 +412,11 @@ function planToEchartsOption(plan, fallbackTitle) {
|
|
|
437
412
|
const baseTitle = plan.title ?? fallbackTitle;
|
|
438
413
|
const grid = { left: 48, right: 24, top: 56, bottom: 48, containLabel: true };
|
|
439
414
|
if (plan.chartType === "pie") {
|
|
415
|
+
// Echarts crashes on null pie slices - filter them out.
|
|
416
|
+
// `{name, value}` slices are the only valid pie data shape,
|
|
417
|
+
// so drop bare numbers / tuples / nulls the planner may
|
|
418
|
+
// have leaked into a pie series.
|
|
419
|
+
const slices = (plan.series[0]?.data ?? []).filter((d) => d !== null && typeof d === "object" && !Array.isArray(d));
|
|
440
420
|
return {
|
|
441
421
|
title: { text: baseTitle, left: "center" },
|
|
442
422
|
tooltip: { trigger: "item" },
|
|
@@ -446,12 +426,15 @@ function planToEchartsOption(plan, fallbackTitle) {
|
|
|
446
426
|
name: plan.series[0]?.name ?? baseTitle,
|
|
447
427
|
type: "pie",
|
|
448
428
|
radius: ["35%", "65%"],
|
|
449
|
-
data:
|
|
429
|
+
data: slices,
|
|
450
430
|
},
|
|
451
431
|
],
|
|
452
432
|
};
|
|
453
433
|
}
|
|
454
434
|
if (plan.chartType === "scatter") {
|
|
435
|
+
// Echarts crashes on null scatter points - keep only valid
|
|
436
|
+
// `[x, y]` tuples. Bare numbers / objects / nulls from a
|
|
437
|
+
// mismatched plan get dropped silently.
|
|
455
438
|
return {
|
|
456
439
|
title: { text: baseTitle, left: "center" },
|
|
457
440
|
tooltip: { trigger: "item" },
|
|
@@ -462,7 +445,7 @@ function planToEchartsOption(plan, fallbackTitle) {
|
|
|
462
445
|
series: plan.series.map((s) => ({
|
|
463
446
|
name: s.name,
|
|
464
447
|
type: "scatter",
|
|
465
|
-
data: s.data,
|
|
448
|
+
data: s.data.filter((d) => Array.isArray(d) && d.length === 2),
|
|
466
449
|
})),
|
|
467
450
|
};
|
|
468
451
|
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -8,12 +8,46 @@ import type { BasePluginConfig, getExecutionContext } from "@databricks/appkit";
|
|
|
8
8
|
import type { AgentConfig } from "@mastra/core/agent";
|
|
9
9
|
import type { PgVectorConfig, PostgresStoreConfig } from "@mastra/pg";
|
|
10
10
|
import type { MastraAgentDefinition, MastraTools } from "./agents.js";
|
|
11
|
+
import type { GenieSpacesConfig } from "./genie.js";
|
|
11
12
|
/**
|
|
12
13
|
* `RequestContext` key under which {@link MastraServer} stores the
|
|
13
14
|
* resolved AppKit user. `model.ts` reads it to mint user-scoped
|
|
14
15
|
* Databricks tokens.
|
|
15
16
|
*/
|
|
16
17
|
export declare const MASTRA_USER_KEY = "mastra__user";
|
|
18
|
+
/**
|
|
19
|
+
* `RequestContext` keys for AppKit user metadata stamped by
|
|
20
|
+
* {@link MastraServer}. Surfaced as trace metadata via
|
|
21
|
+
* {@link TRACE_REQUEST_CONTEXT_KEYS} so traces are filterable by who
|
|
22
|
+
* issued the request without leaking the full user object.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MASTRA_USER_NAME_KEY = "mastra__userName";
|
|
25
|
+
export declare const MASTRA_USER_EMAIL_KEY = "mastra__userEmail";
|
|
26
|
+
/**
|
|
27
|
+
* `RequestContext` key for the per-HTTP-request id stamped by
|
|
28
|
+
* {@link MastraServer}. Reads `X-Request-Id` from the incoming
|
|
29
|
+
* headers when present (so an upstream load balancer / API gateway
|
|
30
|
+
* can keep its trace correlation), falls back to a freshly minted
|
|
31
|
+
* UUID. Echoed back on the response and surfaced on every span via
|
|
32
|
+
* {@link TRACE_REQUEST_CONTEXT_KEYS} so logs and traces share a
|
|
33
|
+
* join key.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MASTRA_REQUEST_ID_KEY = "mastra__requestId";
|
|
36
|
+
/**
|
|
37
|
+
* Canonical list of `RequestContext` keys we want Mastra to extract
|
|
38
|
+
* as metadata on every observability span (agent runs, model calls,
|
|
39
|
+
* tool invocations, workflow steps).
|
|
40
|
+
*
|
|
41
|
+
* Mirrors {@link https://mastra.ai/docs/observability/tracing/overview#automatic-metadata-from-requestcontext}:
|
|
42
|
+
* passed verbatim into `Observability.configs[*].requestContextKeys`,
|
|
43
|
+
* so any key listed here is read from `RequestContext` at trace
|
|
44
|
+
* start and attached as scalar span metadata. Keep the set to plain
|
|
45
|
+
* scalars - never include {@link MASTRA_USER_KEY} (it carries the
|
|
46
|
+
* full AppKit execution context with a `WorkspaceClient` reference).
|
|
47
|
+
*
|
|
48
|
+
* Order is purely cosmetic; Mastra de-dupes internally.
|
|
49
|
+
*/
|
|
50
|
+
export declare const TRACE_REQUEST_CONTEXT_KEYS: readonly string[];
|
|
17
51
|
/** AppKit execution context plus the canonical user id. */
|
|
18
52
|
export interface User {
|
|
19
53
|
id: string;
|
|
@@ -180,4 +214,74 @@ export interface MastraPluginConfig extends BasePluginConfig {
|
|
|
180
214
|
* and the style block leans on the model's recency bias.
|
|
181
215
|
*/
|
|
182
216
|
styleInstructions?: string | false;
|
|
217
|
+
/**
|
|
218
|
+
* Genie spaces this plugin's agents can delegate to. One Mastra
|
|
219
|
+
* tool is registered per alias (`genie` for the well-known
|
|
220
|
+
* `default` alias, `genie_<alias>` otherwise). Each tool spins
|
|
221
|
+
* up a per-question Genie sub-agent that runs Databricks
|
|
222
|
+
* "agent mode" against the space, broadcasts wire events to the
|
|
223
|
+
* UI, fetches statement rows for non-empty results, and returns
|
|
224
|
+
* a `(string | data | chart)[]` summary the host UI renders
|
|
225
|
+
* inline.
|
|
226
|
+
*
|
|
227
|
+
* Entries accept either a full {@link GenieSpaceConfig} object
|
|
228
|
+
* or a bare `space_id` string when no extras are needed:
|
|
229
|
+
*
|
|
230
|
+
* ```ts
|
|
231
|
+
* mastra({
|
|
232
|
+
* genieSpaces: {
|
|
233
|
+
* default: "01ef0d3c0e1b1f4a8d2c3e4f5a6b7c8d",
|
|
234
|
+
* forecasts: { spaceId: "01ef...", hint: "weekly demand forecasts" },
|
|
235
|
+
* },
|
|
236
|
+
* });
|
|
237
|
+
* ```
|
|
238
|
+
*
|
|
239
|
+
* Reach the spaces from an agent's `tools(plugins)` callback via
|
|
240
|
+
* `plugins.genie?.toolkit()`; the resulting tools accept
|
|
241
|
+
* `{ content, conversationId? }` and return a hydrated summary.
|
|
242
|
+
*
|
|
243
|
+
* **Fallback discovery** (highest precedence first): if this
|
|
244
|
+
* field is omitted, the Genie agent also picks up spaces from
|
|
245
|
+
* (1) the AppKit `genie({ spaces: { ... } })` plugin instance
|
|
246
|
+
* when registered, and (2) the `DATABRICKS_GENIE_SPACE_ID`
|
|
247
|
+
* env var (registered under the `default` alias). This keeps
|
|
248
|
+
* existing AppKit deployments working without restating the
|
|
249
|
+
* spaces config in two places.
|
|
250
|
+
*/
|
|
251
|
+
genieSpaces?: GenieSpacesConfig;
|
|
252
|
+
/**
|
|
253
|
+
* TTL for the in-memory Genie space metadata cache, in
|
|
254
|
+
* milliseconds. Defaults to 5 minutes. The Genie agent calls
|
|
255
|
+
* `client.genie.getSpace(...)` on every cold-start to get the
|
|
256
|
+
* title / description / warehouse id; cached responses skip the
|
|
257
|
+
* round-trip and concurrent callers coalesce on a single
|
|
258
|
+
* in-flight fetch. Drop to a smaller value when analysts are
|
|
259
|
+
* actively editing space metadata and you want changes visible
|
|
260
|
+
* within seconds; raise it to amortise the round-trip when
|
|
261
|
+
* space metadata is effectively frozen.
|
|
262
|
+
*
|
|
263
|
+
* Backed by AppKit's `CacheManager`, so the cache participates
|
|
264
|
+
* in telemetry spans (`cache.getOrExecute`) and benefits from
|
|
265
|
+
* Lakebase persistence when the `lakebase` plugin is wired up.
|
|
266
|
+
*/
|
|
267
|
+
genieSpaceCacheTtlMs?: number;
|
|
268
|
+
/**
|
|
269
|
+
* Maximum LLM steps the Genie agent gets per turn.
|
|
270
|
+
* Each step is one round-trip to the underlying model
|
|
271
|
+
* (`get_space_description`, each `ask_genie` call, and the
|
|
272
|
+
* mandatory `submit_summary` closer all consume one step).
|
|
273
|
+
*
|
|
274
|
+
* Defaults to 16, which fits ~10 `ask_genie` calls plus
|
|
275
|
+
* grounding plus the summary closer - matches the
|
|
276
|
+
* decomposition pattern the Genie agent instructions prescribe
|
|
277
|
+
* ("2-6 ask_genie calls for any non-trivial question").
|
|
278
|
+
* Mastra's own `agent.generate` default of 5 would cut the
|
|
279
|
+
* Genie agent off after 2-3 Genie calls, so explicitly raising
|
|
280
|
+
* the ceiling here is what lets the agent-mode loop play out.
|
|
281
|
+
*
|
|
282
|
+
* Lower this when an unusually slow or expensive model makes
|
|
283
|
+
* long turns unaffordable; raise it for exploratory workloads
|
|
284
|
+
* that need to drill into a dataset.
|
|
285
|
+
*/
|
|
286
|
+
genieAgentMaxSteps?: number;
|
|
183
287
|
}
|
package/dist/src/config.js
CHANGED
|
@@ -4,9 +4,52 @@
|
|
|
4
4
|
* Kept in a leaf module so `plugin.ts`, `server.ts`, `model.ts`, and
|
|
5
5
|
* `memory.ts` can import them without creating a cycle.
|
|
6
6
|
*/
|
|
7
|
+
import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, } from "@mastra/core/request-context";
|
|
7
8
|
/**
|
|
8
9
|
* `RequestContext` key under which {@link MastraServer} stores the
|
|
9
10
|
* resolved AppKit user. `model.ts` reads it to mint user-scoped
|
|
10
11
|
* Databricks tokens.
|
|
11
12
|
*/
|
|
12
13
|
export const MASTRA_USER_KEY = "mastra__user";
|
|
14
|
+
/**
|
|
15
|
+
* `RequestContext` keys for AppKit user metadata stamped by
|
|
16
|
+
* {@link MastraServer}. Surfaced as trace metadata via
|
|
17
|
+
* {@link TRACE_REQUEST_CONTEXT_KEYS} so traces are filterable by who
|
|
18
|
+
* issued the request without leaking the full user object.
|
|
19
|
+
*/
|
|
20
|
+
export const MASTRA_USER_NAME_KEY = "mastra__userName";
|
|
21
|
+
export const MASTRA_USER_EMAIL_KEY = "mastra__userEmail";
|
|
22
|
+
/**
|
|
23
|
+
* `RequestContext` key for the per-HTTP-request id stamped by
|
|
24
|
+
* {@link MastraServer}. Reads `X-Request-Id` from the incoming
|
|
25
|
+
* headers when present (so an upstream load balancer / API gateway
|
|
26
|
+
* can keep its trace correlation), falls back to a freshly minted
|
|
27
|
+
* UUID. Echoed back on the response and surfaced on every span via
|
|
28
|
+
* {@link TRACE_REQUEST_CONTEXT_KEYS} so logs and traces share a
|
|
29
|
+
* join key.
|
|
30
|
+
*/
|
|
31
|
+
export const MASTRA_REQUEST_ID_KEY = "mastra__requestId";
|
|
32
|
+
/**
|
|
33
|
+
* Canonical list of `RequestContext` keys we want Mastra to extract
|
|
34
|
+
* as metadata on every observability span (agent runs, model calls,
|
|
35
|
+
* tool invocations, workflow steps).
|
|
36
|
+
*
|
|
37
|
+
* Mirrors {@link https://mastra.ai/docs/observability/tracing/overview#automatic-metadata-from-requestcontext}:
|
|
38
|
+
* passed verbatim into `Observability.configs[*].requestContextKeys`,
|
|
39
|
+
* so any key listed here is read from `RequestContext` at trace
|
|
40
|
+
* start and attached as scalar span metadata. Keep the set to plain
|
|
41
|
+
* scalars - never include {@link MASTRA_USER_KEY} (it carries the
|
|
42
|
+
* full AppKit execution context with a `WorkspaceClient` reference).
|
|
43
|
+
*
|
|
44
|
+
* Order is purely cosmetic; Mastra de-dupes internally.
|
|
45
|
+
*/
|
|
46
|
+
export const TRACE_REQUEST_CONTEXT_KEYS = [
|
|
47
|
+
MASTRA_RESOURCE_ID_KEY,
|
|
48
|
+
MASTRA_THREAD_ID_KEY,
|
|
49
|
+
MASTRA_REQUEST_ID_KEY,
|
|
50
|
+
MASTRA_USER_NAME_KEY,
|
|
51
|
+
MASTRA_USER_EMAIL_KEY,
|
|
52
|
+
// Model override key is owned by `serving.ts`; spelled inline here
|
|
53
|
+
// so this module stays leaf-level (no cycles with `serving.ts`).
|
|
54
|
+
"mastra__model_override",
|
|
55
|
+
];
|