@dbx-tools/appkit-mastra 0.1.5 → 0.1.13
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 +735 -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.d.ts +21 -0
- package/dist/src/memory.js +47 -2
- package/dist/src/model.js +18 -14
- package/dist/src/observability.d.ts +33 -0
- package/dist/src/observability.js +71 -0
- package/dist/src/plugin.d.ts +1 -1
- package/dist/src/plugin.js +32 -4
- 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 +23 -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 +55 -2
- package/src/model.ts +18 -13
- package/src/observability.ts +92 -0
- package/src/plugin.ts +33 -4
- 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.d.ts
CHANGED
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* index is almost always what users want; opt into per-agent recall
|
|
15
15
|
* by passing a {@link MastraMemoryConfigOverride} on the agent.
|
|
16
16
|
*
|
|
17
|
+
* Additionally, {@link MemoryBuilder.instanceStorage} returns a
|
|
18
|
+
* **Mastra-instance-level** `PostgresStore` (schema `mastra_instance`)
|
|
19
|
+
* used for workflow snapshots - the persistence layer
|
|
20
|
+
* `agent.resumeStream()` reads from when waking a suspended
|
|
21
|
+
* `requireApproval` tool call. Per-agent stores are not enough for
|
|
22
|
+
* this: workflow runs are scoped to the Mastra instance, not an
|
|
23
|
+
* individual agent's `Memory`.
|
|
24
|
+
*
|
|
17
25
|
* Plugin-level `config.storage` / `config.memory` act as the baseline
|
|
18
26
|
* (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
|
|
19
27
|
* is registered); per-agent settings cascade on top of that.
|
|
@@ -21,6 +29,7 @@
|
|
|
21
29
|
import { lakebase } from "@databricks/appkit";
|
|
22
30
|
import { pluginUtils } from "@dbx-tools/appkit-shared";
|
|
23
31
|
import { Memory } from "@mastra/memory";
|
|
32
|
+
import { PostgresStore } from "@mastra/pg";
|
|
24
33
|
import type { MastraAgentDefinition } from "./agents.js";
|
|
25
34
|
import type { MastraPluginConfig } from "./config.js";
|
|
26
35
|
/** Pool handle returned by the AppKit `lakebase` plugin `exports().pool`. */
|
|
@@ -62,6 +71,18 @@ export declare class MemoryBuilder {
|
|
|
62
71
|
* vector store enabled - Mastra accepts a missing `memory` field
|
|
63
72
|
* and treats the agent as stateless.
|
|
64
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* Build the Mastra-instance-level storage used for workflow
|
|
76
|
+
* snapshots. Returns `undefined` when plugin-level `storage` is
|
|
77
|
+
* disabled, in which case `agent.resumeStream()` (and therefore
|
|
78
|
+
* the `requireApproval` flow) will not be available.
|
|
79
|
+
*
|
|
80
|
+
* The store lives in a dedicated `mastra_instance` schema so it
|
|
81
|
+
* never collides with per-agent `mastra_<agentId>` namespaces.
|
|
82
|
+
* Workflow snapshots are not per-agent state; they belong to the
|
|
83
|
+
* `Mastra` instance that owns the workflow execution.
|
|
84
|
+
*/
|
|
85
|
+
instanceStorage(): PostgresStore | undefined;
|
|
65
86
|
forAgent(agentId: string, def: MastraAgentDefinition): Memory | undefined;
|
|
66
87
|
private buildStorage;
|
|
67
88
|
/**
|
package/dist/src/memory.js
CHANGED
|
@@ -14,16 +14,25 @@
|
|
|
14
14
|
* index is almost always what users want; opt into per-agent recall
|
|
15
15
|
* by passing a {@link MastraMemoryConfigOverride} on the agent.
|
|
16
16
|
*
|
|
17
|
+
* Additionally, {@link MemoryBuilder.instanceStorage} returns a
|
|
18
|
+
* **Mastra-instance-level** `PostgresStore` (schema `mastra_instance`)
|
|
19
|
+
* used for workflow snapshots - the persistence layer
|
|
20
|
+
* `agent.resumeStream()` reads from when waking a suspended
|
|
21
|
+
* `requireApproval` tool call. Per-agent stores are not enough for
|
|
22
|
+
* this: workflow runs are scoped to the Mastra instance, not an
|
|
23
|
+
* individual agent's `Memory`.
|
|
24
|
+
*
|
|
17
25
|
* Plugin-level `config.storage` / `config.memory` act as the baseline
|
|
18
26
|
* (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
|
|
19
27
|
* is registered); per-agent settings cascade on top of that.
|
|
20
28
|
*/
|
|
21
29
|
import { lakebase } from "@databricks/appkit";
|
|
22
|
-
import { pluginUtils } from "@dbx-tools/appkit-shared";
|
|
30
|
+
import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
|
|
23
31
|
import { fastembed } from "@mastra/fastembed";
|
|
24
32
|
import { Memory } from "@mastra/memory";
|
|
25
33
|
import { PgVector, PostgresStore } from "@mastra/pg";
|
|
26
34
|
import { randomUUID } from "node:crypto";
|
|
35
|
+
const log = logUtils.logger("mastra/memory");
|
|
27
36
|
/**
|
|
28
37
|
* True when any plugin-level or per-agent setting could need the
|
|
29
38
|
* Lakebase pool. Used by `plugin.ts` to gate pool acquisition; the
|
|
@@ -75,13 +84,49 @@ export class MemoryBuilder {
|
|
|
75
84
|
* vector store enabled - Mastra accepts a missing `memory` field
|
|
76
85
|
* and treats the agent as stateless.
|
|
77
86
|
*/
|
|
87
|
+
/**
|
|
88
|
+
* Build the Mastra-instance-level storage used for workflow
|
|
89
|
+
* snapshots. Returns `undefined` when plugin-level `storage` is
|
|
90
|
+
* disabled, in which case `agent.resumeStream()` (and therefore
|
|
91
|
+
* the `requireApproval` flow) will not be available.
|
|
92
|
+
*
|
|
93
|
+
* The store lives in a dedicated `mastra_instance` schema so it
|
|
94
|
+
* never collides with per-agent `mastra_<agentId>` namespaces.
|
|
95
|
+
* Workflow snapshots are not per-agent state; they belong to the
|
|
96
|
+
* `Mastra` instance that owns the workflow execution.
|
|
97
|
+
*/
|
|
98
|
+
instanceStorage() {
|
|
99
|
+
const setting = this.config.storage;
|
|
100
|
+
if (!setting)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (typeof setting === "object") {
|
|
103
|
+
return new PostgresStore(withId(setting, "mastra-store__instance"));
|
|
104
|
+
}
|
|
105
|
+
return new PostgresStore({
|
|
106
|
+
id: "mastra-store__instance",
|
|
107
|
+
schemaName: "mastra_instance",
|
|
108
|
+
pool: this.requirePool(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
78
111
|
forAgent(agentId, def) {
|
|
79
112
|
const storageSetting = def.storage ?? this.config.storage;
|
|
80
113
|
const memorySetting = def.memory ?? this.config.memory;
|
|
81
114
|
const storage = this.buildStorage(agentId, storageSetting);
|
|
82
115
|
const vector = this.buildVector(memorySetting);
|
|
83
|
-
if (!storage && !vector)
|
|
116
|
+
if (!storage && !vector) {
|
|
117
|
+
log.debug("agent:stateless", { agentId });
|
|
84
118
|
return undefined;
|
|
119
|
+
}
|
|
120
|
+
log.debug("agent:configured", {
|
|
121
|
+
agentId,
|
|
122
|
+
storage: storage !== undefined,
|
|
123
|
+
vector: vector !== undefined,
|
|
124
|
+
vectorMode: vector === undefined
|
|
125
|
+
? "off"
|
|
126
|
+
: typeof memorySetting === "object"
|
|
127
|
+
? "dedicated"
|
|
128
|
+
: "shared",
|
|
129
|
+
});
|
|
85
130
|
return new Memory({
|
|
86
131
|
...(storage ? { storage } : {}),
|
|
87
132
|
...(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
|
});
|