@dbx-tools/appkit-mastra 0.1.13 → 0.1.18
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 +34 -39
- package/dist/src/agents.d.ts +2 -2
- package/dist/src/agents.js +66 -14
- package/dist/src/chart.d.ts +39 -105
- package/dist/src/chart.js +199 -194
- package/dist/src/config.d.ts +104 -0
- package/dist/src/config.js +43 -0
- package/dist/src/genie.d.ts +170 -107
- package/dist/src/genie.js +1003 -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/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +21 -18
- package/src/agents.ts +73 -17
- package/src/chart.ts +221 -251
- package/src/config.ts +120 -0
- package/src/genie.ts +1199 -654
- 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/dist/src/chart.js
CHANGED
|
@@ -1,40 +1,32 @@
|
|
|
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";
|
|
@@ -47,6 +39,75 @@ import { ModelTier, modelForTier, buildModel } from "./model.js";
|
|
|
47
39
|
* `write:ok(data)` → `planner:done` → `write:ok(option)`).
|
|
48
40
|
*/
|
|
49
41
|
const log = logUtils.logger("mastra/chart");
|
|
42
|
+
/**
|
|
43
|
+
* One series data point. Wide variant set so the planner agent can
|
|
44
|
+
* faithfully pass through whatever the SQL row set contained
|
|
45
|
+
* (numbers, stringified numbers, nulls for missing measurements,
|
|
46
|
+
* `[x, y]` tuples for scatter, `{name, value}` slices for pie)
|
|
47
|
+
* without the structured-output guard rejecting the whole plan.
|
|
48
|
+
*
|
|
49
|
+
* Three layers of tolerance:
|
|
50
|
+
*
|
|
51
|
+
* 1. {@link z.preprocess} normalizes wire shapes BEFORE union
|
|
52
|
+
* dispatch: stringified numbers parse to numbers, finite
|
|
53
|
+
* checks reject `NaN` / `Infinity`, 2-element arrays coerce
|
|
54
|
+
* tuple components, and `{value}` objects with missing /
|
|
55
|
+
* stringified `value` get coerced or rejected uniformly.
|
|
56
|
+
* Anything not handleable becomes `null`.
|
|
57
|
+
* 2. The union accepts `null` as a first-class variant. Echarts
|
|
58
|
+
* renders null as a gap on bar / line / area (which is the
|
|
59
|
+
* right visual signal for "missing reading"). Scatter and
|
|
60
|
+
* pie filter nulls in {@link planToEchartsOption} because
|
|
61
|
+
* Echarts crashes on null tuples / slices.
|
|
62
|
+
* 3. {@link z.union#catch} backstops the whole thing: if
|
|
63
|
+
* preprocess somehow produces a shape that still doesn't
|
|
64
|
+
* match any variant, the bad item becomes `null` instead of
|
|
65
|
+
* taking down the entire chart with a
|
|
66
|
+
* `Structured output validation failed` error.
|
|
67
|
+
*
|
|
68
|
+
* Net effect: a 200-row dataset with a few sparse/null/string
|
|
69
|
+
* values still produces a chart; only a totally-malformed planner
|
|
70
|
+
* response (no items at all) falls through to the table fallback.
|
|
71
|
+
*/
|
|
72
|
+
const chartDataPointSchema = z
|
|
73
|
+
.preprocess((v) => {
|
|
74
|
+
if (v === null || v === undefined)
|
|
75
|
+
return null;
|
|
76
|
+
if (typeof v === "number")
|
|
77
|
+
return Number.isFinite(v) ? v : null;
|
|
78
|
+
if (typeof v === "string") {
|
|
79
|
+
const n = Number(v);
|
|
80
|
+
return Number.isFinite(n) ? n : null;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(v) && v.length === 2) {
|
|
83
|
+
const x = typeof v[0] === "number" ? v[0] : Number(v[0]);
|
|
84
|
+
const y = typeof v[1] === "number" ? v[1] : Number(v[1]);
|
|
85
|
+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
|
|
86
|
+
}
|
|
87
|
+
if (typeof v === "object" && v !== null && "value" in v) {
|
|
88
|
+
const obj = v;
|
|
89
|
+
const val = typeof obj.value === "number" ? obj.value : Number(obj.value);
|
|
90
|
+
if (!Number.isFinite(val))
|
|
91
|
+
return null;
|
|
92
|
+
// Coerce numeric / boolean / nullish names to strings so a
|
|
93
|
+
// pie slice keyed on a year (`2024`) or category id is
|
|
94
|
+
// accepted without round-tripping through the catch arm.
|
|
95
|
+
const rawName = obj.name;
|
|
96
|
+
const name = typeof rawName === "string"
|
|
97
|
+
? rawName
|
|
98
|
+
: rawName == null
|
|
99
|
+
? ""
|
|
100
|
+
: String(rawName);
|
|
101
|
+
return { name, value: val };
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}, z.union([
|
|
105
|
+
z.number(),
|
|
106
|
+
z.null(),
|
|
107
|
+
z.tuple([z.number(), z.number()]),
|
|
108
|
+
z.object({ name: z.string(), value: z.number() }),
|
|
109
|
+
]))
|
|
110
|
+
.catch(null);
|
|
50
111
|
/**
|
|
51
112
|
* Compact, model-friendly representation of an Echarts spec. The
|
|
52
113
|
* planner agent emits this; {@link planToEchartsOption} expands it
|
|
@@ -58,8 +119,7 @@ const log = logUtils.logger("mastra/chart");
|
|
|
58
119
|
* across charts.
|
|
59
120
|
*/
|
|
60
121
|
const chartPlanSchema = z.object({
|
|
61
|
-
chartType: z
|
|
62
|
-
.enum(["bar", "line", "area", "scatter", "pie"])
|
|
122
|
+
chartType: z.enum(["bar", "line", "area", "scatter", "pie"])
|
|
63
123
|
.describe(stringUtils.toDescription `
|
|
64
124
|
The chart shape that best matches the data and intent. Use
|
|
65
125
|
\`bar\` for category-vs-value comparisons, \`line\` for
|
|
@@ -79,10 +139,7 @@ const chartPlanSchema = z.object({
|
|
|
79
139
|
Axis label to the left of the chart. Used for bar / line /
|
|
80
140
|
area / scatter; ignored for pie.
|
|
81
141
|
`),
|
|
82
|
-
categories: z
|
|
83
|
-
.array(z.string())
|
|
84
|
-
.optional()
|
|
85
|
-
.describe(stringUtils.toDescription `
|
|
142
|
+
categories: z.array(z.string()).optional().describe(stringUtils.toDescription `
|
|
86
143
|
X-axis category labels for \`bar\` / \`line\` / \`area\`
|
|
87
144
|
charts (one per data point in each series). Omit for
|
|
88
145
|
\`scatter\` (uses [x, y] tuples) and \`pie\` (each slice
|
|
@@ -93,24 +150,14 @@ const chartPlanSchema = z.object({
|
|
|
93
150
|
name: z.string().describe(stringUtils.toDescription `
|
|
94
151
|
Legend name for this series.
|
|
95
152
|
`),
|
|
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 `
|
|
153
|
+
data: z.array(chartDataPointSchema).describe(stringUtils.toDescription `
|
|
106
154
|
Data points. For \`bar\` / \`line\` / \`area\`, an
|
|
107
155
|
array of numbers aligned to \`categories\`. For
|
|
108
156
|
\`scatter\`, an array of \`[x, y]\` numeric tuples.
|
|
109
157
|
For \`pie\`, an array of \`{name, value}\` objects.
|
|
110
158
|
`),
|
|
111
159
|
}))
|
|
112
|
-
.min(1)
|
|
113
|
-
.describe(stringUtils.toDescription `
|
|
160
|
+
.min(1).describe(stringUtils.toDescription `
|
|
114
161
|
One or more series to plot. Pie charts use exactly one
|
|
115
162
|
series; bar/line/area can stack multiple series sharing
|
|
116
163
|
the same \`categories\` axis.
|
|
@@ -193,13 +240,14 @@ function getPlannerAgent(config) {
|
|
|
193
240
|
}
|
|
194
241
|
/**
|
|
195
242
|
* Run the chart planner against the given dataset and return a
|
|
196
|
-
* full Echarts `EChartsOption` JSON.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
243
|
+
* full Echarts `EChartsOption` JSON. Pure async function: no
|
|
244
|
+
* writer side-effects, no id minting, no background work.
|
|
245
|
+
* Producers (the `render_data` tool, the Genie agent,
|
|
246
|
+
* anything else that needs a chart) await this and stitch the
|
|
247
|
+
* result into whatever shape their wire contract needs.
|
|
200
248
|
*/
|
|
201
249
|
export async function runChartPlanner(opts) {
|
|
202
|
-
const { config, requestContext, title, description, data } = opts;
|
|
250
|
+
const { config, requestContext, title, description, data, signal } = opts;
|
|
203
251
|
const planner = getPlannerAgent(config);
|
|
204
252
|
const prompt = [
|
|
205
253
|
`Title: ${title}`,
|
|
@@ -215,122 +263,12 @@ export async function runChartPlanner(opts) {
|
|
|
215
263
|
const result = await planner.generate(prompt, {
|
|
216
264
|
structuredOutput: { schema: chartPlanSchema },
|
|
217
265
|
...(requestContext ? { requestContext } : {}),
|
|
266
|
+
...(signal ? { abortSignal: signal } : {}),
|
|
218
267
|
});
|
|
219
268
|
const plan = result.object;
|
|
220
269
|
const option = planToEchartsOption(plan, title);
|
|
221
270
|
return { option, chartType: plan.chartType };
|
|
222
271
|
}
|
|
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
272
|
const renderDataInputSchema = z.object({
|
|
335
273
|
title: z.string().describe(stringUtils.toDescription `
|
|
336
274
|
Title shown above the rendered chart. Use a concise
|
|
@@ -343,9 +281,7 @@ const renderDataInputSchema = z.object({
|
|
|
343
281
|
The chart-planner reads this when picking the chart type and
|
|
344
282
|
axis encodings; the user does not see it directly.
|
|
345
283
|
`),
|
|
346
|
-
data: z
|
|
347
|
-
.array(z.record(z.string(), z.unknown()))
|
|
348
|
-
.min(1)
|
|
284
|
+
data: z.array(z.record(z.string(), z.unknown())).min(1)
|
|
349
285
|
.describe(stringUtils.toDescription `
|
|
350
286
|
Tabular dataset to chart. One object per row, keyed by
|
|
351
287
|
column name. Values may be strings, numbers, booleans, or
|
|
@@ -365,16 +301,14 @@ const renderDataOutputSchema = z.object({
|
|
|
365
301
|
/**
|
|
366
302
|
* Build the `render_data` tool bound to the given plugin config.
|
|
367
303
|
*
|
|
368
|
-
* The tool
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* `{ chartId }`, so its context stays small regardless of dataset
|
|
377
|
-
* size.
|
|
304
|
+
* The tool awaits {@link runChartPlanner} so the planner's
|
|
305
|
+
* latency is attributed to this tool's trace span, then emits
|
|
306
|
+
* one `type: "chart"` writer event carrying the dataset and the
|
|
307
|
+
* resolved `EChartsOption`. The LLM-bound output is just
|
|
308
|
+
* `{ chartId }` so the model's context stays flat regardless of
|
|
309
|
+
* dataset size. Planner failures are caught and surfaced as a
|
|
310
|
+
* `type: "error"` writer event so the slot can fall back to
|
|
311
|
+
* "couldn't render chart" without taking the parent agent down.
|
|
378
312
|
*/
|
|
379
313
|
export function buildRenderDataTool(config) {
|
|
380
314
|
return createTool({
|
|
@@ -389,14 +323,14 @@ export function buildRenderDataTool(config) {
|
|
|
389
323
|
|
|
390
324
|
Placement contract: embed \`[[chart:<chartId>]]\` on its own
|
|
391
325
|
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
|
-
|
|
326
|
+
chart to appear in your reply. The chart is fully resolved
|
|
327
|
+
by the time the tool returns, so it renders immediately at
|
|
328
|
+
that spot. You can call \`render_data\` multiple times in
|
|
329
|
+
the same turn (the tool is parallel-safe) and interleave
|
|
330
|
+
the markers with prose so each chart sits next to its
|
|
331
|
+
commentary. A chart whose marker is omitted falls through
|
|
332
|
+
to the end of your reply as a fallback - safe but less
|
|
333
|
+
polished.
|
|
400
334
|
|
|
401
335
|
Use whenever a SQL row set, API response, or hand-built
|
|
402
336
|
dataset would land better as a picture than as a list or
|
|
@@ -410,22 +344,85 @@ export function buildRenderDataTool(config) {
|
|
|
410
344
|
const writer = ctx?.writer;
|
|
411
345
|
const requestContext = ctx
|
|
412
346
|
?.requestContext;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
347
|
+
// Marker-friendly short id. The LLM types this verbatim
|
|
348
|
+
// into `[[chart:<id>]]`; 8 hex chars is unique within a
|
|
349
|
+
// single assistant turn and easy for the model to copy.
|
|
350
|
+
const chartId = commonUtils.shortId();
|
|
351
|
+
const startedAt = Date.now();
|
|
352
|
+
log.debug("render:start", {
|
|
353
|
+
chartId,
|
|
417
354
|
title,
|
|
418
|
-
|
|
419
|
-
data,
|
|
355
|
+
rows: data.length,
|
|
356
|
+
columns: data[0] ? Object.keys(data[0]) : [],
|
|
357
|
+
hasWriter: writer !== undefined,
|
|
420
358
|
});
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
359
|
+
try {
|
|
360
|
+
const { option, chartType } = await runChartPlanner({
|
|
361
|
+
config,
|
|
362
|
+
...(requestContext ? { requestContext } : {}),
|
|
363
|
+
title,
|
|
364
|
+
...(description ? { description } : {}),
|
|
365
|
+
data,
|
|
366
|
+
});
|
|
367
|
+
log.debug("render:done", {
|
|
368
|
+
chartId,
|
|
369
|
+
chartType,
|
|
370
|
+
elapsedMs: Date.now() - startedAt,
|
|
371
|
+
});
|
|
372
|
+
// Single chart event with everything resolved: dataset
|
|
373
|
+
// for the table-like fallback / hover, option for the
|
|
374
|
+
// actual render. Best-effort write so a closed
|
|
375
|
+
// downstream stream can't take the tool down.
|
|
376
|
+
await safeWrite(writer, chartId, {
|
|
377
|
+
type: "chart",
|
|
378
|
+
chartId,
|
|
379
|
+
title,
|
|
380
|
+
...(description ? { description } : {}),
|
|
381
|
+
data,
|
|
382
|
+
option,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
log.warn("render:error", {
|
|
387
|
+
chartId,
|
|
388
|
+
elapsedMs: Date.now() - startedAt,
|
|
389
|
+
error: err instanceof Error ? err.message : String(err),
|
|
390
|
+
});
|
|
391
|
+
// Surface as a writer-level error so the slot can
|
|
392
|
+
// transition to "couldn't render chart" without the
|
|
393
|
+
// parent agent surfacing a stack trace.
|
|
394
|
+
await safeWrite(writer, chartId, {
|
|
395
|
+
type: "error",
|
|
396
|
+
error: err instanceof Error ? err.message : String(err),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
425
399
|
return { chartId };
|
|
426
400
|
},
|
|
427
401
|
});
|
|
428
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Best-effort writer.write. Failures are logged at `warn` (a
|
|
405
|
+
* persistently-closed writer is the most likely culprit when
|
|
406
|
+
* chart events go missing client-side) but swallowed so a closed
|
|
407
|
+
* downstream stream (cancelled request, client navigated away)
|
|
408
|
+
* can't take a tool down.
|
|
409
|
+
*/
|
|
410
|
+
async function safeWrite(writer, chartId, chunk) {
|
|
411
|
+
if (!writer) {
|
|
412
|
+
log.debug("write:no-writer", { chartId });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
await writer.write(chunk);
|
|
417
|
+
log.debug("write:ok", { chartId });
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
log.warn("write:error", {
|
|
421
|
+
chartId,
|
|
422
|
+
error: err instanceof Error ? err.message : String(err),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
429
426
|
/**
|
|
430
427
|
* Expand a {@link ChartPlan} into a full Echarts `EChartsOption`
|
|
431
428
|
* JSON. Centralized here so the planner agent only fills in the
|
|
@@ -437,6 +434,11 @@ function planToEchartsOption(plan, fallbackTitle) {
|
|
|
437
434
|
const baseTitle = plan.title ?? fallbackTitle;
|
|
438
435
|
const grid = { left: 48, right: 24, top: 56, bottom: 48, containLabel: true };
|
|
439
436
|
if (plan.chartType === "pie") {
|
|
437
|
+
// Echarts crashes on null pie slices - filter them out.
|
|
438
|
+
// `{name, value}` slices are the only valid pie data shape,
|
|
439
|
+
// so drop bare numbers / tuples / nulls the planner may
|
|
440
|
+
// have leaked into a pie series.
|
|
441
|
+
const slices = (plan.series[0]?.data ?? []).filter((d) => d !== null && typeof d === "object" && !Array.isArray(d));
|
|
440
442
|
return {
|
|
441
443
|
title: { text: baseTitle, left: "center" },
|
|
442
444
|
tooltip: { trigger: "item" },
|
|
@@ -446,12 +448,15 @@ function planToEchartsOption(plan, fallbackTitle) {
|
|
|
446
448
|
name: plan.series[0]?.name ?? baseTitle,
|
|
447
449
|
type: "pie",
|
|
448
450
|
radius: ["35%", "65%"],
|
|
449
|
-
data:
|
|
451
|
+
data: slices,
|
|
450
452
|
},
|
|
451
453
|
],
|
|
452
454
|
};
|
|
453
455
|
}
|
|
454
456
|
if (plan.chartType === "scatter") {
|
|
457
|
+
// Echarts crashes on null scatter points - keep only valid
|
|
458
|
+
// `[x, y]` tuples. Bare numbers / objects / nulls from a
|
|
459
|
+
// mismatched plan get dropped silently.
|
|
455
460
|
return {
|
|
456
461
|
title: { text: baseTitle, left: "center" },
|
|
457
462
|
tooltip: { trigger: "item" },
|
|
@@ -462,7 +467,7 @@ function planToEchartsOption(plan, fallbackTitle) {
|
|
|
462
467
|
series: plan.series.map((s) => ({
|
|
463
468
|
name: s.name,
|
|
464
469
|
type: "scatter",
|
|
465
|
-
data: s.data,
|
|
470
|
+
data: s.data.filter((d) => Array.isArray(d) && d.length === 2),
|
|
466
471
|
})),
|
|
467
472
|
};
|
|
468
473
|
}
|
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
|
}
|