@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/src/genie.ts
CHANGED
|
@@ -20,14 +20,25 @@
|
|
|
20
20
|
* `render_data` tool when the model decides one is useful.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { randomUUID } from "node:crypto";
|
|
24
|
-
|
|
25
23
|
import { genie } from "@databricks/appkit";
|
|
26
|
-
import { stringUtils } from "@dbx-tools/appkit-shared";
|
|
24
|
+
import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
|
|
25
|
+
import type { RequestContext } from "@mastra/core/request-context";
|
|
27
26
|
import { createTool } from "@mastra/core/tools";
|
|
28
27
|
import type { ToolStream } from "@mastra/core/tools";
|
|
29
28
|
import { z } from "zod";
|
|
30
29
|
|
|
30
|
+
import { emitChartWithPlanning } from "./chart.js";
|
|
31
|
+
import type { MastraPluginConfig } from "./config.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Module-level logger tagged `[mastra/genie]`. Uses the shared
|
|
35
|
+
* {@link logUtils.logger} so calls below `LOG_LEVEL` are
|
|
36
|
+
* discarded for free. Default `LOG_LEVEL` is `info`; flip to
|
|
37
|
+
* `debug` to see per-turn timing (`query_result` → planner
|
|
38
|
+
* waits → `drain:return`).
|
|
39
|
+
*/
|
|
40
|
+
const log = logUtils.logger("mastra/genie");
|
|
41
|
+
|
|
31
42
|
/** Live AppKit `GeniePlugin` instance. */
|
|
32
43
|
export type GeniePluginInstance = InstanceType<ReturnType<typeof genie>["plugin"]>;
|
|
33
44
|
|
|
@@ -137,17 +148,21 @@ const genieToolOutputSchema = z.object({
|
|
|
137
148
|
});
|
|
138
149
|
|
|
139
150
|
type DrainResult = z.infer<typeof genieToolOutputSchema>;
|
|
140
|
-
type DatasetMeta = z.infer<typeof datasetSchema> & { statementId: string };
|
|
141
151
|
|
|
142
152
|
/**
|
|
143
153
|
* Normalised progress event surfaced to the UI as a Mastra
|
|
144
154
|
* `tool-output` chunk. Loading pill events (`started`, `status`,
|
|
145
155
|
* `sql`, `suggested`, `error`) are pure UI metadata and never reach
|
|
146
|
-
* the LLM.
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
156
|
+
* the LLM.
|
|
157
|
+
*
|
|
158
|
+
* The `chart` variant is the wire shape emitted by
|
|
159
|
+
* {@link emitChartWithPlanning} (used by both this Genie
|
|
160
|
+
* draining loop and the system-level `render_data` tool). All
|
|
161
|
+
* fields except `chartId` are optional because two events per
|
|
162
|
+
* chartId arrive on the wire: the first carries the rows
|
|
163
|
+
* (`title` + `description?` + `data`); the second, on planner
|
|
164
|
+
* success, carries just the resolved Echarts spec (`option`).
|
|
165
|
+
* The host UI's `<ChartSlot>` merges them by `chartId`.
|
|
151
166
|
*/
|
|
152
167
|
export type GenieProgress =
|
|
153
168
|
| { kind: "started"; conversationId: string; messageId: string; spaceId: string }
|
|
@@ -162,9 +177,10 @@ export type GenieProgress =
|
|
|
162
177
|
| {
|
|
163
178
|
kind: "chart";
|
|
164
179
|
chartId: string;
|
|
165
|
-
title
|
|
180
|
+
title?: string;
|
|
166
181
|
description?: string;
|
|
167
|
-
data
|
|
182
|
+
data?: Array<Record<string, unknown>>;
|
|
183
|
+
option?: Record<string, unknown>;
|
|
168
184
|
}
|
|
169
185
|
| { kind: "text"; content: string }
|
|
170
186
|
| { kind: "suggested"; questions: string[] }
|
|
@@ -302,10 +318,16 @@ export function defaultGenieToolName(alias: string): string {
|
|
|
302
318
|
* Build one `sendMessage` tool per configured Genie alias plus a single
|
|
303
319
|
* `getConversation` tool. Returns a record keyed by tool id, ready to
|
|
304
320
|
* spread into an `Agent`'s `tools` map.
|
|
321
|
+
*
|
|
322
|
+
* `config` must be the active plugin config; Genie's
|
|
323
|
+
* `query_result` events are routed through
|
|
324
|
+
* {@link emitChartWithPlanning} which uses it to resolve the
|
|
325
|
+
* chart-planner's model.
|
|
305
326
|
*/
|
|
306
327
|
export function buildGenieTools(opts: {
|
|
307
328
|
aliases: string[];
|
|
308
329
|
exports: GenieExports;
|
|
330
|
+
config: MastraPluginConfig;
|
|
309
331
|
signal?: AbortSignal;
|
|
310
332
|
}): Record<string, ReturnType<typeof createTool>> {
|
|
311
333
|
const tools: Record<string, ReturnType<typeof createTool>> = {};
|
|
@@ -317,23 +339,17 @@ export function buildGenieTools(opts: {
|
|
|
317
339
|
description: stringUtils.toDescription`
|
|
318
340
|
Ask the Databricks Genie space "${alias}" a single
|
|
319
341
|
natural-language question. Genie translates it to SQL,
|
|
320
|
-
runs
|
|
321
|
-
\`
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Calling this tool is expensive; issue **one** focused
|
|
332
|
-
question per user turn. If the first answer doesn't fit,
|
|
333
|
-
ask the user a clarifying question rather than
|
|
334
|
-
re-querying with rephrased intent. Prefer aggregated
|
|
335
|
-
questions over raw-row queries (e.g. ask for "monthly
|
|
336
|
-
averages" instead of "all rows" for time-series).
|
|
342
|
+
runs it, and returns \`genieAnswer\` (prose) plus
|
|
343
|
+
\`datasets[]\` (one entry per executed query, each with
|
|
344
|
+
a short \`chartId\`). Embed \`[[chart:<chartId>]]\` on
|
|
345
|
+
its own line at the position you want that data rendered
|
|
346
|
+
as an inline chart. Add interpretation around the chart
|
|
347
|
+
(deltas, anomalies, takeaways); do not paraphrase row
|
|
348
|
+
values.
|
|
349
|
+
|
|
350
|
+
Issue ONE focused question per user turn. Prefer
|
|
351
|
+
aggregated queries over raw-row queries for time-series
|
|
352
|
+
and distributions.
|
|
337
353
|
`,
|
|
338
354
|
inputSchema: sendMessageSchema,
|
|
339
355
|
outputSchema: genieToolOutputSchema,
|
|
@@ -341,7 +357,12 @@ export function buildGenieTools(opts: {
|
|
|
341
357
|
const stream = opts.exports.sendMessage(alias, content, conversationId, {
|
|
342
358
|
signal: opts.signal,
|
|
343
359
|
});
|
|
344
|
-
|
|
360
|
+
const requestContext = (ctx as { requestContext?: RequestContext } | undefined)
|
|
361
|
+
?.requestContext;
|
|
362
|
+
return drainGenieStream(stream, ctx.writer, {
|
|
363
|
+
config: opts.config,
|
|
364
|
+
...(requestContext ? { requestContext } : {}),
|
|
365
|
+
});
|
|
345
366
|
},
|
|
346
367
|
});
|
|
347
368
|
}
|
|
@@ -363,6 +384,12 @@ export function buildGenieTools(opts: {
|
|
|
363
384
|
return tools;
|
|
364
385
|
}
|
|
365
386
|
|
|
387
|
+
/** Inputs to {@link drainGenieStream}. */
|
|
388
|
+
interface DrainGenieStreamOptions {
|
|
389
|
+
config: MastraPluginConfig;
|
|
390
|
+
requestContext?: RequestContext;
|
|
391
|
+
}
|
|
392
|
+
|
|
366
393
|
/**
|
|
367
394
|
* Drain the genie `sendMessage` AsyncGenerator into a flat result
|
|
368
395
|
* the agent's calling LLM can reason about, while forwarding
|
|
@@ -373,26 +400,33 @@ export function buildGenieTools(opts: {
|
|
|
373
400
|
* 1. {@link GenieProgress} pill events on the writer (`started`,
|
|
374
401
|
* `status`, `sql`, `suggested`, `error`) drive the loading
|
|
375
402
|
* pill in the chat bubble.
|
|
376
|
-
* 2. `kind: "chart"` events on the writer
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
403
|
+
* 2. `kind: "chart"` events on the writer (emitted via
|
|
404
|
+
* {@link emitChartWithPlanning}) carry the row payload from
|
|
405
|
+
* each Genie SQL statement and, on planner success, a
|
|
406
|
+
* follow-up event with the rendered Echarts spec. The host
|
|
407
|
+
* UI's `<ChartSlot>` merges the two by `chartId` and
|
|
408
|
+
* renders inline at the marker position the model picked.
|
|
409
|
+
* The data never reaches the LLM.
|
|
410
|
+
* 3. The `DrainResult` returned to the LLM contains Genie's
|
|
411
|
+
* prose answer plus a `datasets[]` array of metadata
|
|
412
|
+
* (chartId, title, columns, rowCount, sql) the model uses
|
|
413
|
+
* to cite charts via `[[chart:<chartId>]]` markers.
|
|
384
414
|
*
|
|
385
415
|
* `query_result` and `message_result` events arrive in either
|
|
386
|
-
* order; we buffer per-statement
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
416
|
+
* order; we buffer per-statement scratch keyed by `statementId`
|
|
417
|
+
* so each half can fill in what it knows. The chart event
|
|
418
|
+
* fires the moment `query_result` lands; the planner runs in
|
|
419
|
+
* the background. We `Promise.allSettled` every planner promise
|
|
420
|
+
* before returning so all chart work is attributed to the tool's
|
|
421
|
+
* trace span and so the LLM's `datasets[]` includes every
|
|
422
|
+
* chartId that has actually been queued.
|
|
391
423
|
*/
|
|
392
424
|
async function drainGenieStream(
|
|
393
425
|
stream: AsyncGenerator<GenieStreamEvent>,
|
|
394
|
-
writer
|
|
426
|
+
writer: ToolStream | undefined,
|
|
427
|
+
opts: DrainGenieStreamOptions,
|
|
395
428
|
): Promise<DrainResult> {
|
|
429
|
+
const { config, requestContext } = opts;
|
|
396
430
|
let conversationId: string | undefined;
|
|
397
431
|
let genieAnswer: string | undefined;
|
|
398
432
|
let suggestedFollowUps: string[] | undefined;
|
|
@@ -406,15 +440,37 @@ async function drainGenieStream(
|
|
|
406
440
|
let lastStatus: string | undefined;
|
|
407
441
|
|
|
408
442
|
// Per-statement scratch keyed by Genie's `statementId`. Filled in
|
|
409
|
-
// by both `query_result` (
|
|
410
|
-
// (sql + title + description)
|
|
411
|
-
// built from this at end-of-stream,
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
443
|
+
// by both `query_result` (chartId + columns + rows) and
|
|
444
|
+
// `message_result` (sql + title + description). The LLM-bound
|
|
445
|
+
// `datasets[]` is built from this at end-of-stream, after all
|
|
446
|
+
// planner promises settle.
|
|
447
|
+
type Scratch = {
|
|
448
|
+
statementId: string;
|
|
449
|
+
chartId?: string;
|
|
450
|
+
title?: string;
|
|
451
|
+
description?: string;
|
|
452
|
+
sql?: string;
|
|
453
|
+
columns: string[];
|
|
454
|
+
rowCount: number;
|
|
455
|
+
};
|
|
456
|
+
const scratchByStatementId = new Map<string, Scratch>();
|
|
457
|
+
const getScratch = (statementId: string): Scratch => {
|
|
458
|
+
let s = scratchByStatementId.get(statementId);
|
|
459
|
+
if (!s) {
|
|
460
|
+
s = { statementId, columns: [], rowCount: 0 };
|
|
461
|
+
scratchByStatementId.set(statementId, s);
|
|
462
|
+
}
|
|
463
|
+
return s;
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* Planner promises kicked off per `query_result`. Awaited
|
|
467
|
+
* (Promise.allSettled) before drainGenieStream returns so the
|
|
468
|
+
* Genie tool's trace span covers the chart work and the LLM's
|
|
469
|
+
* `datasets[]` accurately reflects every chartId that's been
|
|
470
|
+
* queued for rendering.
|
|
471
|
+
*/
|
|
472
|
+
const plannerPromises: Promise<void>[] = [];
|
|
473
|
+
|
|
418
474
|
const emit = async (event: GenieProgress) => {
|
|
419
475
|
if (!writer) return;
|
|
420
476
|
try {
|
|
@@ -425,13 +481,12 @@ async function drainGenieStream(
|
|
|
425
481
|
};
|
|
426
482
|
|
|
427
483
|
for await (const event of stream) {
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
// console.log("[mastra/genie] event", event);
|
|
484
|
+
// Per-event raw payload for tuning the pill / answer pipeline
|
|
485
|
+
// against real Genie traffic. At `info` (the default) this is
|
|
486
|
+
// discarded for free; flip `LOG_LEVEL=debug` to see every
|
|
487
|
+
// raw wire event before the switch routes it through writer
|
|
488
|
+
// and DrainResult.
|
|
489
|
+
log.debug("event", { type: event.type, payload: event });
|
|
435
490
|
switch (event.type) {
|
|
436
491
|
case "message_start":
|
|
437
492
|
conversationId = event.conversationId;
|
|
@@ -459,17 +514,30 @@ async function drainGenieStream(
|
|
|
459
514
|
Array<string | null>
|
|
460
515
|
>;
|
|
461
516
|
const rows = genieRowsToObjects(columns, dataArray);
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
517
|
+
const scratch = getScratch(event.statementId);
|
|
518
|
+
// emitChartWithPlanning emits the dataset event immediately
|
|
519
|
+
// and kicks off the chart-planner agent in the background.
|
|
520
|
+
// It returns the chartId synchronously; the plannerPromise
|
|
521
|
+
// is awaited at end-of-stream so chart work shows up under
|
|
522
|
+
// this tool's trace span.
|
|
523
|
+
const { chartId, plannerPromise } = await emitChartWithPlanning({
|
|
524
|
+
...(writer ? { writer } : {}),
|
|
525
|
+
config,
|
|
526
|
+
...(requestContext ? { requestContext } : {}),
|
|
527
|
+
title: scratch.title ?? `Genie query`,
|
|
528
|
+
...(scratch.description ? { description: scratch.description } : {}),
|
|
471
529
|
data: rows,
|
|
472
530
|
});
|
|
531
|
+
scratch.chartId = chartId;
|
|
532
|
+
scratch.columns = columns;
|
|
533
|
+
scratch.rowCount = rows.length;
|
|
534
|
+
plannerPromises.push(plannerPromise);
|
|
535
|
+
log.debug("query_result", {
|
|
536
|
+
statementId: event.statementId,
|
|
537
|
+
chartId,
|
|
538
|
+
rows: rows.length,
|
|
539
|
+
columns,
|
|
540
|
+
});
|
|
473
541
|
break;
|
|
474
542
|
}
|
|
475
543
|
case "message_result":
|
|
@@ -477,14 +545,13 @@ async function drainGenieStream(
|
|
|
477
545
|
for (const attachment of event.message.attachments ?? []) {
|
|
478
546
|
const sqlText = attachment.query?.query;
|
|
479
547
|
const stmtId = attachment.query?.statementId;
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
});
|
|
548
|
+
if (stmtId) {
|
|
549
|
+
const scratch = getScratch(stmtId);
|
|
550
|
+
if (sqlText) scratch.sql = sqlText;
|
|
551
|
+
if (attachment.query?.title) scratch.title = attachment.query.title;
|
|
552
|
+
if (attachment.query?.description) {
|
|
553
|
+
scratch.description = attachment.query.description;
|
|
554
|
+
}
|
|
488
555
|
}
|
|
489
556
|
if (sqlText) {
|
|
490
557
|
await emit({
|
|
@@ -519,21 +586,42 @@ async function drainGenieStream(
|
|
|
519
586
|
}
|
|
520
587
|
}
|
|
521
588
|
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
589
|
+
// Wait for all chart planners to settle before returning so the
|
|
590
|
+
// tool's trace span covers chart work and the LLM's
|
|
591
|
+
// `datasets[]` reflects only chartIds the client has actually
|
|
592
|
+
// received writer events for. Failures in `emitChartWithPlanning`
|
|
593
|
+
// are already swallowed inside the helper, so this never
|
|
594
|
+
// throws.
|
|
595
|
+
log.debug("planners:awaiting", { count: plannerPromises.length });
|
|
596
|
+
await Promise.allSettled(plannerPromises);
|
|
597
|
+
log.debug("planners:settled", { count: plannerPromises.length });
|
|
598
|
+
|
|
599
|
+
// Build the LLM-bound `datasets[]` from scratch entries that
|
|
600
|
+
// actually ran a query (chartId is assigned at `query_result`
|
|
601
|
+
// time). Entries that only saw `message_result` metadata
|
|
602
|
+
// without a row payload are skipped.
|
|
525
603
|
const datasets: Array<z.infer<typeof datasetSchema>> = [];
|
|
526
|
-
for (const
|
|
604
|
+
for (const scratch of scratchByStatementId.values()) {
|
|
605
|
+
if (!scratch.chartId) continue;
|
|
527
606
|
datasets.push({
|
|
528
|
-
chartId:
|
|
529
|
-
...(
|
|
530
|
-
...(
|
|
531
|
-
columns:
|
|
532
|
-
rowCount:
|
|
533
|
-
...(
|
|
607
|
+
chartId: scratch.chartId,
|
|
608
|
+
...(scratch.title ? { title: scratch.title } : {}),
|
|
609
|
+
...(scratch.description ? { description: scratch.description } : {}),
|
|
610
|
+
columns: scratch.columns,
|
|
611
|
+
rowCount: scratch.rowCount,
|
|
612
|
+
...(scratch.sql ? { sql: scratch.sql } : {}),
|
|
534
613
|
});
|
|
535
614
|
}
|
|
536
615
|
|
|
616
|
+
log.debug("drain:return", {
|
|
617
|
+
conversationId,
|
|
618
|
+
hasAnswer: typeof genieAnswer === "string",
|
|
619
|
+
answerLength: genieAnswer?.length ?? 0,
|
|
620
|
+
chartIds: datasets.map((d) => d.chartId),
|
|
621
|
+
suggestedCount: suggestedFollowUps?.length ?? 0,
|
|
622
|
+
error,
|
|
623
|
+
});
|
|
624
|
+
|
|
537
625
|
return {
|
|
538
626
|
...(conversationId ? { conversationId } : {}),
|
|
539
627
|
...(genieAnswer ? { genieAnswer } : {}),
|
|
@@ -543,35 +631,6 @@ async function drainGenieStream(
|
|
|
543
631
|
};
|
|
544
632
|
}
|
|
545
633
|
|
|
546
|
-
/**
|
|
547
|
-
* Get-or-create-and-merge the per-statement scratch entry. Both
|
|
548
|
-
* `query_result` and `message_result` paths call this with their
|
|
549
|
-
* partial bag of fields; the resulting record is the union of
|
|
550
|
-
* everything we know about that statement so far.
|
|
551
|
-
*/
|
|
552
|
-
function upsertDatasetMeta(
|
|
553
|
-
store: Map<string, DatasetMeta>,
|
|
554
|
-
statementId: string,
|
|
555
|
-
patch: Partial<Omit<DatasetMeta, "chartId" | "statementId">>,
|
|
556
|
-
): DatasetMeta {
|
|
557
|
-
const existing = store.get(statementId);
|
|
558
|
-
const merged: DatasetMeta = {
|
|
559
|
-
chartId: existing?.chartId ?? randomUUID().replace(/-/g, "").slice(0, 8),
|
|
560
|
-
statementId,
|
|
561
|
-
columns: patch.columns ?? existing?.columns ?? [],
|
|
562
|
-
rowCount: patch.rowCount ?? existing?.rowCount ?? 0,
|
|
563
|
-
...(patch.title ?? existing?.title
|
|
564
|
-
? { title: patch.title ?? existing?.title }
|
|
565
|
-
: {}),
|
|
566
|
-
...(patch.description ?? existing?.description
|
|
567
|
-
? { description: patch.description ?? existing?.description }
|
|
568
|
-
: {}),
|
|
569
|
-
...(patch.sql ?? existing?.sql ? { sql: patch.sql ?? existing?.sql } : {}),
|
|
570
|
-
};
|
|
571
|
-
store.set(statementId, merged);
|
|
572
|
-
return merged;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
634
|
/**
|
|
576
635
|
* Convert Genie's `data_array` (column-positional `string | null`
|
|
577
636
|
* tuples) into plain JS row objects keyed by column name. Numeric
|
|
@@ -626,7 +685,10 @@ function coerceCell(cell: string | null): unknown {
|
|
|
626
685
|
* all-or-nothing bundle. Wire `only` / `except` / `prefix` / `rename`
|
|
627
686
|
* later if a caller needs them.
|
|
628
687
|
*/
|
|
629
|
-
export function buildGenieProvider(
|
|
688
|
+
export function buildGenieProvider(
|
|
689
|
+
plugin: GeniePluginInstance,
|
|
690
|
+
opts: { config: MastraPluginConfig },
|
|
691
|
+
): {
|
|
630
692
|
toolkit(opts?: unknown): Record<string, ReturnType<typeof createTool>>;
|
|
631
693
|
} {
|
|
632
694
|
return {
|
|
@@ -638,6 +700,7 @@ export function buildGenieProvider(plugin: GeniePluginInstance): {
|
|
|
638
700
|
sendMessage: plugin.sendMessage.bind(plugin),
|
|
639
701
|
getConversation: plugin.getConversation.bind(plugin),
|
|
640
702
|
},
|
|
703
|
+
config: opts.config,
|
|
641
704
|
});
|
|
642
705
|
},
|
|
643
706
|
};
|
package/src/history.ts
CHANGED
|
@@ -16,21 +16,23 @@
|
|
|
16
16
|
* session-cookie logic stays the single source of truth in `server.ts`.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { logUtils } from "@dbx-tools/appkit-shared";
|
|
19
20
|
import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
|
|
20
21
|
import type { Agent } from "@mastra/core/agent";
|
|
21
|
-
import type {
|
|
22
|
-
MastraDBMessage,
|
|
23
|
-
} from "@mastra/core/agent/message-list";
|
|
22
|
+
import type { MastraDBMessage } from "@mastra/core/agent/message-list";
|
|
24
23
|
import {
|
|
25
24
|
MASTRA_RESOURCE_ID_KEY,
|
|
26
25
|
MASTRA_THREAD_ID_KEY,
|
|
27
26
|
} from "@mastra/core/request-context";
|
|
28
27
|
import { registerApiRoute } from "@mastra/core/server";
|
|
28
|
+
import type { ContextWithMastra } from "@mastra/core/server";
|
|
29
29
|
import type {
|
|
30
30
|
MastraHistoryResponse,
|
|
31
31
|
MastraHistoryUIMessage,
|
|
32
32
|
} from "@dbx-tools/appkit-mastra-shared";
|
|
33
33
|
|
|
34
|
+
const log = logUtils.logger("mastra/history");
|
|
35
|
+
|
|
34
36
|
/** Default history page size; matches the Mastra storage default. */
|
|
35
37
|
const DEFAULT_PER_PAGE = 20;
|
|
36
38
|
/** Hard cap so a misbehaving client can't fetch the whole thread at once. */
|
|
@@ -69,8 +71,10 @@ export async function loadHistory(
|
|
|
69
71
|
const page = Math.max(0, Math.trunc(opts.page ?? 0));
|
|
70
72
|
const memory = await opts.agent.getMemory();
|
|
71
73
|
if (!memory) {
|
|
74
|
+
log.debug("recall:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
|
|
72
75
|
return { uiMessages: [], page, perPage, total: 0, hasMore: false };
|
|
73
76
|
}
|
|
77
|
+
const startedAt = Date.now();
|
|
74
78
|
const result = await memory.recall({
|
|
75
79
|
threadId: opts.threadId,
|
|
76
80
|
...(opts.resourceId ? { resourceId: opts.resourceId } : {}),
|
|
@@ -85,6 +89,16 @@ export async function loadHistory(
|
|
|
85
89
|
const uiMessages = toAISdkV5Messages(
|
|
86
90
|
chronological,
|
|
87
91
|
) as unknown as MastraHistoryUIMessage[];
|
|
92
|
+
log.debug("recall:done", {
|
|
93
|
+
agentId: opts.agent.id,
|
|
94
|
+
threadId: opts.threadId,
|
|
95
|
+
page,
|
|
96
|
+
perPage,
|
|
97
|
+
returned: uiMessages.length,
|
|
98
|
+
total: result.total,
|
|
99
|
+
hasMore: result.hasMore,
|
|
100
|
+
elapsedMs: Date.now() - startedAt,
|
|
101
|
+
});
|
|
88
102
|
return {
|
|
89
103
|
uiMessages,
|
|
90
104
|
page,
|
|
@@ -122,7 +136,7 @@ export function historyRoute(options: HistoryRouteOptions) {
|
|
|
122
136
|
}
|
|
123
137
|
return registerApiRoute(path, {
|
|
124
138
|
method: "GET",
|
|
125
|
-
handler: async (c) => {
|
|
139
|
+
handler: async (c: ContextWithMastra) => {
|
|
126
140
|
const mastra = c.get("mastra");
|
|
127
141
|
const requestContext = c.get("requestContext");
|
|
128
142
|
const agentId = fixedAgent ?? c.req.param("agentId");
|
|
@@ -133,9 +147,7 @@ export function historyRoute(options: HistoryRouteOptions) {
|
|
|
133
147
|
if (!agent) {
|
|
134
148
|
return c.json({ error: `Unknown agent "${agentId}"` }, 404);
|
|
135
149
|
}
|
|
136
|
-
const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as
|
|
137
|
-
| string
|
|
138
|
-
| undefined;
|
|
150
|
+
const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as string | undefined;
|
|
139
151
|
if (!threadId) {
|
|
140
152
|
return c.json({ error: "thread id missing from request context" }, 400);
|
|
141
153
|
}
|
package/src/memory.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { lakebase } from "@databricks/appkit";
|
|
23
|
-
import { pluginUtils } from "@dbx-tools/appkit-shared";
|
|
23
|
+
import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
|
|
24
24
|
import { fastembed } from "@mastra/fastembed";
|
|
25
25
|
import { Memory } from "@mastra/memory";
|
|
26
26
|
import { PgVector, PostgresStore } from "@mastra/pg";
|
|
@@ -33,6 +33,8 @@ import type {
|
|
|
33
33
|
} from "./agents.js";
|
|
34
34
|
import type { MastraPluginConfig } from "./config.js";
|
|
35
35
|
|
|
36
|
+
const log = logUtils.logger("mastra/memory");
|
|
37
|
+
|
|
36
38
|
/** Pool handle returned by the AppKit `lakebase` plugin `exports().pool`. */
|
|
37
39
|
export type LakebasePool = ReturnType<
|
|
38
40
|
InstanceType<ReturnType<typeof lakebase>["plugin"]>["exports"]
|
|
@@ -109,7 +111,22 @@ export class MemoryBuilder {
|
|
|
109
111
|
|
|
110
112
|
const storage = this.buildStorage(agentId, storageSetting);
|
|
111
113
|
const vector = this.buildVector(memorySetting);
|
|
112
|
-
if (!storage && !vector)
|
|
114
|
+
if (!storage && !vector) {
|
|
115
|
+
log.debug("agent:stateless", { agentId });
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log.debug("agent:configured", {
|
|
120
|
+
agentId,
|
|
121
|
+
storage: storage !== undefined,
|
|
122
|
+
vector: vector !== undefined,
|
|
123
|
+
vectorMode:
|
|
124
|
+
vector === undefined
|
|
125
|
+
? "off"
|
|
126
|
+
: typeof memorySetting === "object"
|
|
127
|
+
? "dedicated"
|
|
128
|
+
: "shared",
|
|
129
|
+
});
|
|
113
130
|
|
|
114
131
|
return new Memory({
|
|
115
132
|
...(storage ? { storage } : {}),
|
package/src/model.ts
CHANGED
|
@@ -306,6 +306,7 @@ async function pickModelId(
|
|
|
306
306
|
user: User,
|
|
307
307
|
host: string,
|
|
308
308
|
): Promise<string> {
|
|
309
|
+
const log = logUtils.logger(config);
|
|
309
310
|
const serving = resolveServingConfig(config, FALLBACK_MODEL_IDS);
|
|
310
311
|
const override = serving.allowOverride
|
|
311
312
|
? (requestContext.get(MASTRA_MODEL_OVERRIDE_KEY) as string | undefined)
|
|
@@ -315,7 +316,10 @@ async function pickModelId(
|
|
|
315
316
|
|
|
316
317
|
// Cheap exit: when the caller named a specific model and fuzzy
|
|
317
318
|
// matching is off, there's no reason to touch the catalogue at all.
|
|
318
|
-
if (explicit !== undefined && !serving.fuzzy)
|
|
319
|
+
if (explicit !== undefined && !serving.fuzzy) {
|
|
320
|
+
log.debug("model selected", { modelId: explicit, source: "explicit" });
|
|
321
|
+
return explicit;
|
|
322
|
+
}
|
|
319
323
|
|
|
320
324
|
const endpoints = await listServingEndpoints(user.executionContext.client, host, {
|
|
321
325
|
ttlMs: serving.ttlMs,
|
|
@@ -324,7 +328,11 @@ async function pickModelId(
|
|
|
324
328
|
explicit !== undefined
|
|
325
329
|
? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
|
|
326
330
|
: pickFirstAvailable(serving.fallbacks, endpoints);
|
|
327
|
-
|
|
331
|
+
log.debug("model selected", {
|
|
332
|
+
modelId,
|
|
333
|
+
source: explicit !== undefined ? "fuzzy-match" : "fallback",
|
|
334
|
+
requestedExplicit: explicit,
|
|
335
|
+
});
|
|
328
336
|
return modelId;
|
|
329
337
|
}
|
|
330
338
|
|
|
@@ -369,9 +377,9 @@ interface ChatMessage {
|
|
|
369
377
|
* 1. Rewrites the outgoing `messages` array to repair Mastra/AI SDK
|
|
370
378
|
* stream-replay quirks that Databricks-hosted Claude rejects (see
|
|
371
379
|
* {@link sanitizeServingMessages}).
|
|
372
|
-
* 2.
|
|
373
|
-
*
|
|
374
|
-
*
|
|
380
|
+
* 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
|
|
381
|
+
* 4xx debugging doesn't have to fight AI SDK's `[Array]`
|
|
382
|
+
* formatter.
|
|
375
383
|
*
|
|
376
384
|
* Safe to call from any hot path: {@link commonUtils.memoize} ensures
|
|
377
385
|
* the wrapper is installed at most once per process, so subsequent
|
|
@@ -379,7 +387,7 @@ interface ChatMessage {
|
|
|
379
387
|
* {@link buildModel} fires on every agent step.
|
|
380
388
|
*/
|
|
381
389
|
const setupFetchInterceptor = commonUtils.memoize((): void => {
|
|
382
|
-
const
|
|
390
|
+
const log = logUtils.logger("mastra/llm");
|
|
383
391
|
const original = globalThis.fetch.bind(globalThis);
|
|
384
392
|
globalThis.fetch = (async (input, init) => {
|
|
385
393
|
const url = httpUtils.toURL(input);
|
|
@@ -394,13 +402,10 @@ const setupFetchInterceptor = commonUtils.memoize((): void => {
|
|
|
394
402
|
if (rewritten !== init.body) {
|
|
395
403
|
init = { ...init, body: rewritten };
|
|
396
404
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
} catch {
|
|
402
|
-
console.error("[mastra:llm-debug] -> POST", url.toString(), "(non-JSON body)");
|
|
403
|
-
}
|
|
405
|
+
try {
|
|
406
|
+
log.debug("POST", { url: url.toString(), body: JSON.parse(rewritten) });
|
|
407
|
+
} catch {
|
|
408
|
+
log.debug("POST", { url: url.toString(), bodyType: "non-JSON" });
|
|
404
409
|
}
|
|
405
410
|
return original(input, init);
|
|
406
411
|
}) as typeof globalThis.fetch;
|
package/src/plugin.ts
CHANGED
|
@@ -47,7 +47,6 @@ import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.js";
|
|
|
47
47
|
import type { MastraClientConfig } from "@dbx-tools/appkit-mastra-shared";
|
|
48
48
|
import type { MastraPluginConfig } from "./config.js";
|
|
49
49
|
import { historyRoute } from "./history.js";
|
|
50
|
-
import { renderChartRoute } from "./render-chart-route.js";
|
|
51
50
|
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
52
51
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
53
52
|
import {
|
|
@@ -191,7 +190,6 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
191
190
|
modelsPath: `${basePath}/models`,
|
|
192
191
|
historyPath: `${basePath}/route/history`,
|
|
193
192
|
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
194
|
-
renderChartPath: `${basePath}/route/render-chart`,
|
|
195
193
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
196
194
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
197
195
|
};
|
|
@@ -251,6 +249,11 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
251
249
|
? createMemoryBuilder(this.config, this.context)
|
|
252
250
|
: undefined;
|
|
253
251
|
|
|
252
|
+
this.log.debug("build:start", {
|
|
253
|
+
lakebase: memoryBuilder !== undefined,
|
|
254
|
+
stripStaleCharts: this.config.stripStaleCharts !== false,
|
|
255
|
+
});
|
|
256
|
+
|
|
254
257
|
// Build every agent declared in `config.agents` (or the built-in
|
|
255
258
|
// fallback when none are declared). Each agent's `model` resolves
|
|
256
259
|
// workspace URL + bearer at call time so concurrent requests get
|
|
@@ -280,10 +283,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
280
283
|
chatRoute({ path: "/route/chat/:agentId" }),
|
|
281
284
|
historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
|
|
282
285
|
historyRoute({ path: "/route/history/:agentId" }),
|
|
283
|
-
renderChartRoute({ path: "/route/render-chart", config: this.config }),
|
|
284
286
|
],
|
|
285
287
|
});
|
|
286
288
|
await this.mastraServer.init();
|
|
289
|
+
this.log.debug("build:done", {
|
|
290
|
+
agents: Object.keys(this.built.agents),
|
|
291
|
+
defaultAgent: this.built.defaultAgentId,
|
|
292
|
+
routes: ["/route/chat", "/route/history", "/models"],
|
|
293
|
+
});
|
|
287
294
|
}
|
|
288
295
|
}
|
|
289
296
|
|