@dbx-tools/appkit-mastra 0.1.33 → 0.1.35
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/dist/src/genie.d.ts +18 -0
- package/dist/src/genie.js +98 -6
- package/dist/src/plugin.d.ts +9 -0
- package/dist/src/plugin.js +50 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/genie.ts +107 -12
- package/src/plugin.ts +51 -0
package/package.json
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"name": "@dbx-tools/appkit-mastra",
|
|
12
|
-
"version": "0.1.
|
|
12
|
+
"version": "0.1.35",
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@databricks/sdk-experimental": "^0.17",
|
|
15
|
-
"@dbx-tools/appkit-mastra-shared": "0.1.
|
|
16
|
-
"@dbx-tools/genie": "0.1.
|
|
17
|
-
"@dbx-tools/genie-shared": "0.1.
|
|
18
|
-
"@dbx-tools/shared": "0.1.
|
|
15
|
+
"@dbx-tools/appkit-mastra-shared": "0.1.35",
|
|
16
|
+
"@dbx-tools/genie": "0.1.35",
|
|
17
|
+
"@dbx-tools/genie-shared": "0.1.35",
|
|
18
|
+
"@dbx-tools/shared": "0.1.35",
|
|
19
19
|
"@mastra/ai-sdk": "^1",
|
|
20
20
|
"@mastra/core": "^1",
|
|
21
21
|
"@mastra/express": "^1",
|
package/src/genie.ts
CHANGED
|
@@ -41,15 +41,9 @@ import {
|
|
|
41
41
|
type MinimalWriter,
|
|
42
42
|
type StartedEvent,
|
|
43
43
|
} from "@dbx-tools/appkit-mastra-shared";
|
|
44
|
-
import { genieEventChat } from "@dbx-tools/genie";
|
|
44
|
+
import { genieEventChat, genieSampleQuestions, getGenieSpace } from "@dbx-tools/genie";
|
|
45
45
|
import { type GenieMessage } from "@dbx-tools/genie-shared";
|
|
46
|
-
import {
|
|
47
|
-
apiUtils,
|
|
48
|
-
appkitUtils,
|
|
49
|
-
commonUtils,
|
|
50
|
-
logUtils,
|
|
51
|
-
stringUtils,
|
|
52
|
-
} from "@dbx-tools/shared";
|
|
46
|
+
import { appkitUtils, commonUtils, logUtils, stringUtils } from "@dbx-tools/shared";
|
|
53
47
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
54
48
|
import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
|
|
55
49
|
import { createTool } from "@mastra/core/tools";
|
|
@@ -565,8 +559,15 @@ function buildSpaceDescriptionTool(opts: { spaceId: string; alias: string }) {
|
|
|
565
559
|
const ctx = ctxRaw as ToolExecuteCtx;
|
|
566
560
|
const { client } = requireClient(ctx, toolId);
|
|
567
561
|
const signal = ctx?.abortSignal;
|
|
568
|
-
|
|
569
|
-
|
|
562
|
+
// Route through the package's central Genie space fetch. The
|
|
563
|
+
// description surface (title / description / warehouse id) lives
|
|
564
|
+
// on the directory-listing shape, so skip the larger
|
|
565
|
+
// `serialized_space` payload here.
|
|
566
|
+
const space = await getGenieSpace(spaceId, {
|
|
567
|
+
workspaceClient: client,
|
|
568
|
+
serialized: false,
|
|
569
|
+
...(signal ? { context: signal } : {}),
|
|
570
|
+
});
|
|
570
571
|
return {
|
|
571
572
|
spaceId,
|
|
572
573
|
...(space.title ? { title: space.title } : {}),
|
|
@@ -594,8 +595,13 @@ function buildSpaceSerializedTool(opts: { spaceId: string; alias: string }) {
|
|
|
594
595
|
const ctx = ctxRaw as ToolExecuteCtx;
|
|
595
596
|
const { client } = requireClient(ctx, toolId);
|
|
596
597
|
const signal = ctx?.abortSignal;
|
|
597
|
-
|
|
598
|
-
|
|
598
|
+
// Central Genie space fetch with the opt-in `serialized_space`
|
|
599
|
+
// blob (catalogs, tables, sample questions, prompts) that the
|
|
600
|
+
// typed SDK `getSpace` omits - the whole point of this tool.
|
|
601
|
+
const space = await getGenieSpace(spaceId, {
|
|
602
|
+
workspaceClient: client,
|
|
603
|
+
...(signal ? { context: signal } : {}),
|
|
604
|
+
});
|
|
599
605
|
return { space };
|
|
600
606
|
},
|
|
601
607
|
});
|
|
@@ -1044,3 +1050,92 @@ export function hasAnyGenieSpaces(
|
|
|
1044
1050
|
): boolean {
|
|
1045
1051
|
return Object.keys(resolveGenieSpaces(config, context)).length > 0;
|
|
1046
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
/* --------------------------- starter suggestions --------------------------- */
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Default cap on starter suggestions surfaced to the chat empty
|
|
1058
|
+
* state. Sample-question lists can run long (10+ on a curated
|
|
1059
|
+
* space); a handful of one-tap prompts reads better than a wall of
|
|
1060
|
+
* buttons.
|
|
1061
|
+
*/
|
|
1062
|
+
const SUGGESTION_LIMIT = 6;
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* How long a space's parsed sample questions stay cached. They're
|
|
1066
|
+
* authored config that changes rarely, and the lookup is an extra
|
|
1067
|
+
* REST round-trip per chat mount, so a few minutes of caching keeps
|
|
1068
|
+
* the empty state instant on reload without going stale for long.
|
|
1069
|
+
*/
|
|
1070
|
+
const SUGGESTION_CACHE_TTL_MS = 10 * 60_000;
|
|
1071
|
+
|
|
1072
|
+
/** Space-id -> cached sample questions with an absolute expiry. */
|
|
1073
|
+
const _suggestionCache = new Map<string, { questions: string[]; expires: number }>();
|
|
1074
|
+
|
|
1075
|
+
/** Read a space's cached questions, or `undefined` on miss / expiry. */
|
|
1076
|
+
function readSuggestionCache(spaceId: string): string[] | undefined {
|
|
1077
|
+
const entry = _suggestionCache.get(spaceId);
|
|
1078
|
+
if (!entry) return undefined;
|
|
1079
|
+
if (entry.expires <= Date.now()) {
|
|
1080
|
+
_suggestionCache.delete(spaceId);
|
|
1081
|
+
return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
return entry.questions;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/** Cache a space's parsed questions for {@link SUGGESTION_CACHE_TTL_MS}. */
|
|
1087
|
+
function writeSuggestionCache(spaceId: string, questions: string[]): void {
|
|
1088
|
+
_suggestionCache.set(spaceId, {
|
|
1089
|
+
questions,
|
|
1090
|
+
expires: Date.now() + SUGGESTION_CACHE_TTL_MS,
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Collect the curated starter questions across every resolved Genie
|
|
1096
|
+
* space, deduped and capped. Each space's `sample_questions` are
|
|
1097
|
+
* fetched once (cached for {@link SUGGESTION_CACHE_TTL_MS}) via
|
|
1098
|
+
* {@link getGenieSpace} + {@link genieSampleQuestions}, then merged in
|
|
1099
|
+
* alias-iteration order so a single-space app surfaces that space's
|
|
1100
|
+
* questions and a multi-space app round-trips breadth-first up to the
|
|
1101
|
+
* cap. A per-space fetch failure degrades to "no questions for that
|
|
1102
|
+
* space" (logged, not thrown) so one unreachable space never blanks
|
|
1103
|
+
* the whole list. Returns `[]` when no spaces are configured.
|
|
1104
|
+
*/
|
|
1105
|
+
export async function collectSpaceSuggestions(opts: {
|
|
1106
|
+
spaces: Record<string, GenieSpaceConfig>;
|
|
1107
|
+
client: WorkspaceClient;
|
|
1108
|
+
signal?: AbortSignal;
|
|
1109
|
+
limit?: number;
|
|
1110
|
+
}): Promise<string[]> {
|
|
1111
|
+
const limit = opts.limit ?? SUGGESTION_LIMIT;
|
|
1112
|
+
const merged: string[] = [];
|
|
1113
|
+
const seen = new Set<string>();
|
|
1114
|
+
|
|
1115
|
+
for (const { spaceId } of Object.values(opts.spaces)) {
|
|
1116
|
+
let questions = readSuggestionCache(spaceId);
|
|
1117
|
+
if (!questions) {
|
|
1118
|
+
try {
|
|
1119
|
+
const space = await getGenieSpace(spaceId, {
|
|
1120
|
+
workspaceClient: opts.client,
|
|
1121
|
+
...(opts.signal ? { context: opts.signal } : {}),
|
|
1122
|
+
});
|
|
1123
|
+
questions = genieSampleQuestions(space);
|
|
1124
|
+
writeSuggestionCache(spaceId, questions);
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
log.warn("suggestions:fetch-error", {
|
|
1127
|
+
spaceId,
|
|
1128
|
+
error: commonUtils.errorMessage(err),
|
|
1129
|
+
});
|
|
1130
|
+
questions = [];
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
for (const question of questions) {
|
|
1134
|
+
if (seen.has(question)) continue;
|
|
1135
|
+
seen.add(question);
|
|
1136
|
+
merged.push(question);
|
|
1137
|
+
if (merged.length >= limit) return merged;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return merged;
|
|
1141
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -50,6 +50,7 @@ import type {
|
|
|
50
50
|
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.js";
|
|
51
51
|
import { fetchChart } from "./chart.js";
|
|
52
52
|
import type { MastraPluginConfig } from "./config.js";
|
|
53
|
+
import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.js";
|
|
53
54
|
import { historyRoute } from "./history.js";
|
|
54
55
|
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
55
56
|
import { buildObservability } from "./observability.js";
|
|
@@ -209,6 +210,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
209
210
|
historyPath: `${basePath}/route/history`,
|
|
210
211
|
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
211
212
|
embedPathTemplate: `${basePath}/embed/:type/:id`,
|
|
213
|
+
suggestionsPath: `${basePath}/suggestions`,
|
|
214
|
+
suggestionsPathTemplate: `${basePath}/suggestions/:agentId`,
|
|
212
215
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
213
216
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
214
217
|
};
|
|
@@ -305,12 +308,60 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
305
308
|
.catch(next);
|
|
306
309
|
});
|
|
307
310
|
|
|
311
|
+
// `GET /suggestions` (and `/suggestions/:agentId`) returns the
|
|
312
|
+
// curated starter questions for the agent's Genie space(s) - the
|
|
313
|
+
// author-configured `sample_questions`, surfaced as one-tap
|
|
314
|
+
// prompts on the chat empty state. Returns `{ questions: [] }`
|
|
315
|
+
// when no Genie space is wired so the client renders a bare
|
|
316
|
+
// empty state (no built-in example prompts). The `:agentId`
|
|
317
|
+
// segment is accepted for URL symmetry with the chat / history
|
|
318
|
+
// routes; Genie spaces are resolved per-plugin, not per-agent,
|
|
319
|
+
// so it doesn't change the result. OBO-scoped like the other
|
|
320
|
+
// data routes so the space lookup runs as the calling user.
|
|
321
|
+
const handleSuggestions = (req: express.Request, res: express.Response): void => {
|
|
322
|
+
const controller = new AbortController();
|
|
323
|
+
req.on("close", () => controller.abort());
|
|
324
|
+
this.userScopedSelf(req)
|
|
325
|
+
.fetchSuggestions(controller.signal)
|
|
326
|
+
.then((questions) => res.json({ questions }))
|
|
327
|
+
.catch((err: unknown) => {
|
|
328
|
+
// Suggestions are a non-critical enhancement; a lookup
|
|
329
|
+
// failure should leave the chat usable with a bare empty
|
|
330
|
+
// state rather than surfacing a 500. Log and degrade.
|
|
331
|
+
this.log.warn("suggestions:error", {
|
|
332
|
+
error: err instanceof Error ? err.message : String(err),
|
|
333
|
+
});
|
|
334
|
+
res.json({ questions: [] });
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
router.get("/suggestions", handleSuggestions);
|
|
338
|
+
router.get("/suggestions/:agentId", handleSuggestions);
|
|
339
|
+
|
|
308
340
|
router.use("", (req, res, next) => {
|
|
309
341
|
if (!this.mastraApp) return res.status(503).end();
|
|
310
342
|
return this.userScopedSelf(req).mastraApp!(req, res, next);
|
|
311
343
|
});
|
|
312
344
|
}
|
|
313
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Implementation backing the `/suggestions` route. Runs inside the
|
|
348
|
+
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
349
|
+
* OBO-scoped client. Resolves the plugin's Genie spaces and merges
|
|
350
|
+
* their curated `sample_questions` (see {@link collectSpaceSuggestions}).
|
|
351
|
+
* Returns `[]` when no Genie space is configured so the client
|
|
352
|
+
* shows a bare empty state instead of built-in example prompts.
|
|
353
|
+
*/
|
|
354
|
+
private async fetchSuggestions(signal?: AbortSignal): Promise<string[]> {
|
|
355
|
+
const spaces = resolveGenieSpaces(this.config, this.context);
|
|
356
|
+
if (Object.keys(spaces).length === 0) return [];
|
|
357
|
+
const client = getExecutionContext().client;
|
|
358
|
+
return collectSpaceSuggestions({
|
|
359
|
+
spaces,
|
|
360
|
+
client,
|
|
361
|
+
...(signal ? { signal } : {}),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
314
365
|
/**
|
|
315
366
|
* Implementation backing the `data` embed resolver
|
|
316
367
|
* (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
|