@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/dist/src/genie.d.ts
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* `instructions` when you want the canonical "how to drive the
|
|
34
34
|
* Genie tools" guidance.
|
|
35
35
|
*/
|
|
36
|
+
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
36
37
|
import { appkitUtils } from "@dbx-tools/shared";
|
|
37
38
|
import type { MastraTools } from "./agents.js";
|
|
38
39
|
import type { MastraPluginConfig } from "./config.js";
|
|
@@ -153,3 +154,20 @@ export declare function buildGenieToolkitProvider(opts: {
|
|
|
153
154
|
* absent-plugin semantics) when neither source is configured.
|
|
154
155
|
*/
|
|
155
156
|
export declare function hasAnyGenieSpaces(config: MastraPluginConfig, context: appkitUtils.PluginContextLike | undefined): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Collect the curated starter questions across every resolved Genie
|
|
159
|
+
* space, deduped and capped. Each space's `sample_questions` are
|
|
160
|
+
* fetched once (cached for {@link SUGGESTION_CACHE_TTL_MS}) via
|
|
161
|
+
* {@link getGenieSpace} + {@link genieSampleQuestions}, then merged in
|
|
162
|
+
* alias-iteration order so a single-space app surfaces that space's
|
|
163
|
+
* questions and a multi-space app round-trips breadth-first up to the
|
|
164
|
+
* cap. A per-space fetch failure degrades to "no questions for that
|
|
165
|
+
* space" (logged, not thrown) so one unreachable space never blanks
|
|
166
|
+
* the whole list. Returns `[]` when no spaces are configured.
|
|
167
|
+
*/
|
|
168
|
+
export declare function collectSpaceSuggestions(opts: {
|
|
169
|
+
spaces: Record<string, GenieSpaceConfig>;
|
|
170
|
+
client: WorkspaceClient;
|
|
171
|
+
signal?: AbortSignal;
|
|
172
|
+
limit?: number;
|
|
173
|
+
}): Promise<string[]>;
|
package/dist/src/genie.js
CHANGED
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
import { CacheManager, genie } from "@databricks/appkit";
|
|
37
37
|
import { ApiError, HttpError, WorkspaceClient } from "@databricks/sdk-experimental";
|
|
38
38
|
import { ChartSchema, } from "@dbx-tools/appkit-mastra-shared";
|
|
39
|
-
import { genieEventChat } from "@dbx-tools/genie";
|
|
39
|
+
import { genieEventChat, genieSampleQuestions, getGenieSpace } from "@dbx-tools/genie";
|
|
40
40
|
import {} from "@dbx-tools/genie-shared";
|
|
41
|
-
import {
|
|
41
|
+
import { appkitUtils, commonUtils, logUtils, stringUtils } from "@dbx-tools/shared";
|
|
42
42
|
import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
|
|
43
43
|
import { createTool } from "@mastra/core/tools";
|
|
44
44
|
import { z } from "zod";
|
|
@@ -461,8 +461,15 @@ function buildSpaceDescriptionTool(opts) {
|
|
|
461
461
|
const ctx = ctxRaw;
|
|
462
462
|
const { client } = requireClient(ctx, toolId);
|
|
463
463
|
const signal = ctx?.abortSignal;
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
// Route through the package's central Genie space fetch. The
|
|
465
|
+
// description surface (title / description / warehouse id) lives
|
|
466
|
+
// on the directory-listing shape, so skip the larger
|
|
467
|
+
// `serialized_space` payload here.
|
|
468
|
+
const space = await getGenieSpace(spaceId, {
|
|
469
|
+
workspaceClient: client,
|
|
470
|
+
serialized: false,
|
|
471
|
+
...(signal ? { context: signal } : {}),
|
|
472
|
+
});
|
|
466
473
|
return {
|
|
467
474
|
spaceId,
|
|
468
475
|
...(space.title ? { title: space.title } : {}),
|
|
@@ -489,8 +496,13 @@ function buildSpaceSerializedTool(opts) {
|
|
|
489
496
|
const ctx = ctxRaw;
|
|
490
497
|
const { client } = requireClient(ctx, toolId);
|
|
491
498
|
const signal = ctx?.abortSignal;
|
|
492
|
-
|
|
493
|
-
|
|
499
|
+
// Central Genie space fetch with the opt-in `serialized_space`
|
|
500
|
+
// blob (catalogs, tables, sample questions, prompts) that the
|
|
501
|
+
// typed SDK `getSpace` omits - the whole point of this tool.
|
|
502
|
+
const space = await getGenieSpace(spaceId, {
|
|
503
|
+
workspaceClient: client,
|
|
504
|
+
...(signal ? { context: signal } : {}),
|
|
505
|
+
});
|
|
494
506
|
return { space };
|
|
495
507
|
},
|
|
496
508
|
});
|
|
@@ -893,3 +905,83 @@ export function buildGenieToolkitProvider(opts) {
|
|
|
893
905
|
export function hasAnyGenieSpaces(config, context) {
|
|
894
906
|
return Object.keys(resolveGenieSpaces(config, context)).length > 0;
|
|
895
907
|
}
|
|
908
|
+
/* --------------------------- starter suggestions --------------------------- */
|
|
909
|
+
/**
|
|
910
|
+
* Default cap on starter suggestions surfaced to the chat empty
|
|
911
|
+
* state. Sample-question lists can run long (10+ on a curated
|
|
912
|
+
* space); a handful of one-tap prompts reads better than a wall of
|
|
913
|
+
* buttons.
|
|
914
|
+
*/
|
|
915
|
+
const SUGGESTION_LIMIT = 6;
|
|
916
|
+
/**
|
|
917
|
+
* How long a space's parsed sample questions stay cached. They're
|
|
918
|
+
* authored config that changes rarely, and the lookup is an extra
|
|
919
|
+
* REST round-trip per chat mount, so a few minutes of caching keeps
|
|
920
|
+
* the empty state instant on reload without going stale for long.
|
|
921
|
+
*/
|
|
922
|
+
const SUGGESTION_CACHE_TTL_MS = 10 * 60_000;
|
|
923
|
+
/** Space-id -> cached sample questions with an absolute expiry. */
|
|
924
|
+
const _suggestionCache = new Map();
|
|
925
|
+
/** Read a space's cached questions, or `undefined` on miss / expiry. */
|
|
926
|
+
function readSuggestionCache(spaceId) {
|
|
927
|
+
const entry = _suggestionCache.get(spaceId);
|
|
928
|
+
if (!entry)
|
|
929
|
+
return undefined;
|
|
930
|
+
if (entry.expires <= Date.now()) {
|
|
931
|
+
_suggestionCache.delete(spaceId);
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
return entry.questions;
|
|
935
|
+
}
|
|
936
|
+
/** Cache a space's parsed questions for {@link SUGGESTION_CACHE_TTL_MS}. */
|
|
937
|
+
function writeSuggestionCache(spaceId, questions) {
|
|
938
|
+
_suggestionCache.set(spaceId, {
|
|
939
|
+
questions,
|
|
940
|
+
expires: Date.now() + SUGGESTION_CACHE_TTL_MS,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Collect the curated starter questions across every resolved Genie
|
|
945
|
+
* space, deduped and capped. Each space's `sample_questions` are
|
|
946
|
+
* fetched once (cached for {@link SUGGESTION_CACHE_TTL_MS}) via
|
|
947
|
+
* {@link getGenieSpace} + {@link genieSampleQuestions}, then merged in
|
|
948
|
+
* alias-iteration order so a single-space app surfaces that space's
|
|
949
|
+
* questions and a multi-space app round-trips breadth-first up to the
|
|
950
|
+
* cap. A per-space fetch failure degrades to "no questions for that
|
|
951
|
+
* space" (logged, not thrown) so one unreachable space never blanks
|
|
952
|
+
* the whole list. Returns `[]` when no spaces are configured.
|
|
953
|
+
*/
|
|
954
|
+
export async function collectSpaceSuggestions(opts) {
|
|
955
|
+
const limit = opts.limit ?? SUGGESTION_LIMIT;
|
|
956
|
+
const merged = [];
|
|
957
|
+
const seen = new Set();
|
|
958
|
+
for (const { spaceId } of Object.values(opts.spaces)) {
|
|
959
|
+
let questions = readSuggestionCache(spaceId);
|
|
960
|
+
if (!questions) {
|
|
961
|
+
try {
|
|
962
|
+
const space = await getGenieSpace(spaceId, {
|
|
963
|
+
workspaceClient: opts.client,
|
|
964
|
+
...(opts.signal ? { context: opts.signal } : {}),
|
|
965
|
+
});
|
|
966
|
+
questions = genieSampleQuestions(space);
|
|
967
|
+
writeSuggestionCache(spaceId, questions);
|
|
968
|
+
}
|
|
969
|
+
catch (err) {
|
|
970
|
+
log.warn("suggestions:fetch-error", {
|
|
971
|
+
spaceId,
|
|
972
|
+
error: commonUtils.errorMessage(err),
|
|
973
|
+
});
|
|
974
|
+
questions = [];
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
for (const question of questions) {
|
|
978
|
+
if (seen.has(question))
|
|
979
|
+
continue;
|
|
980
|
+
seen.add(question);
|
|
981
|
+
merged.push(question);
|
|
982
|
+
if (merged.length >= limit)
|
|
983
|
+
return merged;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return merged;
|
|
987
|
+
}
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -109,6 +109,15 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
109
109
|
};
|
|
110
110
|
clientConfig(): Record<string, unknown>;
|
|
111
111
|
injectRoutes(router: IAppRouter): void;
|
|
112
|
+
/**
|
|
113
|
+
* Implementation backing the `/suggestions` route. Runs inside the
|
|
114
|
+
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
115
|
+
* OBO-scoped client. Resolves the plugin's Genie spaces and merges
|
|
116
|
+
* their curated `sample_questions` (see {@link collectSpaceSuggestions}).
|
|
117
|
+
* Returns `[]` when no Genie space is configured so the client
|
|
118
|
+
* shows a bare empty state instead of built-in example prompts.
|
|
119
|
+
*/
|
|
120
|
+
private fetchSuggestions;
|
|
112
121
|
/**
|
|
113
122
|
* Implementation backing the `data` embed resolver
|
|
114
123
|
* (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
|
package/dist/src/plugin.js
CHANGED
|
@@ -33,6 +33,7 @@ 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 { fetchChart } from "./chart.js";
|
|
36
|
+
import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.js";
|
|
36
37
|
import { historyRoute } from "./history.js";
|
|
37
38
|
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
38
39
|
import { buildObservability } from "./observability.js";
|
|
@@ -174,6 +175,8 @@ export class MastraPlugin extends Plugin {
|
|
|
174
175
|
historyPath: `${basePath}/route/history`,
|
|
175
176
|
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
176
177
|
embedPathTemplate: `${basePath}/embed/:type/:id`,
|
|
178
|
+
suggestionsPath: `${basePath}/suggestions`,
|
|
179
|
+
suggestionsPathTemplate: `${basePath}/suggestions/:agentId`,
|
|
177
180
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
178
181
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
179
182
|
};
|
|
@@ -266,12 +269,59 @@ export class MastraPlugin extends Plugin {
|
|
|
266
269
|
})
|
|
267
270
|
.catch(next);
|
|
268
271
|
});
|
|
272
|
+
// `GET /suggestions` (and `/suggestions/:agentId`) returns the
|
|
273
|
+
// curated starter questions for the agent's Genie space(s) - the
|
|
274
|
+
// author-configured `sample_questions`, surfaced as one-tap
|
|
275
|
+
// prompts on the chat empty state. Returns `{ questions: [] }`
|
|
276
|
+
// when no Genie space is wired so the client renders a bare
|
|
277
|
+
// empty state (no built-in example prompts). The `:agentId`
|
|
278
|
+
// segment is accepted for URL symmetry with the chat / history
|
|
279
|
+
// routes; Genie spaces are resolved per-plugin, not per-agent,
|
|
280
|
+
// so it doesn't change the result. OBO-scoped like the other
|
|
281
|
+
// data routes so the space lookup runs as the calling user.
|
|
282
|
+
const handleSuggestions = (req, res) => {
|
|
283
|
+
const controller = new AbortController();
|
|
284
|
+
req.on("close", () => controller.abort());
|
|
285
|
+
this.userScopedSelf(req)
|
|
286
|
+
.fetchSuggestions(controller.signal)
|
|
287
|
+
.then((questions) => res.json({ questions }))
|
|
288
|
+
.catch((err) => {
|
|
289
|
+
// Suggestions are a non-critical enhancement; a lookup
|
|
290
|
+
// failure should leave the chat usable with a bare empty
|
|
291
|
+
// state rather than surfacing a 500. Log and degrade.
|
|
292
|
+
this.log.warn("suggestions:error", {
|
|
293
|
+
error: err instanceof Error ? err.message : String(err),
|
|
294
|
+
});
|
|
295
|
+
res.json({ questions: [] });
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
router.get("/suggestions", handleSuggestions);
|
|
299
|
+
router.get("/suggestions/:agentId", handleSuggestions);
|
|
269
300
|
router.use("", (req, res, next) => {
|
|
270
301
|
if (!this.mastraApp)
|
|
271
302
|
return res.status(503).end();
|
|
272
303
|
return this.userScopedSelf(req).mastraApp(req, res, next);
|
|
273
304
|
});
|
|
274
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Implementation backing the `/suggestions` route. Runs inside the
|
|
308
|
+
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
309
|
+
* OBO-scoped client. Resolves the plugin's Genie spaces and merges
|
|
310
|
+
* their curated `sample_questions` (see {@link collectSpaceSuggestions}).
|
|
311
|
+
* Returns `[]` when no Genie space is configured so the client
|
|
312
|
+
* shows a bare empty state instead of built-in example prompts.
|
|
313
|
+
*/
|
|
314
|
+
async fetchSuggestions(signal) {
|
|
315
|
+
const spaces = resolveGenieSpaces(this.config, this.context);
|
|
316
|
+
if (Object.keys(spaces).length === 0)
|
|
317
|
+
return [];
|
|
318
|
+
const client = getExecutionContext().client;
|
|
319
|
+
return collectSpaceSuggestions({
|
|
320
|
+
spaces,
|
|
321
|
+
client,
|
|
322
|
+
...(signal ? { signal } : {}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
275
325
|
/**
|
|
276
326
|
* Implementation backing the `data` embed resolver
|
|
277
327
|
* (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
|