@dbx-tools/appkit-mastra 0.1.5 → 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 +728 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/agents.js +18 -8
- package/dist/src/chart.d.ts +101 -35
- package/dist/src/chart.js +178 -62
- package/dist/src/config.d.ts +13 -0
- package/dist/src/genie.d.ts +23 -8
- package/dist/src/genie.js +137 -101
- package/dist/src/history.js +14 -0
- package/dist/src/memory.js +15 -2
- package/dist/src/model.js +18 -14
- package/dist/src/plugin.d.ts +1 -1
- package/dist/src/plugin.js +9 -3
- 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.js +10 -0
- 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 +1 -0
- package/package.json +21 -25
- package/src/agents.ts +19 -6
- package/src/chart.ts +232 -64
- package/src/config.ts +13 -0
- package/src/genie.ts +179 -116
- package/src/history.ts +19 -7
- package/src/memory.ts +19 -2
- package/src/model.ts +18 -13
- package/src/plugin.ts +10 -3
- package/src/processors/strip-stale-charts.ts +105 -0
- package/src/server.ts +11 -0
- package/src/serving.ts +21 -2
- package/src/tools/email.ts +147 -0
- package/dist/src/render-chart-route.d.ts +0 -33
- package/dist/src/render-chart-route.js +0 -120
- package/src/render-chart-route.ts +0 -141
package/dist/src/genie.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { genie } from "@databricks/appkit";
|
|
23
23
|
import { createTool } from "@mastra/core/tools";
|
|
24
|
+
import type { MastraPluginConfig } from "./config.js";
|
|
24
25
|
/** Live AppKit `GeniePlugin` instance. */
|
|
25
26
|
export type GeniePluginInstance = InstanceType<ReturnType<typeof genie>["plugin"]>;
|
|
26
27
|
/** Full `exports()` shape of the AppKit `genie` plugin. */
|
|
@@ -37,11 +38,16 @@ export type GenieConversation = Awaited<ReturnType<GenieExports["getConversation
|
|
|
37
38
|
* Normalised progress event surfaced to the UI as a Mastra
|
|
38
39
|
* `tool-output` chunk. Loading pill events (`started`, `status`,
|
|
39
40
|
* `sql`, `suggested`, `error`) are pure UI metadata and never reach
|
|
40
|
-
* the LLM.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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`.
|
|
45
51
|
*/
|
|
46
52
|
export type GenieProgress = {
|
|
47
53
|
kind: "started";
|
|
@@ -61,9 +67,10 @@ export type GenieProgress = {
|
|
|
61
67
|
} | {
|
|
62
68
|
kind: "chart";
|
|
63
69
|
chartId: string;
|
|
64
|
-
title
|
|
70
|
+
title?: string;
|
|
65
71
|
description?: string;
|
|
66
|
-
data
|
|
72
|
+
data?: Array<Record<string, unknown>>;
|
|
73
|
+
option?: Record<string, unknown>;
|
|
67
74
|
} | {
|
|
68
75
|
kind: "text";
|
|
69
76
|
content: string;
|
|
@@ -86,10 +93,16 @@ export declare function defaultGenieToolName(alias: string): string;
|
|
|
86
93
|
* Build one `sendMessage` tool per configured Genie alias plus a single
|
|
87
94
|
* `getConversation` tool. Returns a record keyed by tool id, ready to
|
|
88
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.
|
|
89
101
|
*/
|
|
90
102
|
export declare function buildGenieTools(opts: {
|
|
91
103
|
aliases: string[];
|
|
92
104
|
exports: GenieExports;
|
|
105
|
+
config: MastraPluginConfig;
|
|
93
106
|
signal?: AbortSignal;
|
|
94
107
|
}): Record<string, ReturnType<typeof createTool>>;
|
|
95
108
|
/**
|
|
@@ -111,6 +124,8 @@ export declare function buildGenieTools(opts: {
|
|
|
111
124
|
* all-or-nothing bundle. Wire `only` / `except` / `prefix` / `rename`
|
|
112
125
|
* later if a caller needs them.
|
|
113
126
|
*/
|
|
114
|
-
export declare function buildGenieProvider(plugin: GeniePluginInstance
|
|
127
|
+
export declare function buildGenieProvider(plugin: GeniePluginInstance, opts: {
|
|
128
|
+
config: MastraPluginConfig;
|
|
129
|
+
}): {
|
|
115
130
|
toolkit(opts?: unknown): Record<string, ReturnType<typeof createTool>>;
|
|
116
131
|
};
|
package/dist/src/genie.js
CHANGED
|
@@ -19,11 +19,19 @@
|
|
|
19
19
|
* LLM never sees rows, and charts come from the separate
|
|
20
20
|
* `render_data` tool when the model decides one is useful.
|
|
21
21
|
*/
|
|
22
|
-
import { randomUUID } from "node:crypto";
|
|
23
22
|
import { genie } from "@databricks/appkit";
|
|
24
|
-
import { stringUtils } from "@dbx-tools/appkit-shared";
|
|
23
|
+
import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
|
|
25
24
|
import { createTool } from "@mastra/core/tools";
|
|
26
25
|
import { z } from "zod";
|
|
26
|
+
import { emitChartWithPlanning } from "./chart.js";
|
|
27
|
+
/**
|
|
28
|
+
* Module-level logger tagged `[mastra/genie]`. Uses the shared
|
|
29
|
+
* {@link logUtils.logger} so calls below `LOG_LEVEL` are
|
|
30
|
+
* discarded for free. Default `LOG_LEVEL` is `info`; flip to
|
|
31
|
+
* `debug` to see per-turn timing (`query_result` → planner
|
|
32
|
+
* waits → `drain:return`).
|
|
33
|
+
*/
|
|
34
|
+
const log = logUtils.logger("mastra/genie");
|
|
27
35
|
/**
|
|
28
36
|
* Per-dataset metadata surfaced to the LLM. The actual rows are
|
|
29
37
|
* dispatched separately as a `kind: "chart"` writer event so the
|
|
@@ -240,6 +248,11 @@ export function defaultGenieToolName(alias) {
|
|
|
240
248
|
* Build one `sendMessage` tool per configured Genie alias plus a single
|
|
241
249
|
* `getConversation` tool. Returns a record keyed by tool id, ready to
|
|
242
250
|
* spread into an `Agent`'s `tools` map.
|
|
251
|
+
*
|
|
252
|
+
* `config` must be the active plugin config; Genie's
|
|
253
|
+
* `query_result` events are routed through
|
|
254
|
+
* {@link emitChartWithPlanning} which uses it to resolve the
|
|
255
|
+
* chart-planner's model.
|
|
243
256
|
*/
|
|
244
257
|
export function buildGenieTools(opts) {
|
|
245
258
|
const tools = {};
|
|
@@ -250,23 +263,17 @@ export function buildGenieTools(opts) {
|
|
|
250
263
|
description: stringUtils.toDescription `
|
|
251
264
|
Ask the Databricks Genie space "${alias}" a single
|
|
252
265
|
natural-language question. Genie translates it to SQL,
|
|
253
|
-
runs
|
|
254
|
-
\`
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
the rendering. Add interpretation around the chart
|
|
261
|
-
(highlights, deltas, anomalies, takeaways) instead of
|
|
262
|
-
repeating numbers.
|
|
266
|
+
runs it, and returns \`genieAnswer\` (prose) plus
|
|
267
|
+
\`datasets[]\` (one entry per executed query, each with
|
|
268
|
+
a short \`chartId\`). Embed \`[[chart:<chartId>]]\` on
|
|
269
|
+
its own line at the position you want that data rendered
|
|
270
|
+
as an inline chart. Add interpretation around the chart
|
|
271
|
+
(deltas, anomalies, takeaways); do not paraphrase row
|
|
272
|
+
values.
|
|
263
273
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
re-querying with rephrased intent. Prefer aggregated
|
|
268
|
-
questions over raw-row queries (e.g. ask for "monthly
|
|
269
|
-
averages" instead of "all rows" for time-series).
|
|
274
|
+
Issue ONE focused question per user turn. Prefer
|
|
275
|
+
aggregated queries over raw-row queries for time-series
|
|
276
|
+
and distributions.
|
|
270
277
|
`,
|
|
271
278
|
inputSchema: sendMessageSchema,
|
|
272
279
|
outputSchema: genieToolOutputSchema,
|
|
@@ -274,7 +281,12 @@ export function buildGenieTools(opts) {
|
|
|
274
281
|
const stream = opts.exports.sendMessage(alias, content, conversationId, {
|
|
275
282
|
signal: opts.signal,
|
|
276
283
|
});
|
|
277
|
-
|
|
284
|
+
const requestContext = ctx
|
|
285
|
+
?.requestContext;
|
|
286
|
+
return drainGenieStream(stream, ctx.writer, {
|
|
287
|
+
config: opts.config,
|
|
288
|
+
...(requestContext ? { requestContext } : {}),
|
|
289
|
+
});
|
|
278
290
|
},
|
|
279
291
|
});
|
|
280
292
|
}
|
|
@@ -303,23 +315,29 @@ export function buildGenieTools(opts) {
|
|
|
303
315
|
* 1. {@link GenieProgress} pill events on the writer (`started`,
|
|
304
316
|
* `status`, `sql`, `suggested`, `error`) drive the loading
|
|
305
317
|
* pill in the chat bubble.
|
|
306
|
-
* 2. `kind: "chart"` events on the writer
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
318
|
+
* 2. `kind: "chart"` events on the writer (emitted via
|
|
319
|
+
* {@link emitChartWithPlanning}) carry the row payload from
|
|
320
|
+
* each Genie SQL statement and, on planner success, a
|
|
321
|
+
* follow-up event with the rendered Echarts spec. The host
|
|
322
|
+
* UI's `<ChartSlot>` merges the two by `chartId` and
|
|
323
|
+
* renders inline at the marker position the model picked.
|
|
324
|
+
* The data never reaches the LLM.
|
|
325
|
+
* 3. The `DrainResult` returned to the LLM contains Genie's
|
|
326
|
+
* prose answer plus a `datasets[]` array of metadata
|
|
327
|
+
* (chartId, title, columns, rowCount, sql) the model uses
|
|
328
|
+
* to cite charts via `[[chart:<chartId>]]` markers.
|
|
314
329
|
*
|
|
315
330
|
* `query_result` and `message_result` events arrive in either
|
|
316
|
-
* order; we buffer per-statement
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
331
|
+
* order; we buffer per-statement scratch keyed by `statementId`
|
|
332
|
+
* so each half can fill in what it knows. The chart event
|
|
333
|
+
* fires the moment `query_result` lands; the planner runs in
|
|
334
|
+
* the background. We `Promise.allSettled` every planner promise
|
|
335
|
+
* before returning so all chart work is attributed to the tool's
|
|
336
|
+
* trace span and so the LLM's `datasets[]` includes every
|
|
337
|
+
* chartId that has actually been queued.
|
|
321
338
|
*/
|
|
322
|
-
async function drainGenieStream(stream, writer) {
|
|
339
|
+
async function drainGenieStream(stream, writer, opts) {
|
|
340
|
+
const { config, requestContext } = opts;
|
|
323
341
|
let conversationId;
|
|
324
342
|
let genieAnswer;
|
|
325
343
|
let suggestedFollowUps;
|
|
@@ -331,15 +349,23 @@ async function drainGenieStream(stream, writer) {
|
|
|
331
349
|
// behaviour here so the UI status pill doesn't flicker and we don't
|
|
332
350
|
// burn writer bytes on no-op events.
|
|
333
351
|
let lastStatus;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
352
|
+
const scratchByStatementId = new Map();
|
|
353
|
+
const getScratch = (statementId) => {
|
|
354
|
+
let s = scratchByStatementId.get(statementId);
|
|
355
|
+
if (!s) {
|
|
356
|
+
s = { statementId, columns: [], rowCount: 0 };
|
|
357
|
+
scratchByStatementId.set(statementId, s);
|
|
358
|
+
}
|
|
359
|
+
return s;
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Planner promises kicked off per `query_result`. Awaited
|
|
363
|
+
* (Promise.allSettled) before drainGenieStream returns so the
|
|
364
|
+
* Genie tool's trace span covers the chart work and the LLM's
|
|
365
|
+
* `datasets[]` accurately reflects every chartId that's been
|
|
366
|
+
* queued for rendering.
|
|
367
|
+
*/
|
|
368
|
+
const plannerPromises = [];
|
|
343
369
|
const emit = async (event) => {
|
|
344
370
|
if (!writer)
|
|
345
371
|
return;
|
|
@@ -351,13 +377,12 @@ async function drainGenieStream(stream, writer) {
|
|
|
351
377
|
}
|
|
352
378
|
};
|
|
353
379
|
for await (const event of stream) {
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
// console.log("[mastra/genie] event", event);
|
|
380
|
+
// Per-event raw payload for tuning the pill / answer pipeline
|
|
381
|
+
// against real Genie traffic. At `info` (the default) this is
|
|
382
|
+
// discarded for free; flip `LOG_LEVEL=debug` to see every
|
|
383
|
+
// raw wire event before the switch routes it through writer
|
|
384
|
+
// and DrainResult.
|
|
385
|
+
log.debug("event", { type: event.type, payload: event });
|
|
361
386
|
switch (event.type) {
|
|
362
387
|
case "message_start":
|
|
363
388
|
conversationId = event.conversationId;
|
|
@@ -382,17 +407,30 @@ async function drainGenieStream(stream, writer) {
|
|
|
382
407
|
const columns = (event.data?.manifest?.schema?.columns ?? []).map((c) => c.name);
|
|
383
408
|
const dataArray = (event.data?.result?.data_array ?? []);
|
|
384
409
|
const rows = genieRowsToObjects(columns, dataArray);
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
410
|
+
const scratch = getScratch(event.statementId);
|
|
411
|
+
// emitChartWithPlanning emits the dataset event immediately
|
|
412
|
+
// and kicks off the chart-planner agent in the background.
|
|
413
|
+
// It returns the chartId synchronously; the plannerPromise
|
|
414
|
+
// is awaited at end-of-stream so chart work shows up under
|
|
415
|
+
// this tool's trace span.
|
|
416
|
+
const { chartId, plannerPromise } = await emitChartWithPlanning({
|
|
417
|
+
...(writer ? { writer } : {}),
|
|
418
|
+
config,
|
|
419
|
+
...(requestContext ? { requestContext } : {}),
|
|
420
|
+
title: scratch.title ?? `Genie query`,
|
|
421
|
+
...(scratch.description ? { description: scratch.description } : {}),
|
|
394
422
|
data: rows,
|
|
395
423
|
});
|
|
424
|
+
scratch.chartId = chartId;
|
|
425
|
+
scratch.columns = columns;
|
|
426
|
+
scratch.rowCount = rows.length;
|
|
427
|
+
plannerPromises.push(plannerPromise);
|
|
428
|
+
log.debug("query_result", {
|
|
429
|
+
statementId: event.statementId,
|
|
430
|
+
chartId,
|
|
431
|
+
rows: rows.length,
|
|
432
|
+
columns,
|
|
433
|
+
});
|
|
396
434
|
break;
|
|
397
435
|
}
|
|
398
436
|
case "message_result":
|
|
@@ -400,14 +438,15 @@ async function drainGenieStream(stream, writer) {
|
|
|
400
438
|
for (const attachment of event.message.attachments ?? []) {
|
|
401
439
|
const sqlText = attachment.query?.query;
|
|
402
440
|
const stmtId = attachment.query?.statementId;
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
441
|
+
if (stmtId) {
|
|
442
|
+
const scratch = getScratch(stmtId);
|
|
443
|
+
if (sqlText)
|
|
444
|
+
scratch.sql = sqlText;
|
|
445
|
+
if (attachment.query?.title)
|
|
446
|
+
scratch.title = attachment.query.title;
|
|
447
|
+
if (attachment.query?.description) {
|
|
448
|
+
scratch.description = attachment.query.description;
|
|
449
|
+
}
|
|
411
450
|
}
|
|
412
451
|
if (sqlText) {
|
|
413
452
|
await emit({
|
|
@@ -441,20 +480,40 @@ async function drainGenieStream(stream, writer) {
|
|
|
441
480
|
break;
|
|
442
481
|
}
|
|
443
482
|
}
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
483
|
+
// Wait for all chart planners to settle before returning so the
|
|
484
|
+
// tool's trace span covers chart work and the LLM's
|
|
485
|
+
// `datasets[]` reflects only chartIds the client has actually
|
|
486
|
+
// received writer events for. Failures in `emitChartWithPlanning`
|
|
487
|
+
// are already swallowed inside the helper, so this never
|
|
488
|
+
// throws.
|
|
489
|
+
log.debug("planners:awaiting", { count: plannerPromises.length });
|
|
490
|
+
await Promise.allSettled(plannerPromises);
|
|
491
|
+
log.debug("planners:settled", { count: plannerPromises.length });
|
|
492
|
+
// Build the LLM-bound `datasets[]` from scratch entries that
|
|
493
|
+
// actually ran a query (chartId is assigned at `query_result`
|
|
494
|
+
// time). Entries that only saw `message_result` metadata
|
|
495
|
+
// without a row payload are skipped.
|
|
447
496
|
const datasets = [];
|
|
448
|
-
for (const
|
|
497
|
+
for (const scratch of scratchByStatementId.values()) {
|
|
498
|
+
if (!scratch.chartId)
|
|
499
|
+
continue;
|
|
449
500
|
datasets.push({
|
|
450
|
-
chartId:
|
|
451
|
-
...(
|
|
452
|
-
...(
|
|
453
|
-
columns:
|
|
454
|
-
rowCount:
|
|
455
|
-
...(
|
|
501
|
+
chartId: scratch.chartId,
|
|
502
|
+
...(scratch.title ? { title: scratch.title } : {}),
|
|
503
|
+
...(scratch.description ? { description: scratch.description } : {}),
|
|
504
|
+
columns: scratch.columns,
|
|
505
|
+
rowCount: scratch.rowCount,
|
|
506
|
+
...(scratch.sql ? { sql: scratch.sql } : {}),
|
|
456
507
|
});
|
|
457
508
|
}
|
|
509
|
+
log.debug("drain:return", {
|
|
510
|
+
conversationId,
|
|
511
|
+
hasAnswer: typeof genieAnswer === "string",
|
|
512
|
+
answerLength: genieAnswer?.length ?? 0,
|
|
513
|
+
chartIds: datasets.map((d) => d.chartId),
|
|
514
|
+
suggestedCount: suggestedFollowUps?.length ?? 0,
|
|
515
|
+
error,
|
|
516
|
+
});
|
|
458
517
|
return {
|
|
459
518
|
...(conversationId ? { conversationId } : {}),
|
|
460
519
|
...(genieAnswer ? { genieAnswer } : {}),
|
|
@@ -463,30 +522,6 @@ async function drainGenieStream(stream, writer) {
|
|
|
463
522
|
...(error ? { error } : {}),
|
|
464
523
|
};
|
|
465
524
|
}
|
|
466
|
-
/**
|
|
467
|
-
* Get-or-create-and-merge the per-statement scratch entry. Both
|
|
468
|
-
* `query_result` and `message_result` paths call this with their
|
|
469
|
-
* partial bag of fields; the resulting record is the union of
|
|
470
|
-
* everything we know about that statement so far.
|
|
471
|
-
*/
|
|
472
|
-
function upsertDatasetMeta(store, statementId, patch) {
|
|
473
|
-
const existing = store.get(statementId);
|
|
474
|
-
const merged = {
|
|
475
|
-
chartId: existing?.chartId ?? randomUUID().replace(/-/g, "").slice(0, 8),
|
|
476
|
-
statementId,
|
|
477
|
-
columns: patch.columns ?? existing?.columns ?? [],
|
|
478
|
-
rowCount: patch.rowCount ?? existing?.rowCount ?? 0,
|
|
479
|
-
...(patch.title ?? existing?.title
|
|
480
|
-
? { title: patch.title ?? existing?.title }
|
|
481
|
-
: {}),
|
|
482
|
-
...(patch.description ?? existing?.description
|
|
483
|
-
? { description: patch.description ?? existing?.description }
|
|
484
|
-
: {}),
|
|
485
|
-
...(patch.sql ?? existing?.sql ? { sql: patch.sql ?? existing?.sql } : {}),
|
|
486
|
-
};
|
|
487
|
-
store.set(statementId, merged);
|
|
488
|
-
return merged;
|
|
489
|
-
}
|
|
490
525
|
/**
|
|
491
526
|
* Convert Genie's `data_array` (column-positional `string | null`
|
|
492
527
|
* tuples) into plain JS row objects keyed by column name. Numeric
|
|
@@ -538,7 +573,7 @@ function coerceCell(cell) {
|
|
|
538
573
|
* all-or-nothing bundle. Wire `only` / `except` / `prefix` / `rename`
|
|
539
574
|
* later if a caller needs them.
|
|
540
575
|
*/
|
|
541
|
-
export function buildGenieProvider(plugin) {
|
|
576
|
+
export function buildGenieProvider(plugin, opts) {
|
|
542
577
|
return {
|
|
543
578
|
toolkit(_opts) {
|
|
544
579
|
const aliases = extractGenieAliases(plugin);
|
|
@@ -548,6 +583,7 @@ export function buildGenieProvider(plugin) {
|
|
|
548
583
|
sendMessage: plugin.sendMessage.bind(plugin),
|
|
549
584
|
getConversation: plugin.getConversation.bind(plugin),
|
|
550
585
|
},
|
|
586
|
+
config: opts.config,
|
|
551
587
|
});
|
|
552
588
|
},
|
|
553
589
|
};
|
package/dist/src/history.js
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* the handler runs - no cookie or user lookups happen here, and the
|
|
16
16
|
* session-cookie logic stays the single source of truth in `server.ts`.
|
|
17
17
|
*/
|
|
18
|
+
import { logUtils } from "@dbx-tools/appkit-shared";
|
|
18
19
|
import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
|
|
19
20
|
import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, } from "@mastra/core/request-context";
|
|
20
21
|
import { registerApiRoute } from "@mastra/core/server";
|
|
22
|
+
const log = logUtils.logger("mastra/history");
|
|
21
23
|
/** Default history page size; matches the Mastra storage default. */
|
|
22
24
|
const DEFAULT_PER_PAGE = 20;
|
|
23
25
|
/** Hard cap so a misbehaving client can't fetch the whole thread at once. */
|
|
@@ -42,8 +44,10 @@ export async function loadHistory(opts) {
|
|
|
42
44
|
const page = Math.max(0, Math.trunc(opts.page ?? 0));
|
|
43
45
|
const memory = await opts.agent.getMemory();
|
|
44
46
|
if (!memory) {
|
|
47
|
+
log.debug("recall:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
|
|
45
48
|
return { uiMessages: [], page, perPage, total: 0, hasMore: false };
|
|
46
49
|
}
|
|
50
|
+
const startedAt = Date.now();
|
|
47
51
|
const result = await memory.recall({
|
|
48
52
|
threadId: opts.threadId,
|
|
49
53
|
...(opts.resourceId ? { resourceId: opts.resourceId } : {}),
|
|
@@ -56,6 +60,16 @@ export async function loadHistory(opts) {
|
|
|
56
60
|
});
|
|
57
61
|
const chronological = sortChronological(result.messages);
|
|
58
62
|
const uiMessages = toAISdkV5Messages(chronological);
|
|
63
|
+
log.debug("recall:done", {
|
|
64
|
+
agentId: opts.agent.id,
|
|
65
|
+
threadId: opts.threadId,
|
|
66
|
+
page,
|
|
67
|
+
perPage,
|
|
68
|
+
returned: uiMessages.length,
|
|
69
|
+
total: result.total,
|
|
70
|
+
hasMore: result.hasMore,
|
|
71
|
+
elapsedMs: Date.now() - startedAt,
|
|
72
|
+
});
|
|
59
73
|
return {
|
|
60
74
|
uiMessages,
|
|
61
75
|
page,
|
package/dist/src/memory.js
CHANGED
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
* is registered); per-agent settings cascade on top of that.
|
|
20
20
|
*/
|
|
21
21
|
import { lakebase } from "@databricks/appkit";
|
|
22
|
-
import { pluginUtils } from "@dbx-tools/appkit-shared";
|
|
22
|
+
import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
|
|
23
23
|
import { fastembed } from "@mastra/fastembed";
|
|
24
24
|
import { Memory } from "@mastra/memory";
|
|
25
25
|
import { PgVector, PostgresStore } from "@mastra/pg";
|
|
26
26
|
import { randomUUID } from "node:crypto";
|
|
27
|
+
const log = logUtils.logger("mastra/memory");
|
|
27
28
|
/**
|
|
28
29
|
* True when any plugin-level or per-agent setting could need the
|
|
29
30
|
* Lakebase pool. Used by `plugin.ts` to gate pool acquisition; the
|
|
@@ -80,8 +81,20 @@ export class MemoryBuilder {
|
|
|
80
81
|
const memorySetting = def.memory ?? this.config.memory;
|
|
81
82
|
const storage = this.buildStorage(agentId, storageSetting);
|
|
82
83
|
const vector = this.buildVector(memorySetting);
|
|
83
|
-
if (!storage && !vector)
|
|
84
|
+
if (!storage && !vector) {
|
|
85
|
+
log.debug("agent:stateless", { agentId });
|
|
84
86
|
return undefined;
|
|
87
|
+
}
|
|
88
|
+
log.debug("agent:configured", {
|
|
89
|
+
agentId,
|
|
90
|
+
storage: storage !== undefined,
|
|
91
|
+
vector: vector !== undefined,
|
|
92
|
+
vectorMode: vector === undefined
|
|
93
|
+
? "off"
|
|
94
|
+
: typeof memorySetting === "object"
|
|
95
|
+
? "dedicated"
|
|
96
|
+
: "shared",
|
|
97
|
+
});
|
|
85
98
|
return new Memory({
|
|
86
99
|
...(storage ? { storage } : {}),
|
|
87
100
|
...(vector ? { vector, embedder: fastembed } : {}),
|
package/dist/src/model.js
CHANGED
|
@@ -263,6 +263,7 @@ export async function buildModel(config, requestContext, overrides = {}) {
|
|
|
263
263
|
* to the top of the priority list.
|
|
264
264
|
*/
|
|
265
265
|
async function pickModelId(config, requestContext, overrides, user, host) {
|
|
266
|
+
const log = logUtils.logger(config);
|
|
266
267
|
const serving = resolveServingConfig(config, FALLBACK_MODEL_IDS);
|
|
267
268
|
const override = serving.allowOverride
|
|
268
269
|
? requestContext.get(MASTRA_MODEL_OVERRIDE_KEY)
|
|
@@ -270,15 +271,21 @@ async function pickModelId(config, requestContext, overrides, user, host) {
|
|
|
270
271
|
const explicit = override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
|
|
271
272
|
// Cheap exit: when the caller named a specific model and fuzzy
|
|
272
273
|
// matching is off, there's no reason to touch the catalogue at all.
|
|
273
|
-
if (explicit !== undefined && !serving.fuzzy)
|
|
274
|
+
if (explicit !== undefined && !serving.fuzzy) {
|
|
275
|
+
log.debug("model selected", { modelId: explicit, source: "explicit" });
|
|
274
276
|
return explicit;
|
|
277
|
+
}
|
|
275
278
|
const endpoints = await listServingEndpoints(user.executionContext.client, host, {
|
|
276
279
|
ttlMs: serving.ttlMs,
|
|
277
280
|
});
|
|
278
281
|
const modelId = explicit !== undefined
|
|
279
282
|
? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
|
|
280
283
|
: pickFirstAvailable(serving.fallbacks, endpoints);
|
|
281
|
-
|
|
284
|
+
log.debug("model selected", {
|
|
285
|
+
modelId,
|
|
286
|
+
source: explicit !== undefined ? "fuzzy-match" : "fallback",
|
|
287
|
+
requestedExplicit: explicit,
|
|
288
|
+
});
|
|
282
289
|
return modelId;
|
|
283
290
|
}
|
|
284
291
|
/**
|
|
@@ -305,9 +312,9 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
|
|
|
305
312
|
* 1. Rewrites the outgoing `messages` array to repair Mastra/AI SDK
|
|
306
313
|
* stream-replay quirks that Databricks-hosted Claude rejects (see
|
|
307
314
|
* {@link sanitizeServingMessages}).
|
|
308
|
-
* 2.
|
|
309
|
-
*
|
|
310
|
-
*
|
|
315
|
+
* 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
|
|
316
|
+
* 4xx debugging doesn't have to fight AI SDK's `[Array]`
|
|
317
|
+
* formatter.
|
|
311
318
|
*
|
|
312
319
|
* Safe to call from any hot path: {@link commonUtils.memoize} ensures
|
|
313
320
|
* the wrapper is installed at most once per process, so subsequent
|
|
@@ -315,7 +322,7 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
|
|
|
315
322
|
* {@link buildModel} fires on every agent step.
|
|
316
323
|
*/
|
|
317
324
|
const setupFetchInterceptor = commonUtils.memoize(() => {
|
|
318
|
-
const
|
|
325
|
+
const log = logUtils.logger("mastra/llm");
|
|
319
326
|
const original = globalThis.fetch.bind(globalThis);
|
|
320
327
|
globalThis.fetch = (async (input, init) => {
|
|
321
328
|
const url = httpUtils.toURL(input);
|
|
@@ -328,14 +335,11 @@ const setupFetchInterceptor = commonUtils.memoize(() => {
|
|
|
328
335
|
if (rewritten !== init.body) {
|
|
329
336
|
init = { ...init, body: rewritten };
|
|
330
337
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
console.error("[mastra:llm-debug] -> POST", url.toString(), "(non-JSON body)");
|
|
338
|
-
}
|
|
338
|
+
try {
|
|
339
|
+
log.debug("POST", { url: url.toString(), body: JSON.parse(rewritten) });
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
log.debug("POST", { url: url.toString(), bodyType: "non-JSON" });
|
|
339
343
|
}
|
|
340
344
|
return original(input, init);
|
|
341
345
|
});
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
88
88
|
*/
|
|
89
89
|
getDefault: () => Agent | null;
|
|
90
90
|
/** Underlying Mastra instance for advanced use (custom routes etc.). */
|
|
91
|
-
getMastra: () => Mastra<Record<string, Agent<any, import("@mastra/core/agent").ToolsInput, undefined, unknown>>, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>> | null;
|
|
91
|
+
getMastra: () => Mastra<Record<string, Agent<any, import("@mastra/core/agent").ToolsInput, undefined, unknown, import("@mastra/core/agent").AgentEditorConfig | undefined>>, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>> | null;
|
|
92
92
|
/** Express subapp Mastra is mounted on; mostly for tests. */
|
|
93
93
|
getMastraServer: () => MastraServer | null;
|
|
94
94
|
/**
|
package/dist/src/plugin.js
CHANGED
|
@@ -33,7 +33,6 @@ import { Mastra } from "@mastra/core/mastra";
|
|
|
33
33
|
import express from "express";
|
|
34
34
|
import { buildAgents, FALLBACK_AGENT_ID } from "./agents.js";
|
|
35
35
|
import { historyRoute } from "./history.js";
|
|
36
|
-
import { renderChartRoute } from "./render-chart-route.js";
|
|
37
36
|
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
38
37
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
39
38
|
import { clearServingEndpointsCache, listServingEndpoints, resolveServingConfig, } from "./serving.js";
|
|
@@ -163,7 +162,6 @@ export class MastraPlugin extends Plugin {
|
|
|
163
162
|
modelsPath: `${basePath}/models`,
|
|
164
163
|
historyPath: `${basePath}/route/history`,
|
|
165
164
|
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
166
|
-
renderChartPath: `${basePath}/route/render-chart`,
|
|
167
165
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
168
166
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
169
167
|
};
|
|
@@ -218,6 +216,10 @@ export class MastraPlugin extends Plugin {
|
|
|
218
216
|
const memoryBuilder = needsLakebase(this.config)
|
|
219
217
|
? createMemoryBuilder(this.config, this.context)
|
|
220
218
|
: undefined;
|
|
219
|
+
this.log.debug("build:start", {
|
|
220
|
+
lakebase: memoryBuilder !== undefined,
|
|
221
|
+
stripStaleCharts: this.config.stripStaleCharts !== false,
|
|
222
|
+
});
|
|
221
223
|
// Build every agent declared in `config.agents` (or the built-in
|
|
222
224
|
// fallback when none are declared). Each agent's `model` resolves
|
|
223
225
|
// workspace URL + bearer at call time so concurrent requests get
|
|
@@ -246,10 +248,14 @@ export class MastraPlugin extends Plugin {
|
|
|
246
248
|
chatRoute({ path: "/route/chat/:agentId" }),
|
|
247
249
|
historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
|
|
248
250
|
historyRoute({ path: "/route/history/:agentId" }),
|
|
249
|
-
renderChartRoute({ path: "/route/render-chart", config: this.config }),
|
|
250
251
|
],
|
|
251
252
|
});
|
|
252
253
|
await this.mastraServer.init();
|
|
254
|
+
this.log.debug("build:done", {
|
|
255
|
+
agents: Object.keys(this.built.agents),
|
|
256
|
+
defaultAgent: this.built.defaultAgentId,
|
|
257
|
+
routes: ["/route/chat", "/route/history", "/models"],
|
|
258
|
+
});
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
export const mastra = toPlugin(MastraPlugin);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra input processor that strips `chartId` fields from every
|
|
3
|
+
* tool-invocation result in prior assistant messages before they
|
|
4
|
+
* reach the model.
|
|
5
|
+
*
|
|
6
|
+
* Why: chartIds are only meaningful within the assistant turn that
|
|
7
|
+
* minted them - the writer events backing them are gone after the
|
|
8
|
+
* stream closes. When the model sees old chartIds in memory recall
|
|
9
|
+
* (Mastra Memory persists tool results), it's tempted to type
|
|
10
|
+
* those ids into the new turn's `[[chart:<id>]]` markers, leaving
|
|
11
|
+
* the chat client's chart slots stuck with no matching event. This
|
|
12
|
+
* processor removes the temptation by deleting `chartId` keys from
|
|
13
|
+
* every assistant message's tool results before the prompt is
|
|
14
|
+
* built. The current turn's tool results don't exist yet at
|
|
15
|
+
* `processInput` time, so they pass through unmodified.
|
|
16
|
+
*
|
|
17
|
+
* The strip is recursive - any nested `chartId` field is removed,
|
|
18
|
+
* regardless of which tool produced the result. This covers Genie's
|
|
19
|
+
* `datasets[].chartId` and `render_data`'s top-level `chartId`
|
|
20
|
+
* uniformly without coupling to specific tool ids.
|
|
21
|
+
*/
|
|
22
|
+
import type { InputProcessor } from "@mastra/core/processors";
|
|
23
|
+
/**
|
|
24
|
+
* Input processor that scrubs `chartId` from every tool-invocation
|
|
25
|
+
* result in the message list. Wired onto every agent by default
|
|
26
|
+
* via {@link buildAgents}; opt out with
|
|
27
|
+
* `MastraPluginConfig.stripStaleCharts: false`.
|
|
28
|
+
*/
|
|
29
|
+
export declare const stripStaleChartsProcessor: InputProcessor;
|