@dbx-tools/appkit-mastra 0.1.41 → 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/model.d.ts +24 -131
- package/dist/src/model.js +39 -268
- package/dist/src/plugin.d.ts +13 -12
- package/dist/src/plugin.js +38 -34
- 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 +6 -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/model.ts +57 -291
- package/src/plugin.ts +45 -42
- 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/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 {
|
|
@@ -38,16 +39,21 @@ import {
|
|
|
38
39
|
type ResourceRequirement,
|
|
39
40
|
} from "@databricks/appkit";
|
|
40
41
|
import { appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
|
|
41
|
-
import { chatRoute } from "@mastra/ai-sdk";
|
|
42
42
|
import type { Agent } from "@mastra/core/agent";
|
|
43
43
|
import { Mastra } from "@mastra/core/mastra";
|
|
44
44
|
import express from "express";
|
|
45
45
|
import type { Pool } from "pg";
|
|
46
46
|
|
|
47
|
-
import
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
import {
|
|
48
|
+
MASTRA_ROUTES,
|
|
49
|
+
type MastraClientConfig,
|
|
50
|
+
type StatementData,
|
|
50
51
|
} from "@dbx-tools/appkit-mastra-shared";
|
|
52
|
+
import {
|
|
53
|
+
clearServingEndpointsCache,
|
|
54
|
+
listServingEndpoints,
|
|
55
|
+
type ServingEndpointSummary,
|
|
56
|
+
} from "@dbx-tools/model";
|
|
51
57
|
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.js";
|
|
52
58
|
import { fetchChart } from "./chart.js";
|
|
53
59
|
import type { MastraPluginConfig } from "./config.js";
|
|
@@ -60,12 +66,7 @@ import {
|
|
|
60
66
|
} from "./memory.js";
|
|
61
67
|
import { buildObservability } from "./observability.js";
|
|
62
68
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
63
|
-
import {
|
|
64
|
-
clearServingEndpointsCache,
|
|
65
|
-
listServingEndpoints,
|
|
66
|
-
resolveServingConfig,
|
|
67
|
-
type ServingEndpointSummary,
|
|
68
|
-
} from "./serving.js";
|
|
69
|
+
import { resolveServingConfig } from "./serving.js";
|
|
69
70
|
import {
|
|
70
71
|
fetchStatementData,
|
|
71
72
|
isStatementNotFoundError,
|
|
@@ -198,9 +199,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
198
199
|
*/
|
|
199
200
|
get: (id: string): Agent | null => this.built?.agents[id] ?? null,
|
|
200
201
|
/**
|
|
201
|
-
* The agent
|
|
202
|
-
*
|
|
203
|
-
*
|
|
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.
|
|
204
205
|
*/
|
|
205
206
|
getDefault: (): Agent | null =>
|
|
206
207
|
(this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
|
|
@@ -229,22 +230,16 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
229
230
|
|
|
230
231
|
override clientConfig(): Record<string, unknown> {
|
|
231
232
|
// AppKit mounts every plugin at `/api/<plugin.name>`. `this.name`
|
|
232
|
-
// honors `config.name` overrides, so
|
|
233
|
-
//
|
|
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`.
|
|
234
238
|
// Return widens to `Record<string, unknown>` to satisfy the
|
|
235
239
|
// base-class signature; consumers read it through the typed
|
|
236
240
|
// `MastraClientConfig` shape via `usePluginClientConfig<...>(...)`.
|
|
237
|
-
const basePath = `/api/${this.name}`;
|
|
238
241
|
const config: MastraClientConfig = {
|
|
239
|
-
basePath
|
|
240
|
-
chatPath: `${basePath}/route/chat`,
|
|
241
|
-
chatPathTemplate: `${basePath}/route/chat/:agentId`,
|
|
242
|
-
modelsPath: `${basePath}/models`,
|
|
243
|
-
historyPath: `${basePath}/route/history`,
|
|
244
|
-
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
245
|
-
embedPathTemplate: `${basePath}/embed/:type/:id`,
|
|
246
|
-
suggestionsPath: `${basePath}/suggestions`,
|
|
247
|
-
suggestionsPathTemplate: `${basePath}/suggestions/:agentId`,
|
|
242
|
+
basePath: `/api/${this.name}`,
|
|
248
243
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
249
244
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
250
245
|
};
|
|
@@ -257,7 +252,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
257
252
|
// be registered before the catch-all that forwards everything to
|
|
258
253
|
// the Mastra subapp. Errors propagate to Express's default error
|
|
259
254
|
// handler via `next(err)` so callers see the real SDK message.
|
|
260
|
-
router.get(
|
|
255
|
+
router.get(MASTRA_ROUTES.models, (req, res, next) => {
|
|
261
256
|
this.userScopedSelf(req)
|
|
262
257
|
.listModels()
|
|
263
258
|
.then((endpoints) => res.json({ endpoints }))
|
|
@@ -311,7 +306,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
311
306
|
},
|
|
312
307
|
};
|
|
313
308
|
|
|
314
|
-
router.get(
|
|
309
|
+
router.get(`${MASTRA_ROUTES.embed}/:type/:id`, (req, res, next) => {
|
|
315
310
|
const type = req.params["type"] ?? "";
|
|
316
311
|
const id = req.params["id"];
|
|
317
312
|
const resolve = embedResolvers[type];
|
|
@@ -367,8 +362,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
367
362
|
res.json({ questions: [] });
|
|
368
363
|
});
|
|
369
364
|
};
|
|
370
|
-
router.get(
|
|
371
|
-
router.get(
|
|
365
|
+
router.get(MASTRA_ROUTES.suggestions, handleSuggestions);
|
|
366
|
+
router.get(`${MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
|
|
372
367
|
|
|
373
368
|
router.use((req, res, next) => {
|
|
374
369
|
if (!this.mastraApp) return res.status(503).end();
|
|
@@ -553,20 +548,28 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
553
548
|
mastra: this.mastra,
|
|
554
549
|
prefix: "",
|
|
555
550
|
customApiRoutes: [
|
|
556
|
-
chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
|
|
557
|
-
chatRoute({ path: "/route/chat/:agentId" }),
|
|
558
551
|
// `historyRoute` registers both GET (load) and DELETE
|
|
559
552
|
// (clear) on the same path, so it returns an array we
|
|
560
553
|
// splice in.
|
|
561
|
-
...historyRoute({
|
|
562
|
-
|
|
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
|
+
}),
|
|
563
566
|
],
|
|
564
567
|
});
|
|
565
568
|
await this.mastraServer.init();
|
|
566
569
|
this.log.debug("build:done", {
|
|
567
570
|
agents: Object.keys(this.built.agents),
|
|
568
571
|
defaultAgent: this.built.defaultAgentId,
|
|
569
|
-
routes: ["/route/
|
|
572
|
+
routes: ["/route/history", "/models", "/suggestions", "/embed/:type/:id"],
|
|
570
573
|
instanceStorage: instanceStorage !== undefined,
|
|
571
574
|
observability: observability !== undefined ? "mlflow" : "off",
|
|
572
575
|
});
|
package/src/server.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Express-layer plumbing for the Mastra plugin: a `MastraServer` that
|
|
3
3
|
* stamps the per-request `RequestContext`, and a route-patch middleware
|
|
4
|
-
* that lets
|
|
5
|
-
* point.
|
|
4
|
+
* that lets the plugin's custom API routes (e.g. `historyRoute`) work
|
|
5
|
+
* behind an Express mount point.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getExecutionContext } from "@databricks/appkit";
|
|
@@ -172,30 +172,23 @@ export class MastraServer extends MastraServerExpress {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* Patches around `@mastra/express`'s custom-route dispatcher so
|
|
176
|
-
*
|
|
177
|
-
* mounted under a parent
|
|
175
|
+
* Patches around `@mastra/express`'s custom-route dispatcher so the
|
|
176
|
+
* plugin's custom API routes (e.g. `historyRoute`) work when
|
|
177
|
+
* `MastraServer` is hosted on an Express subapp mounted under a parent
|
|
178
|
+
* path (e.g. `/api/mastra`).
|
|
178
179
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* until we overwrite `originalUrl` for `/route` and `/route/*` to
|
|
187
|
-
* the mount-relative path.
|
|
188
|
-
*
|
|
189
|
-
* 2. `memory.resource` must be the authenticated user, not whatever the
|
|
190
|
-
* client posts. The custom-route forwarder re-serializes `req.body`
|
|
191
|
-
* into the Request body it hands Hono, so mutating the parsed body
|
|
192
|
-
* here would propagate into `handleChatStream`'s params (kept for
|
|
193
|
-
* future use; `express.json()` runs first so `req.body` is parsed).
|
|
180
|
+
* The adapter's `registerCustomApiRoutes` matches against `req.path`
|
|
181
|
+
* (mount-relative, correct) but dispatches to its internal Hono
|
|
182
|
+
* mini-app using `req.originalUrl`, which still contains the parent
|
|
183
|
+
* mount prefix. The Hono app registers the literal route paths
|
|
184
|
+
* (for example `/route/history`), so the absolute URL never matches
|
|
185
|
+
* until we overwrite `originalUrl` for `/route` and `/route/*` to the
|
|
186
|
+
* mount-relative path.
|
|
194
187
|
*/
|
|
195
188
|
export function attachRoutePatchMiddleware(app: express.Express): void {
|
|
196
189
|
app.use((req, _res, next) => {
|
|
197
|
-
const
|
|
198
|
-
if (!
|
|
190
|
+
const isCustomRoute = req.path === "/route" || req.path.startsWith("/route/");
|
|
191
|
+
if (!isCustomRoute) return next();
|
|
199
192
|
req.originalUrl = req.path;
|
|
200
193
|
next();
|
|
201
194
|
});
|
package/src/serving.ts
CHANGED
|
@@ -1,37 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Mastra-specific glue over the generic `@dbx-tools/model` toolkit.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
* search so tokens like `"claude sonnet"` snap to
|
|
11
|
-
* `databricks-claude-sonnet-4-6`, and a per-request override can be
|
|
12
|
-
* pulled from the request header, query string, or body so a model
|
|
13
|
-
* can be swapped per call without redeploying.
|
|
4
|
+
* The live `/serving-endpoints` catalogue access, fuzzy name
|
|
5
|
+
* resolution, and tier/fallback selection all live in
|
|
6
|
+
* `@dbx-tools/model`; this module only adds what is specific to the
|
|
7
|
+
* Mastra plugin: pulling a per-request model override off an HTTP
|
|
8
|
+
* request (header / query / body) and projecting the plugin config
|
|
9
|
+
* onto the knobs `buildModel` and the `/models` route share.
|
|
14
10
|
*/
|
|
15
11
|
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import Fuse from "fuse.js";
|
|
12
|
+
import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS } from "@dbx-tools/model";
|
|
13
|
+
import { stringUtils } from "@dbx-tools/shared";
|
|
19
14
|
|
|
20
|
-
import type { ServingEndpointSummary } from "@dbx-tools/appkit-mastra-shared";
|
|
21
15
|
import type { MastraPluginConfig } from "./config.js";
|
|
22
16
|
|
|
23
|
-
export type { ServingEndpointSummary };
|
|
24
|
-
|
|
25
|
-
const log = logUtils.logger("mastra/serving");
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Structural type for the Databricks workspace client. Derived from
|
|
29
|
-
* AppKit's `ExecutionContext` so this module doesn't take a direct
|
|
30
|
-
* dependency on `@databricks/sdk-experimental`; the dep flows in
|
|
31
|
-
* transitively through `@databricks/appkit`.
|
|
32
|
-
*/
|
|
33
|
-
type WorkspaceClientLike = ReturnType<typeof getExecutionContext>["client"];
|
|
34
|
-
|
|
35
17
|
/**
|
|
36
18
|
* `RequestContext` key under which {@link MastraServer} stores the
|
|
37
19
|
* per-request model override (header / query / body). `model.ts`
|
|
@@ -48,187 +30,6 @@ export const MODEL_OVERRIDE_QUERY = "model";
|
|
|
48
30
|
/** Body fields (in priority order) inspected for a per-request model override. */
|
|
49
31
|
export const MODEL_OVERRIDE_BODY_FIELDS = ["model", "modelId"] as const;
|
|
50
32
|
|
|
51
|
-
/** Default TTL for the in-memory endpoint cache. Matches the Databricks SDK's session lifetime budget. */
|
|
52
|
-
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
53
|
-
|
|
54
|
-
/** Default Fuse.js score threshold below which a fuzzy match is accepted. */
|
|
55
|
-
const DEFAULT_FUZZY_THRESHOLD = 0.4;
|
|
56
|
-
|
|
57
|
-
/** Cache key parts under which endpoint listings are stored. */
|
|
58
|
-
const CACHE_KEY_NAMESPACE = "mastra:serving-endpoints";
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Stable `userKey` arg for AppKit's `CacheManager.getOrExecute`.
|
|
62
|
-
* Endpoint visibility is effectively workspace-scoped (we cache by
|
|
63
|
-
* host in the key parts), so a single shared key lets every user of
|
|
64
|
-
* the same workspace share one cached fetch and coalesce on the
|
|
65
|
-
* in-flight promise. Permissions can differ in theory, but the
|
|
66
|
-
* Foundation Model API catalogue is the same view for every caller.
|
|
67
|
-
*/
|
|
68
|
-
const SHARED_USER_KEY = "mastra-shared";
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* List Model Serving endpoints for the workspace owning `client`,
|
|
72
|
-
* routed through AppKit's `CacheManager`. The manager gives us
|
|
73
|
-
* everything `cachetools.TTLCache` provides plus what
|
|
74
|
-
* `cachetools-async` adds on top: per-entry TTL, in-flight request
|
|
75
|
-
* coalescing (concurrent callers share one fetch via the manager's
|
|
76
|
-
* internal `inFlightRequests` map), bounded size, telemetry spans
|
|
77
|
-
* (`cache.getOrExecute`), and optional Lakebase persistence so the
|
|
78
|
-
* catalogue survives restarts when the lakebase plugin is wired up.
|
|
79
|
-
*
|
|
80
|
-
* Returns plain {@link ServingEndpointSummary} objects (a stable
|
|
81
|
-
* subset of the SDK type) so cache hits never expose stale SDK
|
|
82
|
-
* internals. Errors from `CacheManager` or the SDK fetch propagate
|
|
83
|
-
* to the caller - we don't swallow them so users see the real
|
|
84
|
-
* auth / network issue.
|
|
85
|
-
*
|
|
86
|
-
* @param host - Workspace host used as the cache key. Pass the value
|
|
87
|
-
* resolved from `client.config.getHost()` so multi-host apps share
|
|
88
|
-
* one entry per workspace.
|
|
89
|
-
* @param opts.ttlMs - Override the default TTL just for this call.
|
|
90
|
-
* Forwarded to `CacheManager` as seconds.
|
|
91
|
-
*/
|
|
92
|
-
export async function listServingEndpoints(
|
|
93
|
-
client: WorkspaceClientLike,
|
|
94
|
-
host: string,
|
|
95
|
-
opts: { ttlMs?: number } = {},
|
|
96
|
-
): Promise<ServingEndpointSummary[]> {
|
|
97
|
-
const ttlSec = Math.max(1, Math.round((opts.ttlMs ?? DEFAULT_TTL_MS) / 1000));
|
|
98
|
-
return CacheManager.getInstanceSync().getOrExecute(
|
|
99
|
-
[CACHE_KEY_NAMESPACE, host],
|
|
100
|
-
() => fetchEndpoints(client),
|
|
101
|
-
SHARED_USER_KEY,
|
|
102
|
-
{ ttl: ttlSec },
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function fetchEndpoints(
|
|
107
|
-
client: WorkspaceClientLike,
|
|
108
|
-
): Promise<ServingEndpointSummary[]> {
|
|
109
|
-
const startedAt = Date.now();
|
|
110
|
-
const out: ServingEndpointSummary[] = [];
|
|
111
|
-
for await (const ep of client.servingEndpoints.list()) {
|
|
112
|
-
if (!ep.name) continue;
|
|
113
|
-
out.push({
|
|
114
|
-
name: ep.name,
|
|
115
|
-
...(ep.task !== undefined ? { task: ep.task } : {}),
|
|
116
|
-
...(ep.state?.ready !== undefined ? { state: String(ep.state.ready) } : {}),
|
|
117
|
-
...(ep.description !== undefined ? { description: ep.description } : {}),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
log.debug("listed", { count: out.length, elapsedMs: Date.now() - startedAt });
|
|
121
|
-
return out;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Force-evict cached endpoint listings via AppKit's `CacheManager`.
|
|
126
|
-
* With a `host` deletes that one workspace's entry; without one
|
|
127
|
-
* clears every cache entry on the manager (since `CacheManager`
|
|
128
|
-
* doesn't expose a namespace-scoped clear, this is the brute-force
|
|
129
|
-
* path - fine for tests, avoid in steady-state code).
|
|
130
|
-
*/
|
|
131
|
-
export async function clearServingEndpointsCache(host?: string): Promise<void> {
|
|
132
|
-
const cache = CacheManager.getInstanceSync();
|
|
133
|
-
if (host) {
|
|
134
|
-
const key = cache.generateKey([CACHE_KEY_NAMESPACE, host], SHARED_USER_KEY);
|
|
135
|
-
await cache.delete(key);
|
|
136
|
-
} else {
|
|
137
|
-
await cache.clear();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Result of fuzzy-resolving a user-supplied model name against the
|
|
143
|
-
* live endpoint list. `score` is Fuse.js's distance (`0` is exact,
|
|
144
|
-
* `1` is no match); `matched` is `false` when the score exceeds the
|
|
145
|
-
* configured threshold so callers can fall back to the original
|
|
146
|
-
* input (Databricks will then return a clean 404).
|
|
147
|
-
*/
|
|
148
|
-
export interface ResolvedModel {
|
|
149
|
-
modelId: string;
|
|
150
|
-
matched: boolean;
|
|
151
|
-
score?: number;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Options accepted by {@link resolveModelId}. */
|
|
155
|
-
export interface ResolveModelOptions {
|
|
156
|
-
/** Fuse.js threshold (0 = exact, 1 = anything). Default `0.4`. */
|
|
157
|
-
threshold?: number;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Snap a user-supplied model name to the closest configured serving
|
|
162
|
-
* endpoint:
|
|
163
|
-
*
|
|
164
|
-
* 1. Exact name match wins immediately (no fuzzy needed).
|
|
165
|
-
* 2. Otherwise the input is tokenized (dashes / underscores / spaces
|
|
166
|
-
* become separators) and fed through Fuse.js extended search,
|
|
167
|
-
* which AND-s each token with fuzzy matching enabled. This is the
|
|
168
|
-
* "tokenized fuzzy match" the user reaches for when they type
|
|
169
|
-
* `"claude sonnet"` instead of the full endpoint name.
|
|
170
|
-
* 3. If the best Fuse score is above `threshold`, return the input
|
|
171
|
-
* unchanged and let the upstream call surface the 404. This keeps
|
|
172
|
-
* deliberate model ids (e.g. brand new endpoints) from being
|
|
173
|
-
* silently rewritten to a similar-looking neighbour.
|
|
174
|
-
*
|
|
175
|
-
* Pass an empty endpoint list to short-circuit fuzzy matching - the
|
|
176
|
-
* input is returned verbatim. This is what {@link buildModel} does
|
|
177
|
-
* when the workspace client can't be reached at resolve time.
|
|
178
|
-
*/
|
|
179
|
-
export function resolveModelId(
|
|
180
|
-
input: string,
|
|
181
|
-
endpoints: readonly ServingEndpointSummary[],
|
|
182
|
-
opts: ResolveModelOptions = {},
|
|
183
|
-
): ResolvedModel {
|
|
184
|
-
if (endpoints.length === 0) {
|
|
185
|
-
log.debug("resolve:no-endpoints", { input });
|
|
186
|
-
return { modelId: input, matched: false };
|
|
187
|
-
}
|
|
188
|
-
for (const ep of endpoints) {
|
|
189
|
-
if (ep.name === input) {
|
|
190
|
-
log.debug("resolve:exact", { input });
|
|
191
|
-
return { modelId: ep.name, matched: true, score: 0 };
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const threshold = opts.threshold ?? DEFAULT_FUZZY_THRESHOLD;
|
|
195
|
-
const fuse = new Fuse(endpoints, {
|
|
196
|
-
keys: ["name"],
|
|
197
|
-
threshold,
|
|
198
|
-
ignoreLocation: true,
|
|
199
|
-
includeScore: true,
|
|
200
|
-
useExtendedSearch: true,
|
|
201
|
-
isCaseSensitive: false,
|
|
202
|
-
});
|
|
203
|
-
// Fuse 7.3 has no built-in tokenize hook; in extended search,
|
|
204
|
-
// space-separated tokens are AND-ed with fuzzy matching enabled. We
|
|
205
|
-
// lean on the shared tokenizer so the splitting rules stay
|
|
206
|
-
// consistent with the rest of the toolkit.
|
|
207
|
-
const query = Array.from(
|
|
208
|
-
stringUtils.tokenizeWithOptions({ lowerCase: true, camelCase: false }, input),
|
|
209
|
-
).join(" ");
|
|
210
|
-
if (!query) {
|
|
211
|
-
log.debug("resolve:empty-tokens", { input });
|
|
212
|
-
return { modelId: input, matched: false };
|
|
213
|
-
}
|
|
214
|
-
const results = fuse.search(query);
|
|
215
|
-
const best = results[0];
|
|
216
|
-
if (best?.item.name && (best.score ?? 0) <= threshold) {
|
|
217
|
-
log.debug("resolve:fuzzy-match", {
|
|
218
|
-
input,
|
|
219
|
-
modelId: best.item.name,
|
|
220
|
-
score: best.score,
|
|
221
|
-
});
|
|
222
|
-
return { modelId: best.item.name, matched: true, score: best.score };
|
|
223
|
-
}
|
|
224
|
-
log.debug("resolve:no-match", {
|
|
225
|
-
input,
|
|
226
|
-
bestScore: best?.score,
|
|
227
|
-
threshold,
|
|
228
|
-
});
|
|
229
|
-
return { modelId: input, matched: false };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
33
|
/**
|
|
233
34
|
* Minimal Express-ish request shape used by {@link extractModelOverride}.
|
|
234
35
|
* Keeps this module independent of `express` so the helper can be
|
|
@@ -278,18 +79,15 @@ export function extractModelOverride(req: ModelOverrideRequest): string | null {
|
|
|
278
79
|
|
|
279
80
|
/**
|
|
280
81
|
* Read the fuzzy-resolution config knobs off the plugin config with
|
|
281
|
-
* defaults applied. Kept here so `buildModel` and
|
|
282
|
-
* agree on what "enabled" means.
|
|
82
|
+
* `@dbx-tools/model` defaults applied. Kept here so `buildModel` and
|
|
83
|
+
* the `/models` route agree on what "enabled" means.
|
|
283
84
|
*
|
|
284
|
-
* `fallbacks` is the priority-ordered list `
|
|
285
|
-
* nothing explicit is set; defaults
|
|
286
|
-
*
|
|
287
|
-
*
|
|
85
|
+
* `fallbacks` is the priority-ordered list `resolveModel` walks first
|
|
86
|
+
* when nothing explicit is set; defaults to an empty list so the
|
|
87
|
+
* generic resolver falls through to the live catalogue and its own
|
|
88
|
+
* `FALLBACK_MODEL_IDS` floor.
|
|
288
89
|
*/
|
|
289
|
-
export function resolveServingConfig(
|
|
290
|
-
config: MastraPluginConfig,
|
|
291
|
-
defaultFallbacks: readonly string[] = [],
|
|
292
|
-
): {
|
|
90
|
+
export function resolveServingConfig(config: MastraPluginConfig): {
|
|
293
91
|
ttlMs: number;
|
|
294
92
|
threshold: number;
|
|
295
93
|
fuzzy: boolean;
|
|
@@ -297,10 +95,10 @@ export function resolveServingConfig(
|
|
|
297
95
|
fallbacks: readonly string[];
|
|
298
96
|
} {
|
|
299
97
|
return {
|
|
300
|
-
ttlMs: config.modelCacheTtlMs ??
|
|
98
|
+
ttlMs: config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS,
|
|
301
99
|
threshold: config.modelFuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD,
|
|
302
100
|
fuzzy: config.modelFuzzyMatch !== false,
|
|
303
101
|
allowOverride: config.modelOverride !== false,
|
|
304
|
-
fallbacks: config.defaultModelFallbacks ??
|
|
102
|
+
fallbacks: config.defaultModelFallbacks ?? [],
|
|
305
103
|
};
|
|
306
104
|
}
|
package/src/writer.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* away can't crash a tool mid-flight.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type {
|
|
13
|
+
import type { MastraWriter } from "@dbx-tools/appkit-mastra-shared";
|
|
14
14
|
import { commonUtils, type logUtils } from "@dbx-tools/shared";
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -24,7 +24,7 @@ import { commonUtils, type logUtils } from "@dbx-tools/shared";
|
|
|
24
24
|
*/
|
|
25
25
|
export async function safeWrite(
|
|
26
26
|
log: logUtils.Logger,
|
|
27
|
-
writer:
|
|
27
|
+
writer: MastraWriter | undefined,
|
|
28
28
|
chunk: unknown,
|
|
29
29
|
context: Record<string, unknown> = {},
|
|
30
30
|
): Promise<void> {
|
package/dist/src/intercept.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-side Server-Sent-Events frame interceptor for the native
|
|
3
|
-
* Mastra agent `/stream` endpoint.
|
|
4
|
-
*
|
|
5
|
-
* The Mastra stream (`@mastra/express`) emits one `data: <json>\n\n`
|
|
6
|
-
* SSE frame per chunk, where the decoded JSON carries a
|
|
7
|
-
* discriminating `type` (`text-delta`, `reasoning-delta`,
|
|
8
|
-
* `tool-call`, `step-finish`, ...) and is terminated by a
|
|
9
|
-
* `data: [DONE]` sentinel. {@link installStreamEventInterceptor} wraps
|
|
10
|
-
* an Express response and runs a caller-supplied
|
|
11
|
-
* {@link StreamFrameInterceptor} over each decoded frame so the caller
|
|
12
|
-
* can keep, rewrite, or drop it; every non-`data:` byte (SSE
|
|
13
|
-
* keep-alive comments, the `[DONE]` sentinel, blank padding) passes
|
|
14
|
-
* through verbatim.
|
|
15
|
-
*
|
|
16
|
-
* The wrap is inert unless the response turns out to be a streamed
|
|
17
|
-
* `200 text/event-stream` body, so non-streaming JSON routes and
|
|
18
|
-
* error responses are never touched.
|
|
19
|
-
*/
|
|
20
|
-
import type express from "express";
|
|
21
|
-
/**
|
|
22
|
-
* Per-frame interceptor invoked with the `JSON.parse`d object of every
|
|
23
|
-
* `data: {json}` frame on the Mastra `/stream` response. The return
|
|
24
|
-
* value decides the frame's fate:
|
|
25
|
-
*
|
|
26
|
-
* - `true`: forward the frame's original bytes unchanged (no
|
|
27
|
-
* re-serialization).
|
|
28
|
-
* - `false`: drop the frame entirely.
|
|
29
|
-
* - `{ replace }`: re-emit the frame with the original `data:` prefix
|
|
30
|
-
* and trailing whitespace preserved and only the JSON body replaced
|
|
31
|
-
* by `JSON.stringify(replace)`.
|
|
32
|
-
*/
|
|
33
|
-
export type StreamFrameInterceptor = (chunk: unknown) => {
|
|
34
|
-
replace: unknown;
|
|
35
|
-
} | true | false;
|
|
36
|
-
/**
|
|
37
|
-
* Wrap `res.write` / `res.end` so each SSE frame on a streamed Mastra
|
|
38
|
-
* response is run through `interceptor` (keep / rewrite / drop).
|
|
39
|
-
*
|
|
40
|
-
* Interception only engages once the response is confirmed to be a
|
|
41
|
-
* `200 text/event-stream` body (decided lazily on the first write,
|
|
42
|
-
* after the handler has set status + content-type); any other
|
|
43
|
-
* response streams through unchanged. A {@link StringDecoder}
|
|
44
|
-
* reassembles multi-byte UTF-8 sequences that straddle chunk
|
|
45
|
-
* boundaries, and a running buffer holds the trailing partial frame
|
|
46
|
-
* until its `\n\n` arrives.
|
|
47
|
-
*/
|
|
48
|
-
export declare function installStreamEventInterceptor(res: express.Response, interceptor: StreamFrameInterceptor): void;
|