@dbx-tools/appkit-mastra 0.3.28 → 0.3.30
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 +86 -15
- package/index.ts +3 -0
- package/package.json +10 -11
- package/src/agents.ts +12 -11
- package/src/chart.ts +69 -38
- package/src/config.ts +170 -18
- package/src/defaults.ts +133 -0
- package/src/filesystems.ts +29 -15
- package/src/genie.ts +81 -39
- package/src/history.ts +5 -2
- package/src/memory.ts +3 -4
- package/src/model.ts +15 -6
- package/src/plugin.ts +469 -194
- package/src/rest.ts +6 -7
- package/src/server.ts +5 -5
- package/src/serving-sanitize.ts +22 -9
- package/src/storage-schema.ts +4 -1
- package/src/threads.ts +10 -3
- package/src/validation.ts +22 -0
- package/test/config.test.ts +64 -0
package/src/genie.ts
CHANGED
|
@@ -36,7 +36,13 @@
|
|
|
36
36
|
* @module
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
CacheManager,
|
|
41
|
+
ConfigurationError,
|
|
42
|
+
ExecutionError,
|
|
43
|
+
genie,
|
|
44
|
+
ValidationError,
|
|
45
|
+
} from "@databricks/appkit";
|
|
40
46
|
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
41
47
|
import { plugin } from "@dbx-tools/appkit";
|
|
42
48
|
import { chat, space as genieSpace } from "@dbx-tools/genie";
|
|
@@ -50,7 +56,8 @@ import { z } from "zod";
|
|
|
50
56
|
|
|
51
57
|
import type { MastraTools } from "./agents";
|
|
52
58
|
import { chartPlannerRequestSchema, prepareChart } from "./chart";
|
|
53
|
-
import
|
|
59
|
+
import { MASTRA_USER_KEY, resolveUserKey } from "./config";
|
|
60
|
+
import type { MastraPluginConfig, User } from "./config";
|
|
54
61
|
import { fetchStatementData } from "./statement";
|
|
55
62
|
import { safeWrite } from "./writer";
|
|
56
63
|
|
|
@@ -113,11 +120,17 @@ function requireClient(
|
|
|
113
120
|
} {
|
|
114
121
|
const requestContext = ctx?.requestContext;
|
|
115
122
|
if (!requestContext) {
|
|
116
|
-
throw
|
|
123
|
+
throw ConfigurationError.resourceNotFound(
|
|
124
|
+
`${toolId} request context`,
|
|
125
|
+
"Invoke the tool from an agent turn served by the mastra plugin.",
|
|
126
|
+
);
|
|
117
127
|
}
|
|
118
128
|
const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
|
|
119
129
|
if (!user) {
|
|
120
|
-
throw
|
|
130
|
+
throw ConfigurationError.resourceNotFound(
|
|
131
|
+
`${toolId} user context`,
|
|
132
|
+
"Invoke the tool from an agent turn served by the mastra plugin, which stamps the AppKit user on the Mastra request context.",
|
|
133
|
+
);
|
|
121
134
|
}
|
|
122
135
|
return { client: user.executionContext.client, requestContext };
|
|
123
136
|
}
|
|
@@ -162,15 +175,6 @@ const CONVERSATION_TTL_SEC = 4 * 60 * 60;
|
|
|
162
175
|
/** Cache namespace prefix so coexisting Mastra caches don't collide. */
|
|
163
176
|
const CONVERSATION_CACHE_NAMESPACE = "mastra:genie:conversation";
|
|
164
177
|
|
|
165
|
-
/**
|
|
166
|
-
* `userKey` for `CacheManager.getOrExecute` / `generateKey`. Genie
|
|
167
|
-
* conversations are scoped to a single user + space + thread, and
|
|
168
|
-
* `threadId` is already user-scoped (Mastra mints threads per
|
|
169
|
-
* `resourceId`), so a constant user key here is safe and keeps the
|
|
170
|
-
* cache key short.
|
|
171
|
-
*/
|
|
172
|
-
const CONVERSATION_USER_KEY = "mastra-genie";
|
|
173
|
-
|
|
174
178
|
/**
|
|
175
179
|
* Build the per-request {@link RequestContext} key the active
|
|
176
180
|
* Genie `conversation_id` lives under for `spaceId`. Scoped by
|
|
@@ -210,18 +214,23 @@ function writeContextConversationId(
|
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
/**
|
|
213
|
-
* Build the canonical cache key for a `(spaceId, threadId)` pair
|
|
214
|
-
* Returns `undefined` when `threadId` is missing - callers should
|
|
217
|
+
* Build the canonical cache key for a `(spaceId, threadId)` pair, owned by
|
|
218
|
+
* `userKey`. Returns `undefined` when `threadId` is missing - callers should
|
|
215
219
|
* skip caching entirely in that case (no Mastra memory wired up).
|
|
220
|
+
*
|
|
221
|
+
* The identity is part of the key because a client picks its own `threadId`
|
|
222
|
+
* (the thread-selection header / `?threadId=`), so the id alone does not
|
|
223
|
+
* establish who owns the Genie conversation behind it.
|
|
216
224
|
*/
|
|
217
225
|
async function conversationCacheKey(
|
|
218
226
|
spaceId: string,
|
|
219
227
|
threadId: string | undefined,
|
|
228
|
+
userKey: string,
|
|
220
229
|
): Promise<string | undefined> {
|
|
221
230
|
if (!threadId) return undefined;
|
|
222
231
|
return (await CacheManager.getInstance()).generateKey(
|
|
223
232
|
[CONVERSATION_CACHE_NAMESPACE, spaceId, threadId],
|
|
224
|
-
|
|
233
|
+
userKey,
|
|
225
234
|
);
|
|
226
235
|
}
|
|
227
236
|
|
|
@@ -394,7 +403,18 @@ function buildAskGenieTool(opts: { spaceId: string; alias: string; hint?: string
|
|
|
394
403
|
automatically while the call is in flight.
|
|
395
404
|
`),
|
|
396
405
|
inputSchema: z.object({
|
|
397
|
-
question: z
|
|
406
|
+
question: z
|
|
407
|
+
.string()
|
|
408
|
+
.min(1, "question is required")
|
|
409
|
+
.describe(
|
|
410
|
+
string.toDescription(`
|
|
411
|
+
ONE focused natural-language question about the data in this
|
|
412
|
+
space, covering a single metric / dimension / time window
|
|
413
|
+
(e.g. "What was Q3 revenue by region?"). Decompose a
|
|
414
|
+
multi-part ask into separate calls rather than combining it
|
|
415
|
+
here.
|
|
416
|
+
`),
|
|
417
|
+
),
|
|
398
418
|
}),
|
|
399
419
|
outputSchema: z.object({
|
|
400
420
|
message: genieModel.GenieMessageSchema,
|
|
@@ -415,10 +435,10 @@ function buildAskGenieTool(opts: { spaceId: string; alias: string; hint?: string
|
|
|
415
435
|
// of wasting a turn.
|
|
416
436
|
const trimmed = question.trim();
|
|
417
437
|
if (trimmed.length === 0 || PLACEHOLDER_QUESTIONS.has(trimmed.toLowerCase())) {
|
|
418
|
-
throw
|
|
419
|
-
`${toolId}
|
|
420
|
-
|
|
421
|
-
|
|
438
|
+
throw ValidationError.invalidValue(
|
|
439
|
+
`${toolId}.question`,
|
|
440
|
+
question,
|
|
441
|
+
"a real natural-language question, not a placeholder; skip the call instead",
|
|
422
442
|
);
|
|
423
443
|
}
|
|
424
444
|
|
|
@@ -429,7 +449,11 @@ function buildAskGenieTool(opts: { spaceId: string; alias: string; hint?: string
|
|
|
429
449
|
// The same `RequestContext` is reused across every `ask_genie`
|
|
430
450
|
// call within one user turn, so `ensureConversationSeeded`
|
|
431
451
|
// hits the cache at most once per request per space.
|
|
432
|
-
const cacheKey = await conversationCacheKey(
|
|
452
|
+
const cacheKey = await conversationCacheKey(
|
|
453
|
+
spaceId,
|
|
454
|
+
threadId,
|
|
455
|
+
resolveUserKey(requestContext),
|
|
456
|
+
);
|
|
433
457
|
await ensureConversationSeeded(requestContext, spaceId, cacheKey);
|
|
434
458
|
|
|
435
459
|
// Fire the lifecycle `started` event before any LLM /
|
|
@@ -465,7 +489,7 @@ function buildAskGenieTool(opts: { spaceId: string; alias: string; hint?: string
|
|
|
465
489
|
}
|
|
466
490
|
}
|
|
467
491
|
if (!finalMessage) {
|
|
468
|
-
throw
|
|
492
|
+
throw ExecutionError.missingData("Genie result event");
|
|
469
493
|
}
|
|
470
494
|
return finalMessage;
|
|
471
495
|
};
|
|
@@ -611,7 +635,17 @@ function buildGetStatementTool() {
|
|
|
611
635
|
upstream total - compare to \`rows.length\` to detect truncation.
|
|
612
636
|
`),
|
|
613
637
|
inputSchema: z.object({
|
|
614
|
-
statement_id: z
|
|
638
|
+
statement_id: z
|
|
639
|
+
.string()
|
|
640
|
+
.min(1, "statement_id is required")
|
|
641
|
+
.describe(
|
|
642
|
+
string.toDescription(`
|
|
643
|
+
Genie \`statement_id\` whose rows to read. Take it from
|
|
644
|
+
\`message.query_result.statement_id\` or
|
|
645
|
+
\`message.attachments[*].query.statement_id\` on an
|
|
646
|
+
\`ask_genie\` result; it is never a value you construct.
|
|
647
|
+
`),
|
|
648
|
+
),
|
|
615
649
|
limit: z
|
|
616
650
|
.number()
|
|
617
651
|
.int()
|
|
@@ -701,9 +735,10 @@ function buildPrepareChartTool(opts: { config: MastraPluginConfig }) {
|
|
|
701
735
|
outputSchema: wire.ChartSchema.pick({ chartId: true }),
|
|
702
736
|
execute: async (request, ctxRaw) => {
|
|
703
737
|
const ctx = ctxRaw as ToolExecuteCtx;
|
|
704
|
-
const { client } = requireClient(ctx, toolId);
|
|
738
|
+
const { client, requestContext } = requireClient(ctx, toolId);
|
|
705
739
|
return prepareChart({
|
|
706
740
|
config,
|
|
741
|
+
userKey: resolveUserKey(requestContext),
|
|
707
742
|
...(request.title ? { title: request.title } : {}),
|
|
708
743
|
...(request.description ? { description: request.description } : {}),
|
|
709
744
|
resolveData: (taskSignal) =>
|
|
@@ -854,9 +889,12 @@ export const GENIE_INSTRUCTIONS = string.toDescription([
|
|
|
854
889
|
* Normalize the {@link GenieSpacesConfig} record. Bare-string
|
|
855
890
|
* entries (`{ default: "01ef..." }`) get wrapped as
|
|
856
891
|
* `{ spaceId: "01ef..." }`; object entries pass through unchanged.
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
892
|
+
*
|
|
893
|
+
* @throws ConfigurationError when an alias is present but carries no space id
|
|
894
|
+
* (`{ default: process.env.DATABRICKS_GENIE_SPACE_ID }` with the variable
|
|
895
|
+
* unset). An alias that resolves to nothing is a wiring contradiction: the
|
|
896
|
+
* agent would advertise no Genie tool for a space the deployment believes it
|
|
897
|
+
* configured, so it fails at construction instead of going quiet.
|
|
860
898
|
*/
|
|
861
899
|
export function normalizeGenieSpaces(
|
|
862
900
|
spaces: GenieSpacesConfig | Record<string, string | GenieSpaceConfig | undefined> | undefined,
|
|
@@ -864,18 +902,22 @@ export function normalizeGenieSpaces(
|
|
|
864
902
|
if (!spaces) return {};
|
|
865
903
|
const out: Record<string, GenieSpaceConfig> = {};
|
|
866
904
|
for (const [alias, value] of Object.entries(spaces)) {
|
|
867
|
-
|
|
868
|
-
if (
|
|
869
|
-
|
|
870
|
-
out[alias] = { spaceId: value };
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
if (!value.spaceId) continue;
|
|
874
|
-
out[alias] = value;
|
|
905
|
+
const spaceId = typeof value === "string" ? value : value?.spaceId;
|
|
906
|
+
if (!spaceId) throw missingSpaceId(alias);
|
|
907
|
+
out[alias] = typeof value === "string" ? { spaceId } : value!;
|
|
875
908
|
}
|
|
876
909
|
return out;
|
|
877
910
|
}
|
|
878
911
|
|
|
912
|
+
/** Config contradiction: an alias present in `genieSpaces` with no space id. */
|
|
913
|
+
function missingSpaceId(alias: string): ConfigurationError {
|
|
914
|
+
const envHint =
|
|
915
|
+
alias === DEFAULT_GENIE_ALIAS
|
|
916
|
+
? "Set DATABRICKS_GENIE_SPACE_ID, pass the space id inline, or drop the alias."
|
|
917
|
+
: `Pass the space id inline (DATABRICKS_GENIE_SPACE_ID only backs the "${DEFAULT_GENIE_ALIAS}" alias) or drop the alias.`;
|
|
918
|
+
return ConfigurationError.resourceNotFound(`genieSpaces.${alias} space id`, envHint);
|
|
919
|
+
}
|
|
920
|
+
|
|
879
921
|
/**
|
|
880
922
|
* AppKit `genie` plugin's config shape, derived from the factory
|
|
881
923
|
* itself so it stays in lock-step with the upstream type without
|
|
@@ -906,9 +948,9 @@ type AppKitGenieConfig = NonNullable<Parameters<typeof genie>[0]>;
|
|
|
906
948
|
* pair just works.
|
|
907
949
|
*
|
|
908
950
|
* Aliases collide cleanly: a higher-precedence source's value
|
|
909
|
-
* replaces a lower one's wholesale.
|
|
910
|
-
* aliases
|
|
911
|
-
*
|
|
951
|
+
* replaces a lower one's wholesale. A source that contributes zero
|
|
952
|
+
* aliases is skipped; a source that names an alias without a space
|
|
953
|
+
* id fails through {@link normalizeGenieSpaces}.
|
|
912
954
|
*/
|
|
913
955
|
export function resolveGenieSpaces(
|
|
914
956
|
config: MastraPluginConfig,
|
package/src/history.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* @module
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { ValidationError } from "@databricks/appkit";
|
|
21
22
|
import { error, log } from "@dbx-tools/shared-core";
|
|
22
23
|
import type {
|
|
23
24
|
MastraClearHistoryResponse,
|
|
@@ -207,8 +208,10 @@ export function historyRoute(options: HistoryRouteOptions) {
|
|
|
207
208
|
const { path } = options;
|
|
208
209
|
const fixedAgent = "agent" in options ? options.agent : undefined;
|
|
209
210
|
if (!fixedAgent && !path.includes(":agentId")) {
|
|
210
|
-
throw
|
|
211
|
-
"historyRoute
|
|
211
|
+
throw ValidationError.invalidValue(
|
|
212
|
+
"historyRoute.path",
|
|
213
|
+
path,
|
|
214
|
+
"a path containing `:agentId`, or an explicit `agent`",
|
|
212
215
|
);
|
|
213
216
|
}
|
|
214
217
|
// Tiny resolver shared by GET / DELETE: derive the active agent
|
package/src/memory.ts
CHANGED
|
@@ -29,9 +29,8 @@
|
|
|
29
29
|
* @module
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { randomUUID } from "node:crypto";
|
|
33
32
|
import { getUsernameWithApiLookup } from "@databricks/appkit";
|
|
34
|
-
import { log } from "@dbx-tools/shared-core";
|
|
33
|
+
import { hash, log } from "@dbx-tools/shared-core";
|
|
35
34
|
import { fastembed } from "@mastra/fastembed";
|
|
36
35
|
import { Memory } from "@mastra/memory";
|
|
37
36
|
import { PgVector, PostgresStore } from "@mastra/pg";
|
|
@@ -252,7 +251,7 @@ export class MemoryBuilder {
|
|
|
252
251
|
*/
|
|
253
252
|
function buildSharedPgVector(pool: Pool): PgVector {
|
|
254
253
|
const vector = new PgVector({
|
|
255
|
-
id: `pg${
|
|
254
|
+
id: `pg${hash.id()}`,
|
|
256
255
|
// Keep the recall index out of `public`: on a Lakebase database the app
|
|
257
256
|
// service principal has no CREATE on `public` (PG15+ locks it down), so a
|
|
258
257
|
// default-schema PgVector fails on CREATE INDEX with "permission denied for
|
|
@@ -276,7 +275,7 @@ function buildSharedPgVector(pool: Pool): PgVector {
|
|
|
276
275
|
/** Per-agent dedicated `PgVector` (rare; opt-in via object override). */
|
|
277
276
|
function buildPgVector(setting: MastraMemoryConfigOverride): PgVector {
|
|
278
277
|
return new PgVector(
|
|
279
|
-
withId(setting, `pg-vector__${
|
|
278
|
+
withId(setting, `pg-vector__${hash.id()}`) as ConstructorParameters<typeof PgVector>[0],
|
|
280
279
|
);
|
|
281
280
|
}
|
|
282
281
|
|
package/src/model.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { getExecutionContext } from "@databricks/appkit";
|
|
27
27
|
import { classes, resolve } from "@dbx-tools/model";
|
|
28
|
-
import { functionModule, log, net } from "@dbx-tools/shared-core";
|
|
28
|
+
import { functionModule, json, log, net } from "@dbx-tools/shared-core";
|
|
29
29
|
import { model } from "@dbx-tools/shared-model";
|
|
30
30
|
import type { MastraModelConfig } from "@mastra/core/llm";
|
|
31
31
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
@@ -62,6 +62,13 @@ export interface BuildModelOverrides {
|
|
|
62
62
|
* while `agent.stream` is inside the `asUser(req)` scope so tokens
|
|
63
63
|
* are user-scoped; outside an active user context the workspace
|
|
64
64
|
* client falls back to the service principal.
|
|
65
|
+
*
|
|
66
|
+
* Endpoint precedence: the per-request override
|
|
67
|
+
* ({@link MASTRA_MODEL_OVERRIDE_KEY}, only when `config.modelOverride` allows
|
|
68
|
+
* it), then {@link BuildModelOverrides.modelId} from the agent / plugin
|
|
69
|
+
* config, then `DATABRICKS_SERVING_ENDPOINT_NAME`. With none of those set the
|
|
70
|
+
* capability class and fallback ladder in `@dbx-tools/model` choose the
|
|
71
|
+
* endpoint.
|
|
65
72
|
*/
|
|
66
73
|
export async function buildModel(
|
|
67
74
|
config: MastraPluginConfig,
|
|
@@ -155,11 +162,13 @@ const setupFetchInterceptor = functionModule.memoize((): void => {
|
|
|
155
162
|
if (rewritten !== init.body) {
|
|
156
163
|
init = { ...init, body: rewritten };
|
|
157
164
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
const parsed = json.parse<unknown>(rewritten);
|
|
166
|
+
logger.debug(
|
|
167
|
+
"POST",
|
|
168
|
+
parsed === undefined
|
|
169
|
+
? { url: url.toString(), bodyType: "non-JSON" }
|
|
170
|
+
: { url: url.toString(), body: parsed },
|
|
171
|
+
);
|
|
163
172
|
return original(input, init);
|
|
164
173
|
}) as typeof globalThis.fetch;
|
|
165
174
|
});
|