@dbx-tools/appkit-mastra 0.1.40 → 0.1.42
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 +59 -58
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/src/agents.d.ts +3 -3
- package/dist/src/agents.js +1 -1
- package/dist/src/chart.d.ts +7 -1
- package/dist/src/chart.js +3 -5
- package/dist/src/config.d.ts +14 -11
- package/dist/src/genie.d.ts +0 -12
- package/dist/src/genie.js +2 -16
- package/dist/src/history.d.ts +6 -6
- package/dist/src/history.js +6 -6
- package/dist/src/memory.d.ts +34 -25
- package/dist/src/memory.js +48 -35
- package/dist/src/model.d.ts +24 -131
- package/dist/src/model.js +39 -268
- package/dist/src/plugin.d.ts +28 -12
- package/dist/src/plugin.js +86 -42
- package/dist/src/server.d.ts +13 -20
- package/dist/src/server.js +15 -22
- package/dist/src/serving.d.ts +14 -98
- package/dist/src/serving.js +18 -163
- package/dist/src/tools/email.d.ts +10 -1
- package/dist/src/writer.d.ts +2 -2
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/index.ts +2 -13
- package/package.json +7 -6
- package/src/agents.ts +3 -3
- package/src/chart.ts +3 -5
- package/src/config.ts +14 -11
- package/src/genie.ts +4 -22
- package/src/history.ts +6 -6
- package/src/memory.ts +48 -45
- package/src/model.ts +57 -291
- package/src/plugin.ts +99 -50
- package/src/server.ts +15 -22
- package/src/serving.ts +18 -220
- package/src/writer.ts +2 -2
- package/dist/src/intercept.d.ts +0 -48
- package/dist/src/intercept.js +0 -167
- package/src/intercept.ts +0 -206
package/src/model.ts
CHANGED
|
@@ -7,237 +7,53 @@
|
|
|
7
7
|
* fresh bearer header, then point Mastra's OpenAI-compatible provider
|
|
8
8
|
* at `/serving-endpoints` on that host.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* names like `"claude sonnet"` resolve to the real endpoint name.
|
|
23
|
-
* Fuzzy matching is best-effort: when the workspace client throws
|
|
24
|
-
* (network blip, expired token at cache-fill time) we fall back to
|
|
25
|
-
* the input verbatim and let Databricks return the canonical error.
|
|
10
|
+
* This module only adds the Mastra-specific glue. The actual model
|
|
11
|
+
* selection - listing the workspace catalogue and resolving an
|
|
12
|
+
* explicit name / tier / fallback chain to a real endpoint id - lives
|
|
13
|
+
* in `@dbx-tools/model` ({@link selectModel}) so non-Mastra consumers
|
|
14
|
+
* (e.g. a job that just needs a model name) can reuse it. Here we
|
|
15
|
+
* assemble the explicit ask from Mastra's request context (the
|
|
16
|
+
* per-request override under {@link MASTRA_MODEL_OVERRIDE_KEY}, the
|
|
17
|
+
* agent / plugin `modelId`, or `DATABRICKS_SERVING_ENDPOINT_NAME`),
|
|
18
|
+
* pass the plugin's fuzzy / tier / fallback knobs through, and wrap
|
|
19
|
+
* the resolved id in the OpenAI-compatible provider config Mastra
|
|
20
|
+
* expects. Catalogue fetches fail loud: network / auth errors
|
|
21
|
+
* propagate so callers see the real SDK message.
|
|
26
22
|
*/
|
|
27
23
|
|
|
24
|
+
import { type ModelTier, parseModelTier, selectModel } from "@dbx-tools/model";
|
|
28
25
|
import { commonUtils, logUtils, netUtils, stringUtils } from "@dbx-tools/shared";
|
|
29
26
|
import type { MastraModelConfig } from "@mastra/core/llm";
|
|
30
27
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
31
28
|
|
|
32
29
|
import { MASTRA_USER_KEY, type MastraPluginConfig, type User } from "./config.js";
|
|
33
|
-
import {
|
|
34
|
-
listServingEndpoints,
|
|
35
|
-
MASTRA_MODEL_OVERRIDE_KEY,
|
|
36
|
-
resolveModelId,
|
|
37
|
-
resolveServingConfig,
|
|
38
|
-
type ServingEndpointSummary,
|
|
39
|
-
} from "./serving.js";
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Capability tiers for Databricks Foundation Model API endpoints.
|
|
43
|
-
*
|
|
44
|
-
* - {@link ModelTier.Thinking}: deepest reasoning / "thinking" models
|
|
45
|
-
* (Claude Opus, GPT-5.5 Pro, Gemini Pro, Llama 4 Maverick, etc).
|
|
46
|
-
* Highest cost and latency; reserve for hard multi-step reasoning.
|
|
47
|
-
* - {@link ModelTier.Balanced}: cost/latency sweet spot for general
|
|
48
|
-
* agent work (Claude Sonnet, GPT-5.x, Gemini Flash, Llama 3.3 70B).
|
|
49
|
-
* The right default for most agents.
|
|
50
|
-
* - {@link ModelTier.Fast}: cheap and quick; classification, routing,
|
|
51
|
-
* tool-arg extraction, simple summarisation (Claude Haiku, GPT-5
|
|
52
|
-
* mini/nano, Gemini Flash Lite, GPT-OSS 20B, Llama 3.1 8B).
|
|
53
|
-
*
|
|
54
|
-
* String enum so the value is the slug we use in cache keys, logs,
|
|
55
|
-
* and as the value users see in serialized configs.
|
|
56
|
-
*/
|
|
57
|
-
export enum ModelTier {
|
|
58
|
-
Thinking = "thinking",
|
|
59
|
-
Balanced = "balanced",
|
|
60
|
-
Fast = "fast",
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Catalogue of Databricks-hosted Foundation Model API endpoints,
|
|
65
|
-
* grouped by capability {@link ModelTier} and then by provider. Each
|
|
66
|
-
* inner array is priority-ordered (most powerful first within the
|
|
67
|
-
* same provider+tier).
|
|
68
|
-
*
|
|
69
|
-
* Provider buckets:
|
|
70
|
-
*
|
|
71
|
-
* - `claude`: Anthropic Claude family (closed; flagship reasoning).
|
|
72
|
-
* - `gpt`: OpenAI GPT-5 family (closed; "ChatGPT" on Databricks FMAPI).
|
|
73
|
-
* - `gemini`: Google Gemini family (closed; multimodal + web-search).
|
|
74
|
-
* - `openSource`: open-weights models (widest regional / SKU availability).
|
|
75
|
-
*
|
|
76
|
-
* The list is curated by hand; refresh from the Databricks "supported
|
|
77
|
-
* foundation models" doc when new endpoints land.
|
|
78
|
-
*/
|
|
79
|
-
export const MODEL_CATALOG = {
|
|
80
|
-
[ModelTier.Thinking]: {
|
|
81
|
-
claude: [
|
|
82
|
-
"databricks-claude-opus-4-8",
|
|
83
|
-
"databricks-claude-opus-4-7",
|
|
84
|
-
"databricks-claude-opus-4-6",
|
|
85
|
-
"databricks-claude-opus-4-5",
|
|
86
|
-
"databricks-claude-opus-4-1",
|
|
87
|
-
],
|
|
88
|
-
gpt: ["databricks-gpt-5-5-pro"],
|
|
89
|
-
gemini: [
|
|
90
|
-
"databricks-gemini-3-1-pro",
|
|
91
|
-
"databricks-gemini-3-pro",
|
|
92
|
-
"databricks-gemini-2-5-pro",
|
|
93
|
-
],
|
|
94
|
-
openSource: [
|
|
95
|
-
"databricks-llama-4-maverick",
|
|
96
|
-
"databricks-gpt-oss-120b",
|
|
97
|
-
"databricks-meta-llama-3-1-405b-instruct",
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
[ModelTier.Balanced]: {
|
|
101
|
-
claude: [
|
|
102
|
-
"databricks-claude-sonnet-4-6",
|
|
103
|
-
"databricks-claude-sonnet-4-5",
|
|
104
|
-
"databricks-claude-sonnet-4",
|
|
105
|
-
],
|
|
106
|
-
gpt: [
|
|
107
|
-
"databricks-gpt-5-5",
|
|
108
|
-
"databricks-gpt-5-4",
|
|
109
|
-
"databricks-gpt-5-2",
|
|
110
|
-
"databricks-gpt-5-1",
|
|
111
|
-
"databricks-gpt-5",
|
|
112
|
-
],
|
|
113
|
-
gemini: [
|
|
114
|
-
"databricks-gemini-3-5-flash",
|
|
115
|
-
"databricks-gemini-3-flash",
|
|
116
|
-
"databricks-gemini-2-5-flash",
|
|
117
|
-
],
|
|
118
|
-
openSource: [
|
|
119
|
-
"databricks-meta-llama-3-3-70b-instruct",
|
|
120
|
-
"databricks-qwen3-next-80b-a3b-instruct",
|
|
121
|
-
"databricks-qwen35-122b-a10b",
|
|
122
|
-
],
|
|
123
|
-
},
|
|
124
|
-
[ModelTier.Fast]: {
|
|
125
|
-
claude: ["databricks-claude-haiku-4-5"],
|
|
126
|
-
gpt: [
|
|
127
|
-
"databricks-gpt-5-4-mini",
|
|
128
|
-
"databricks-gpt-5-4-nano",
|
|
129
|
-
"databricks-gpt-5-mini",
|
|
130
|
-
"databricks-gpt-5-nano",
|
|
131
|
-
],
|
|
132
|
-
gemini: ["databricks-gemini-3-1-flash-lite"],
|
|
133
|
-
openSource: [
|
|
134
|
-
"databricks-gpt-oss-20b",
|
|
135
|
-
"databricks-gemma-3-12b",
|
|
136
|
-
"databricks-meta-llama-3-1-8b-instruct",
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
} as const satisfies Record<ModelTier, Record<string, readonly string[]>>;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Round-robin zip: take one from each input list in order, skipping
|
|
143
|
-
* lists that have already been exhausted. Used to interleave provider
|
|
144
|
-
* buckets within a tier so the resolver alternates between vendors
|
|
145
|
-
* instead of draining one before trying the next.
|
|
146
|
-
*
|
|
147
|
-
* Example: `interleave(["a1","a2","a3"], ["b1","b2"])` ->
|
|
148
|
-
* `["a1","b1","a2","b2","a3"]`.
|
|
149
|
-
*/
|
|
150
|
-
function interleave<T>(...lists: readonly (readonly T[])[]): T[] {
|
|
151
|
-
const out: T[] = [];
|
|
152
|
-
const max = Math.max(0, ...lists.map((l) => l.length));
|
|
153
|
-
for (let i = 0; i < max; i++) {
|
|
154
|
-
for (const list of lists) {
|
|
155
|
-
if (i < list.length) out.push(list[i]!);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return out;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Priority-ordered model ids for a single capability {@link ModelTier},
|
|
163
|
-
* interleaved across providers so a workspace missing the top Claude
|
|
164
|
-
* still lands on a flagship GPT / Gemini on the next probe.
|
|
165
|
-
*
|
|
166
|
-
* Provider order within the interleave: Claude, GPT, Gemini, then the
|
|
167
|
-
* open-weights tail appended verbatim as the universal floor (widest
|
|
168
|
-
* regional availability).
|
|
169
|
-
*
|
|
170
|
-
* @example
|
|
171
|
-
* ```ts
|
|
172
|
-
* mastra({
|
|
173
|
-
* defaultModelFallbacks: modelsForTier(ModelTier.Fast),
|
|
174
|
-
* });
|
|
175
|
-
* ```
|
|
176
|
-
*/
|
|
177
|
-
export function modelsForTier(tier: ModelTier): readonly string[] {
|
|
178
|
-
const bucket = MODEL_CATALOG[tier];
|
|
179
|
-
return [
|
|
180
|
-
...interleave(bucket.claude, bucket.gpt, bucket.gemini),
|
|
181
|
-
...bucket.openSource,
|
|
182
|
-
];
|
|
183
|
-
}
|
|
30
|
+
import { MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig } from "./serving.js";
|
|
184
31
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
* @example
|
|
193
|
-
* ```ts
|
|
194
|
-
* const classifier = createAgent({
|
|
195
|
-
* instructions: "Classify this email",
|
|
196
|
-
* model: modelForTier(ModelTier.Fast), // cheap, quick
|
|
197
|
-
* });
|
|
198
|
-
*
|
|
199
|
-
* const planner = createAgent({
|
|
200
|
-
* instructions: "Plan a multi-step migration",
|
|
201
|
-
* model: modelForTier(ModelTier.Thinking), // deep reasoning
|
|
202
|
-
* });
|
|
203
|
-
* ```
|
|
204
|
-
*/
|
|
205
|
-
export function modelForTier(tier: ModelTier): string {
|
|
206
|
-
return modelsForTier(tier)[0]!;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Last-resort model ids used when neither `config.defaultModel`,
|
|
211
|
-
* per-agent `model`, nor `DATABRICKS_SERVING_ENDPOINT_NAME` is set.
|
|
212
|
-
*
|
|
213
|
-
* Walked in order at resolve time: the first id whose endpoint is
|
|
214
|
-
* actually present in the workspace's `/serving-endpoints` listing
|
|
215
|
-
* wins. Workspaces vary - not every region / SKU has every model,
|
|
216
|
-
* and the list of Foundation Model APIs evolves quickly - so the
|
|
217
|
-
* resolver degrades all the way from "best thinking model" down to
|
|
218
|
-
* "smallest commodity Llama" before giving up.
|
|
219
|
-
*
|
|
220
|
-
* Built by chaining the per-tier interleaves (Thinking -> Balanced
|
|
221
|
-
* -> Fast); within each tier the providers are round-robin-zipped
|
|
222
|
-
* (Claude, GPT, Gemini, then open-weights tail). Override the entire
|
|
223
|
-
* list via `MastraPluginConfig.defaultModelFallbacks` (e.g. to pin a
|
|
224
|
-
* regulated workspace to a specific approved subset, or to bias the
|
|
225
|
-
* priority toward a particular tier).
|
|
226
|
-
*/
|
|
227
|
-
export const FALLBACK_MODEL_IDS: readonly string[] = [
|
|
228
|
-
...modelsForTier(ModelTier.Thinking),
|
|
229
|
-
...modelsForTier(ModelTier.Balanced),
|
|
230
|
-
...modelsForTier(ModelTier.Fast),
|
|
231
|
-
];
|
|
32
|
+
export {
|
|
33
|
+
classifyEndpoints,
|
|
34
|
+
FALLBACK_MODEL_IDS,
|
|
35
|
+
modelForTier,
|
|
36
|
+
modelsForTier,
|
|
37
|
+
ModelTier,
|
|
38
|
+
} from "@dbx-tools/model";
|
|
232
39
|
|
|
233
40
|
/** Optional overrides accepted by {@link buildModel}. */
|
|
234
41
|
export interface BuildModelOverrides {
|
|
235
42
|
/**
|
|
236
43
|
* Static model id from the agent / plugin config (string sugar on
|
|
237
44
|
* `def.model` or `config.defaultModel`). Loses to the per-request
|
|
238
|
-
* override but wins over env / fallback.
|
|
45
|
+
* override but wins over env / tier / fallback.
|
|
239
46
|
*/
|
|
240
47
|
modelId?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Capability tier to resolve when no explicit model id is supplied.
|
|
50
|
+
* Used by internal agents (e.g. the chart planner asks for
|
|
51
|
+
* {@link ModelTier.Fast}) to express intent without pinning an
|
|
52
|
+
* endpoint name; the live catalogue is classified and the top
|
|
53
|
+
* available model in the tier is chosen, falling back to the
|
|
54
|
+
* tier's static list when the workspace has none.
|
|
55
|
+
*/
|
|
56
|
+
tier?: ModelTier;
|
|
241
57
|
}
|
|
242
58
|
|
|
243
59
|
/**
|
|
@@ -262,91 +78,41 @@ export async function buildModel(
|
|
|
262
78
|
// well-formed (`/serving-endpoints/chat/completions`).
|
|
263
79
|
const url = new URL("/serving-endpoints", host).toString().replace(/\/$/, "");
|
|
264
80
|
|
|
265
|
-
const modelId = await pickModelId(config, requestContext, overrides, user, host);
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
providerId: config.providerId ?? "databricks",
|
|
269
|
-
modelId,
|
|
270
|
-
url,
|
|
271
|
-
headers: Object.fromEntries(headers.entries()),
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Walk the resolution ladder and pick a modelId.
|
|
277
|
-
*
|
|
278
|
-
* 1. **Explicit ask** (per-request override, agent `model` string,
|
|
279
|
-
* `config.defaultModel` string, or `DATABRICKS_SERVING_ENDPOINT_NAME`):
|
|
280
|
-
* when fuzzy matching is on, snap the input to the closest live
|
|
281
|
-
* endpoint so loose names like `"claude sonnet"` resolve. When it's
|
|
282
|
-
* off (or no endpoint matches within threshold), the input is used
|
|
283
|
-
* verbatim and Databricks surfaces the canonical 404.
|
|
284
|
-
*
|
|
285
|
-
* 2. **No explicit ask**: walk
|
|
286
|
-
* {@link MastraPluginConfig.defaultModelFallbacks} (or
|
|
287
|
-
* {@link FALLBACK_MODEL_IDS} when unset) and return the first id
|
|
288
|
-
* whose endpoint is actually present in the workspace listing. A
|
|
289
|
-
* workspace without Claude Opus still gets a sensible default by
|
|
290
|
-
* skipping ahead to whichever Sonnet / GPT-5 / Llama variant is
|
|
291
|
-
* wired up.
|
|
292
|
-
*
|
|
293
|
-
* Catalogue fetches fail loud: network / auth errors propagate to the
|
|
294
|
-
* caller so they see the real SDK message instead of a silent fallback
|
|
295
|
-
* to the top of the priority list.
|
|
296
|
-
*/
|
|
297
|
-
async function pickModelId(
|
|
298
|
-
config: MastraPluginConfig,
|
|
299
|
-
requestContext: RequestContext,
|
|
300
|
-
overrides: BuildModelOverrides,
|
|
301
|
-
user: User,
|
|
302
|
-
host: string,
|
|
303
|
-
): Promise<string> {
|
|
304
81
|
const log = logUtils.logger(config);
|
|
305
|
-
const serving = resolveServingConfig(config
|
|
82
|
+
const serving = resolveServingConfig(config);
|
|
306
83
|
const override = serving.allowOverride
|
|
307
84
|
? (requestContext.get(MASTRA_MODEL_OVERRIDE_KEY) as string | undefined)
|
|
308
85
|
: undefined;
|
|
309
|
-
const explicit =
|
|
310
|
-
override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
|
|
311
86
|
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
87
|
+
// The override / agent default / env value can be either a concrete
|
|
88
|
+
// endpoint name or a capability tier slug ("thinking" / "balanced" /
|
|
89
|
+
// "fast"). A tier slug becomes a tier intent (let the live catalogue
|
|
90
|
+
// pick the best model in that band); anything else is an explicit
|
|
91
|
+
// name fuzzy-matched against the catalogue. An internal
|
|
92
|
+
// `overrides.tier` (e.g. the chart planner) is the floor when nothing
|
|
93
|
+
// was requested.
|
|
94
|
+
const requested =
|
|
95
|
+
override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
|
|
96
|
+
const requestedTier = requested !== undefined ? parseModelTier(requested) : null;
|
|
97
|
+
const explicit = requestedTier === null ? requested : undefined;
|
|
98
|
+
const tier = requestedTier ?? overrides.tier;
|
|
318
99
|
|
|
319
|
-
const
|
|
100
|
+
const { modelId, source } = await selectModel(user.executionContext.client, host, {
|
|
101
|
+
...(explicit !== undefined ? { explicit } : {}),
|
|
102
|
+
fuzzy: serving.fuzzy,
|
|
103
|
+
threshold: serving.threshold,
|
|
104
|
+
...(tier !== undefined ? { tier } : {}),
|
|
105
|
+
fallbacks: serving.fallbacks,
|
|
320
106
|
ttlMs: serving.ttlMs,
|
|
321
107
|
});
|
|
322
|
-
|
|
323
|
-
explicit !== undefined
|
|
324
|
-
? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
|
|
325
|
-
: pickFirstAvailable(serving.fallbacks, endpoints);
|
|
326
|
-
log.debug("model selected", {
|
|
327
|
-
modelId,
|
|
328
|
-
source: explicit !== undefined ? "fuzzy-match" : "fallback",
|
|
329
|
-
requestedExplicit: explicit,
|
|
330
|
-
});
|
|
331
|
-
return modelId;
|
|
332
|
-
}
|
|
108
|
+
log.debug("model selected", { modelId, source, requested });
|
|
333
109
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
*/
|
|
341
|
-
function pickFirstAvailable(
|
|
342
|
-
fallbacks: readonly string[],
|
|
343
|
-
endpoints: readonly ServingEndpointSummary[],
|
|
344
|
-
): string {
|
|
345
|
-
const present = new Set(endpoints.map((e) => e.name));
|
|
346
|
-
for (const candidate of fallbacks) {
|
|
347
|
-
if (present.has(candidate)) return candidate;
|
|
348
|
-
}
|
|
349
|
-
return fallbacks[0] ?? FALLBACK_MODEL_IDS[0]!;
|
|
110
|
+
return {
|
|
111
|
+
providerId: config.providerId ?? "databricks",
|
|
112
|
+
modelId,
|
|
113
|
+
url,
|
|
114
|
+
headers: Object.fromEntries(headers.entries()),
|
|
115
|
+
};
|
|
350
116
|
}
|
|
351
117
|
|
|
352
118
|
/** Path prefix that identifies a Databricks Model Serving REST call. */
|
package/src/plugin.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AppKit plugin that builds one or more Mastra `Agent` instances and
|
|
3
|
-
* mounts the `@mastra/express` server
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* mounts the `@mastra/express` server. Clients drive the conversation
|
|
4
|
+
* over the standard Mastra agent stream (`@mastra/client-js`'s
|
|
5
|
+
* `getAgent(id).stream()`), so there's no bespoke chat transport to
|
|
6
|
+
* keep in sync.
|
|
7
7
|
*
|
|
8
8
|
* - Agents: registered through `config.agents` at plugin creation
|
|
9
9
|
* ({@link MastraAgentDefinition}). Each entry's `tools` field accepts
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
* `schemaName: "mastra_<agentId>"`; the vector store is a single
|
|
22
22
|
* shared singleton across every agent.
|
|
23
23
|
* - Server: the Express subapp wiring lives in `./server.js`.
|
|
24
|
-
* - HTTP: AppKit mounts this plugin under `/api/mastra`.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
|
|
25
|
+
* Mastra agent routes, the plugin registers `/route/history`
|
|
26
|
+
* (load + clear thread history), `/models`, `/suggestions`, and the
|
|
27
|
+
* generic `/embed/:type/:id` resolver for inline chart / data
|
|
28
|
+
* markers.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
31
|
import {
|
|
@@ -37,30 +38,35 @@ import {
|
|
|
37
38
|
type PluginManifest,
|
|
38
39
|
type ResourceRequirement,
|
|
39
40
|
} from "@databricks/appkit";
|
|
40
|
-
import { appkitUtils, logUtils } from "@dbx-tools/shared";
|
|
41
|
-
import { chatRoute } from "@mastra/ai-sdk";
|
|
41
|
+
import { appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
|
|
42
42
|
import type { Agent } from "@mastra/core/agent";
|
|
43
43
|
import { Mastra } from "@mastra/core/mastra";
|
|
44
44
|
import express from "express";
|
|
45
|
+
import type { Pool } from "pg";
|
|
45
46
|
|
|
46
|
-
import
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
import {
|
|
48
|
+
MASTRA_ROUTES,
|
|
49
|
+
type MastraClientConfig,
|
|
50
|
+
type StatementData,
|
|
49
51
|
} from "@dbx-tools/appkit-mastra-shared";
|
|
52
|
+
import {
|
|
53
|
+
clearServingEndpointsCache,
|
|
54
|
+
listServingEndpoints,
|
|
55
|
+
type ServingEndpointSummary,
|
|
56
|
+
} from "@dbx-tools/model";
|
|
50
57
|
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.js";
|
|
51
58
|
import { fetchChart } from "./chart.js";
|
|
52
59
|
import type { MastraPluginConfig } from "./config.js";
|
|
53
60
|
import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.js";
|
|
54
61
|
import { historyRoute } from "./history.js";
|
|
55
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
createMemoryBuilder,
|
|
64
|
+
createServicePrincipalPool,
|
|
65
|
+
needsLakebase,
|
|
66
|
+
} from "./memory.js";
|
|
56
67
|
import { buildObservability } from "./observability.js";
|
|
57
68
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
58
|
-
import {
|
|
59
|
-
clearServingEndpointsCache,
|
|
60
|
-
listServingEndpoints,
|
|
61
|
-
resolveServingConfig,
|
|
62
|
-
type ServingEndpointSummary,
|
|
63
|
-
} from "./serving.js";
|
|
69
|
+
import { resolveServingConfig } from "./serving.js";
|
|
64
70
|
import {
|
|
65
71
|
fetchStatementData,
|
|
66
72
|
isStatementNotFoundError,
|
|
@@ -125,6 +131,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
125
131
|
private mastra: Mastra | null = null;
|
|
126
132
|
private mastraApp: express.Express | null = null;
|
|
127
133
|
private mastraServer: MastraServer | null = null;
|
|
134
|
+
/**
|
|
135
|
+
* Dedicated service-principal Lakebase pool backing Mastra memory /
|
|
136
|
+
* storage. Built once in {@link buildAgentAndServer} (outside any
|
|
137
|
+
* `asUser` scope, so it never inherits a request's OBO identity) and
|
|
138
|
+
* drained in {@link abortActiveOperations}. `null` until setup runs
|
|
139
|
+
* or when Lakebase isn't needed.
|
|
140
|
+
*/
|
|
141
|
+
private servicePrincipalPool: Pool | null = null;
|
|
128
142
|
|
|
129
143
|
override async setup(): Promise<void> {
|
|
130
144
|
// Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
|
|
@@ -150,6 +164,26 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
150
164
|
if (this.config.memory === undefined) this.config.memory = true;
|
|
151
165
|
}
|
|
152
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Drain the memory service-principal pool on shutdown. AppKit calls
|
|
169
|
+
* this during teardown; the lakebase plugin closes its own SP / OBO
|
|
170
|
+
* pools the same way. Fire-and-forget so shutdown isn't blocked on a
|
|
171
|
+
* slow drain, and clear the handle so a re-`setup()` rebuilds it.
|
|
172
|
+
*/
|
|
173
|
+
override abortActiveOperations(): void {
|
|
174
|
+
super.abortActiveOperations();
|
|
175
|
+
if (this.servicePrincipalPool) {
|
|
176
|
+
this.log.info("closing memory SP pool");
|
|
177
|
+
const pool = this.servicePrincipalPool;
|
|
178
|
+
this.servicePrincipalPool = null;
|
|
179
|
+
pool.end().catch((err) => {
|
|
180
|
+
this.log.error("error closing memory SP pool", {
|
|
181
|
+
error: commonUtils.errorMessage(err),
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
153
187
|
override exports() {
|
|
154
188
|
return {
|
|
155
189
|
/**
|
|
@@ -165,9 +199,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
165
199
|
*/
|
|
166
200
|
get: (id: string): Agent | null => this.built?.agents[id] ?? null,
|
|
167
201
|
/**
|
|
168
|
-
* The agent
|
|
169
|
-
*
|
|
170
|
-
*
|
|
202
|
+
* The agent the client converses with when it doesn't name one.
|
|
203
|
+
* Resolves to `config.defaultAgent`, the first registered id, or
|
|
204
|
+
* the built-in `default` fallback.
|
|
171
205
|
*/
|
|
172
206
|
getDefault: (): Agent | null =>
|
|
173
207
|
(this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
|
|
@@ -196,22 +230,16 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
196
230
|
|
|
197
231
|
override clientConfig(): Record<string, unknown> {
|
|
198
232
|
// AppKit mounts every plugin at `/api/<plugin.name>`. `this.name`
|
|
199
|
-
// honors `config.name` overrides, so
|
|
200
|
-
//
|
|
233
|
+
// honors `config.name` overrides, so publishing `basePath` is
|
|
234
|
+
// enough for the client to stay correct under a custom mount id -
|
|
235
|
+
// the per-route segments are fixed (`MASTRA_ROUTES`) and the
|
|
236
|
+
// client (`MastraPluginClient`) derives every endpoint from
|
|
237
|
+
// `basePath`.
|
|
201
238
|
// Return widens to `Record<string, unknown>` to satisfy the
|
|
202
239
|
// base-class signature; consumers read it through the typed
|
|
203
240
|
// `MastraClientConfig` shape via `usePluginClientConfig<...>(...)`.
|
|
204
|
-
const basePath = `/api/${this.name}`;
|
|
205
241
|
const config: MastraClientConfig = {
|
|
206
|
-
basePath
|
|
207
|
-
chatPath: `${basePath}/route/chat`,
|
|
208
|
-
chatPathTemplate: `${basePath}/route/chat/:agentId`,
|
|
209
|
-
modelsPath: `${basePath}/models`,
|
|
210
|
-
historyPath: `${basePath}/route/history`,
|
|
211
|
-
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
212
|
-
embedPathTemplate: `${basePath}/embed/:type/:id`,
|
|
213
|
-
suggestionsPath: `${basePath}/suggestions`,
|
|
214
|
-
suggestionsPathTemplate: `${basePath}/suggestions/:agentId`,
|
|
242
|
+
basePath: `/api/${this.name}`,
|
|
215
243
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
216
244
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
217
245
|
};
|
|
@@ -224,7 +252,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
224
252
|
// be registered before the catch-all that forwards everything to
|
|
225
253
|
// the Mastra subapp. Errors propagate to Express's default error
|
|
226
254
|
// handler via `next(err)` so callers see the real SDK message.
|
|
227
|
-
router.get(
|
|
255
|
+
router.get(MASTRA_ROUTES.models, (req, res, next) => {
|
|
228
256
|
this.userScopedSelf(req)
|
|
229
257
|
.listModels()
|
|
230
258
|
.then((endpoints) => res.json({ endpoints }))
|
|
@@ -278,7 +306,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
278
306
|
},
|
|
279
307
|
};
|
|
280
308
|
|
|
281
|
-
router.get(
|
|
309
|
+
router.get(`${MASTRA_ROUTES.embed}/:type/:id`, (req, res, next) => {
|
|
282
310
|
const type = req.params["type"] ?? "";
|
|
283
311
|
const id = req.params["id"];
|
|
284
312
|
const resolve = embedResolvers[type];
|
|
@@ -334,8 +362,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
334
362
|
res.json({ questions: [] });
|
|
335
363
|
});
|
|
336
364
|
};
|
|
337
|
-
router.get(
|
|
338
|
-
router.get(
|
|
365
|
+
router.get(MASTRA_ROUTES.suggestions, handleSuggestions);
|
|
366
|
+
router.get(`${MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
|
|
339
367
|
|
|
340
368
|
router.use((req, res, next) => {
|
|
341
369
|
if (!this.mastraApp) return res.status(503).end();
|
|
@@ -451,12 +479,25 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
451
479
|
}
|
|
452
480
|
|
|
453
481
|
private async buildAgentAndServer(): Promise<void> {
|
|
454
|
-
// Per-agent memory factory.
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
482
|
+
// Per-agent memory factory. When any storage / memory setting needs
|
|
483
|
+
// Postgres, stand up a dedicated service-principal pool first so
|
|
484
|
+
// memory acts as the app SP (owner of the `mastra_*` schemas),
|
|
485
|
+
// never the per-request OBO identity the chat turn runs under.
|
|
486
|
+
// `getPgConfig()` is read here, outside any `asUser` scope, so it
|
|
487
|
+
// returns the SP connection target + token refresh plus any
|
|
488
|
+
// `lakebase({ pool })` overrides; `require` turns a missing
|
|
489
|
+
// sibling into a clear wiring error. The builder caches the shared
|
|
490
|
+
// `PgVector` singleton so registering N agents stays cheap. See
|
|
491
|
+
// `./memory.js`.
|
|
492
|
+
if (needsLakebase(this.config)) {
|
|
493
|
+
const spPgConfig = appkitUtils
|
|
494
|
+
.require(this.context, lakebase, this.config)
|
|
495
|
+
.exports()
|
|
496
|
+
.getPgConfig();
|
|
497
|
+
this.servicePrincipalPool = await createServicePrincipalPool(spPgConfig);
|
|
498
|
+
}
|
|
499
|
+
const memoryBuilder = this.servicePrincipalPool
|
|
500
|
+
? createMemoryBuilder(this.config, this.servicePrincipalPool)
|
|
460
501
|
: undefined;
|
|
461
502
|
|
|
462
503
|
this.log.debug("build:start", {
|
|
@@ -507,20 +548,28 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
507
548
|
mastra: this.mastra,
|
|
508
549
|
prefix: "",
|
|
509
550
|
customApiRoutes: [
|
|
510
|
-
chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
|
|
511
|
-
chatRoute({ path: "/route/chat/:agentId" }),
|
|
512
551
|
// `historyRoute` registers both GET (load) and DELETE
|
|
513
552
|
// (clear) on the same path, so it returns an array we
|
|
514
553
|
// splice in.
|
|
515
|
-
...historyRoute({
|
|
516
|
-
|
|
554
|
+
...historyRoute({
|
|
555
|
+
path: MASTRA_ROUTES.history,
|
|
556
|
+
agent: this.built.defaultAgentId,
|
|
557
|
+
}),
|
|
558
|
+
// Assert the `:agentId` template type: the per-package build's
|
|
559
|
+
// NodeNext resolution widens the imported `MASTRA_ROUTES.history`
|
|
560
|
+
// to `string` (the source/bundler typecheck keeps it a literal),
|
|
561
|
+
// which would otherwise drop this out of the dynamic-agent
|
|
562
|
+
// overload and demand a fixed `agent`.
|
|
563
|
+
...historyRoute({
|
|
564
|
+
path: `${MASTRA_ROUTES.history}/:agentId` as `${string}:agentId`,
|
|
565
|
+
}),
|
|
517
566
|
],
|
|
518
567
|
});
|
|
519
568
|
await this.mastraServer.init();
|
|
520
569
|
this.log.debug("build:done", {
|
|
521
570
|
agents: Object.keys(this.built.agents),
|
|
522
571
|
defaultAgent: this.built.defaultAgentId,
|
|
523
|
-
routes: ["/route/
|
|
572
|
+
routes: ["/route/history", "/models", "/suggestions", "/embed/:type/:id"],
|
|
524
573
|
instanceStorage: instanceStorage !== undefined,
|
|
525
574
|
observability: observability !== undefined ? "mlflow" : "off",
|
|
526
575
|
});
|