@dbx-tools/appkit-mastra 0.1.25 → 0.1.26
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 +107 -297
- package/dist/src/agents.d.ts +10 -0
- package/dist/src/agents.js +14 -1
- package/dist/src/chart.d.ts +131 -79
- package/dist/src/chart.js +388 -279
- package/dist/src/config.d.ts +18 -15
- package/dist/src/genie.d.ts +90 -106
- package/dist/src/genie.js +508 -642
- package/dist/src/plugin.d.ts +12 -0
- package/dist/src/plugin.js +168 -0
- package/dist/src/processors/strip-stale-charts.d.ts +16 -13
- package/dist/src/processors/strip-stale-charts.js +16 -13
- package/dist/src/statement.d.ts +73 -0
- package/dist/src/statement.js +118 -0
- package/dist/src/tools/email.js +27 -28
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/agents.ts +15 -1
- package/src/chart.ts +501 -319
- package/src/config.ts +18 -15
- package/src/genie.ts +545 -768
- package/src/plugin.ts +178 -1
- package/src/processors/strip-stale-charts.ts +16 -13
- package/src/statement.ts +127 -0
- package/src/tools/email.ts +27 -28
package/dist/src/plugin.d.ts
CHANGED
|
@@ -109,6 +109,18 @@ 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 `/statements/:statementId` route.
|
|
114
|
+
* Runs inside the AppKit user-context proxy so
|
|
115
|
+
* `getExecutionContext()` returns the OBO-scoped workspace
|
|
116
|
+
* client, then reuses the same `fetchStatementData` pipeline
|
|
117
|
+
* the `get_statement` tool runs so the LLM and the UI see the
|
|
118
|
+
* exact same shape for the same statement.
|
|
119
|
+
*
|
|
120
|
+
* Returns `undefined` for upstream 404s so the route can map
|
|
121
|
+
* them to a clean HTTP 404; any other failure bubbles up.
|
|
122
|
+
*/
|
|
123
|
+
private fetchStatement;
|
|
112
124
|
/**
|
|
113
125
|
* Return `this.asUser(req)` when the request carries an OBO token,
|
|
114
126
|
* otherwise return `this` directly. Prevents the noisy AppKit warn
|
package/dist/src/plugin.js
CHANGED
|
@@ -32,11 +32,13 @@ import { chatRoute } from "@mastra/ai-sdk";
|
|
|
32
32
|
import { Mastra } from "@mastra/core/mastra";
|
|
33
33
|
import express from "express";
|
|
34
34
|
import { buildAgents, FALLBACK_AGENT_ID } from "./agents.js";
|
|
35
|
+
import { fetchChart } from "./chart.js";
|
|
35
36
|
import { historyRoute } from "./history.js";
|
|
36
37
|
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
37
38
|
import { buildObservability } from "./observability.js";
|
|
38
39
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
39
40
|
import { clearServingEndpointsCache, listServingEndpoints, resolveServingConfig, } from "./serving.js";
|
|
41
|
+
import { fetchStatementData, isStatementNotFoundError, STATEMENT_ROW_CAP, } from "./statement.js";
|
|
40
42
|
const GENIE_MANIFEST = appkitUtils.data(genie).plugin.manifest;
|
|
41
43
|
const LAKEBASE_MANIFEST = appkitUtils.data(lakebase).plugin.manifest;
|
|
42
44
|
/**
|
|
@@ -171,6 +173,8 @@ export class MastraPlugin extends Plugin {
|
|
|
171
173
|
modelsPath: `${basePath}/models`,
|
|
172
174
|
historyPath: `${basePath}/route/history`,
|
|
173
175
|
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
176
|
+
chartsPathTemplate: `${basePath}/charts/:chartId`,
|
|
177
|
+
statementsPathTemplate: `${basePath}/statements/:statementId`,
|
|
174
178
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
175
179
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
176
180
|
};
|
|
@@ -188,12 +192,143 @@ export class MastraPlugin extends Plugin {
|
|
|
188
192
|
.then((endpoints) => res.json({ endpoints }))
|
|
189
193
|
.catch(next);
|
|
190
194
|
});
|
|
195
|
+
// `GET /charts/:chartId` long-polls the chart cache.
|
|
196
|
+
//
|
|
197
|
+
// Charts kicked off by the Genie agent's `prepare_chart` tool
|
|
198
|
+
// (or the system `render_data` tool) resolve asynchronously
|
|
199
|
+
// and land in the AppKit cache under a chartId. The host UI
|
|
200
|
+
// resolves `[chart:<chartId>]` markers by hitting this route,
|
|
201
|
+
// which blocks until the entry settles (`result` or `error`
|
|
202
|
+
// set) or the server-side budget elapses (then returns the
|
|
203
|
+
// last seen still-processing entry so the client can poll
|
|
204
|
+
// again).
|
|
205
|
+
//
|
|
206
|
+
// Status codes:
|
|
207
|
+
// - 200 with JSON body when the entry is found (any state).
|
|
208
|
+
// - 404 when the chartId doesn't exist or its 1h TTL elapsed.
|
|
209
|
+
//
|
|
210
|
+
// Query params:
|
|
211
|
+
// - `timeoutMs` (optional, default 60000): how long to long-poll
|
|
212
|
+
// before returning a still-processing entry.
|
|
213
|
+
//
|
|
214
|
+
// Wired with `req.signal` so a closed connection unblocks the
|
|
215
|
+
// poll immediately and the request thread frees up. Cache
|
|
216
|
+
// failures bubble through `next(err)` to Express's default
|
|
217
|
+
// error handler.
|
|
218
|
+
router.get("/charts/:chartId", (req, res, next) => {
|
|
219
|
+
const chartId = req.params["chartId"];
|
|
220
|
+
if (!chartId) {
|
|
221
|
+
res.status(400).json({ error: "chartId is required" });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const timeoutMs = parseTimeoutMs(req.query["timeoutMs"]);
|
|
225
|
+
// Express's `req` predates `AbortSignal`. Bridge the
|
|
226
|
+
// `close` event onto an `AbortController` so a closed
|
|
227
|
+
// connection unblocks the long-poll immediately and frees
|
|
228
|
+
// the request thread. The `close` listener also fires on
|
|
229
|
+
// a normal completion, but at that point we no longer
|
|
230
|
+
// care about the abort and the listener is GC'd with the
|
|
231
|
+
// request.
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
req.on("close", () => controller.abort());
|
|
234
|
+
fetchChart(chartId, {
|
|
235
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
236
|
+
signal: controller.signal,
|
|
237
|
+
})
|
|
238
|
+
.then((entry) => {
|
|
239
|
+
if (!entry) {
|
|
240
|
+
res.status(404).json({ error: "chart not found" });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
res.json(entry);
|
|
244
|
+
})
|
|
245
|
+
.catch(next);
|
|
246
|
+
});
|
|
247
|
+
// `GET /statements/:statementId` resolves a Genie / Statement
|
|
248
|
+
// Execution result for inline rendering as a table.
|
|
249
|
+
//
|
|
250
|
+
// The agent embeds `[data:<statement_id>]` markers in prose
|
|
251
|
+
// (see `GENIE_INSTRUCTIONS`) whenever it wants the host UI to
|
|
252
|
+
// display a result set without the model re-spelling every
|
|
253
|
+
// row in markdown. This route is what those markers resolve
|
|
254
|
+
// against - one OBO-scoped fetch returns the columns + rows
|
|
255
|
+
// so the client renders an AppKit Table inline.
|
|
256
|
+
//
|
|
257
|
+
// Status codes:
|
|
258
|
+
// - 200 with `StatementData` body when the statement is found.
|
|
259
|
+
// - 404 when the workspace can't find / read the statement.
|
|
260
|
+
//
|
|
261
|
+
// Query params:
|
|
262
|
+
// - `limit` (optional): row cap. Clamped to
|
|
263
|
+
// {@link STATEMENT_ROW_CAP} so a runaway result set
|
|
264
|
+
// can't hose the response.
|
|
265
|
+
//
|
|
266
|
+
// OBO-scoped via {@link userScopedSelf} so the caller's
|
|
267
|
+
// workspace permissions apply; SDK errors propagate through
|
|
268
|
+
// `next(err)` to Express's default error handler.
|
|
269
|
+
router.get("/statements/:statementId", (req, res, next) => {
|
|
270
|
+
const statementId = req.params["statementId"];
|
|
271
|
+
if (!statementId) {
|
|
272
|
+
res.status(400).json({ error: "statementId is required" });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const limit = parseStatementLimit(req.query["limit"]);
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
req.on("close", () => controller.abort());
|
|
278
|
+
this.userScopedSelf(req)
|
|
279
|
+
.fetchStatement(statementId, {
|
|
280
|
+
...(limit !== undefined ? { limit } : {}),
|
|
281
|
+
signal: controller.signal,
|
|
282
|
+
})
|
|
283
|
+
.then((data) => {
|
|
284
|
+
if (!data) {
|
|
285
|
+
res.status(404).json({ error: "statement not found" });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
res.json(data);
|
|
289
|
+
})
|
|
290
|
+
.catch(next);
|
|
291
|
+
});
|
|
191
292
|
router.use("", (req, res, next) => {
|
|
192
293
|
if (!this.mastraApp)
|
|
193
294
|
return res.status(503).end();
|
|
194
295
|
return this.userScopedSelf(req).mastraApp(req, res, next);
|
|
195
296
|
});
|
|
196
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Implementation backing the `/statements/:statementId` route.
|
|
300
|
+
* Runs inside the AppKit user-context proxy so
|
|
301
|
+
* `getExecutionContext()` returns the OBO-scoped workspace
|
|
302
|
+
* client, then reuses the same `fetchStatementData` pipeline
|
|
303
|
+
* the `get_statement` tool runs so the LLM and the UI see the
|
|
304
|
+
* exact same shape for the same statement.
|
|
305
|
+
*
|
|
306
|
+
* Returns `undefined` for upstream 404s so the route can map
|
|
307
|
+
* them to a clean HTTP 404; any other failure bubbles up.
|
|
308
|
+
*/
|
|
309
|
+
async fetchStatement(statementId, options = {}) {
|
|
310
|
+
const client = getExecutionContext().client;
|
|
311
|
+
const limit = Math.min(options.limit ?? STATEMENT_ROW_CAP, STATEMENT_ROW_CAP);
|
|
312
|
+
try {
|
|
313
|
+
const data = await fetchStatementData(client, statementId, {
|
|
314
|
+
limit,
|
|
315
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
columns: data.columns,
|
|
319
|
+
rows: data.rows,
|
|
320
|
+
rowCount: data.rowCount,
|
|
321
|
+
truncated: data.rows.length < data.rowCount,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
// The Databricks SDK throws on 404; surface as `undefined`
|
|
326
|
+
// so the route maps to a clean HTTP 404 instead of a 500.
|
|
327
|
+
if (isStatementNotFoundError(err))
|
|
328
|
+
return undefined;
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
197
332
|
/**
|
|
198
333
|
* Return `this.asUser(req)` when the request carries an OBO token,
|
|
199
334
|
* otherwise return `this` directly. Prevents the noisy AppKit warn
|
|
@@ -290,4 +425,37 @@ export class MastraPlugin extends Plugin {
|
|
|
290
425
|
});
|
|
291
426
|
}
|
|
292
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Parse the optional `?timeoutMs=<n>` query parameter from a
|
|
430
|
+
* `GET /charts/:chartId` request. Accepts a positive integer up
|
|
431
|
+
* to 5 minutes (clamped) and rejects everything else as
|
|
432
|
+
* `undefined` so {@link fetchChart} falls back to its default.
|
|
433
|
+
* Express produces `string | string[] | undefined`; we normalize
|
|
434
|
+
* to the first scalar before parsing.
|
|
435
|
+
*/
|
|
436
|
+
function parseTimeoutMs(raw) {
|
|
437
|
+
const v = Array.isArray(raw) ? raw[0] : raw;
|
|
438
|
+
if (typeof v !== "string")
|
|
439
|
+
return undefined;
|
|
440
|
+
const n = Number(v);
|
|
441
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
442
|
+
return undefined;
|
|
443
|
+
return Math.min(Math.floor(n), 5 * 60_000);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Parse the optional `?limit=<n>` query parameter from a
|
|
447
|
+
* `GET /statements/:statementId` request. Accepts a non-negative
|
|
448
|
+
* integer and lets the route clamp to `STATEMENT_ROW_CAP`;
|
|
449
|
+
* rejects anything else as `undefined` so the route falls back
|
|
450
|
+
* to the server-side cap.
|
|
451
|
+
*/
|
|
452
|
+
function parseStatementLimit(raw) {
|
|
453
|
+
const v = Array.isArray(raw) ? raw[0] : raw;
|
|
454
|
+
if (typeof v !== "string")
|
|
455
|
+
return undefined;
|
|
456
|
+
const n = Number(v);
|
|
457
|
+
if (!Number.isFinite(n) || n < 0)
|
|
458
|
+
return undefined;
|
|
459
|
+
return Math.floor(n);
|
|
460
|
+
}
|
|
293
461
|
export const mastra = toPlugin(MastraPlugin);
|
|
@@ -3,21 +3,24 @@
|
|
|
3
3
|
* tool-invocation result in prior assistant messages before they
|
|
4
4
|
* reach the model.
|
|
5
5
|
*
|
|
6
|
-
* Why: chartIds are
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* Why: chartIds are turn-scoped from the model's point of view -
|
|
7
|
+
* each `prepare_chart` / `render_data` call mints a fresh id and
|
|
8
|
+
* the host UI binds it to that turn's reply. Mastra Memory
|
|
9
|
+
* replays prior tool results into the prompt; if old chartIds
|
|
10
|
+
* leak through, the model is tempted to copy them verbatim into
|
|
11
|
+
* the new turn's `[chart:<id>]` markers and the host UI ends up
|
|
12
|
+
* rendering an unrelated chart from the chart cache (or
|
|
13
|
+
* a 404 once the 1h TTL elapsed). This processor removes the
|
|
14
|
+
* temptation by deleting `chartId` keys from every assistant
|
|
15
|
+
* message's tool results before the prompt is built. The current
|
|
16
|
+
* turn's tool results don't exist yet at `processInput` time, so
|
|
17
|
+
* they pass through unmodified.
|
|
16
18
|
*
|
|
17
19
|
* The strip is recursive - any nested `chartId` field is removed,
|
|
18
|
-
* regardless of which tool produced the result. This covers
|
|
19
|
-
* `
|
|
20
|
-
* uniformly without coupling
|
|
20
|
+
* regardless of which tool produced the result. This covers
|
|
21
|
+
* `prepare_chart` / `render_data` top-level chartIds and any
|
|
22
|
+
* legacy `datasets[].chartId` payloads uniformly without coupling
|
|
23
|
+
* to specific tool ids.
|
|
21
24
|
*/
|
|
22
25
|
import type { InputProcessor } from "@mastra/core/processors";
|
|
23
26
|
/**
|
|
@@ -3,21 +3,24 @@
|
|
|
3
3
|
* tool-invocation result in prior assistant messages before they
|
|
4
4
|
* reach the model.
|
|
5
5
|
*
|
|
6
|
-
* Why: chartIds are
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* Why: chartIds are turn-scoped from the model's point of view -
|
|
7
|
+
* each `prepare_chart` / `render_data` call mints a fresh id and
|
|
8
|
+
* the host UI binds it to that turn's reply. Mastra Memory
|
|
9
|
+
* replays prior tool results into the prompt; if old chartIds
|
|
10
|
+
* leak through, the model is tempted to copy them verbatim into
|
|
11
|
+
* the new turn's `[chart:<id>]` markers and the host UI ends up
|
|
12
|
+
* rendering an unrelated chart from the chart cache (or
|
|
13
|
+
* a 404 once the 1h TTL elapsed). This processor removes the
|
|
14
|
+
* temptation by deleting `chartId` keys from every assistant
|
|
15
|
+
* message's tool results before the prompt is built. The current
|
|
16
|
+
* turn's tool results don't exist yet at `processInput` time, so
|
|
17
|
+
* they pass through unmodified.
|
|
16
18
|
*
|
|
17
19
|
* The strip is recursive - any nested `chartId` field is removed,
|
|
18
|
-
* regardless of which tool produced the result. This covers
|
|
19
|
-
* `
|
|
20
|
-
* uniformly without coupling
|
|
20
|
+
* regardless of which tool produced the result. This covers
|
|
21
|
+
* `prepare_chart` / `render_data` top-level chartIds and any
|
|
22
|
+
* legacy `datasets[].chartId` payloads uniformly without coupling
|
|
23
|
+
* to specific tool ids.
|
|
21
24
|
*/
|
|
22
25
|
import { logUtils } from "@dbx-tools/shared";
|
|
23
26
|
const log = logUtils.logger("mastra/processor/strip-stale-charts");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Databricks Statement Execution helpers for the Mastra plugin.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `client.statementExecution.getStatement` with the shape
|
|
5
|
+
* + size + error handling the plugin's tools and the
|
|
6
|
+
* `/statements/:statementId` route both need:
|
|
7
|
+
*
|
|
8
|
+
* - {@link fetchStatementData}: low-level fetch that returns the
|
|
9
|
+
* raw `{columns, rows, rowCount}` shape used by the
|
|
10
|
+
* `get_statement` tool's output, the `prepare_chart` tool's
|
|
11
|
+
* dataset resolver, and the route's response body. Coerces
|
|
12
|
+
* numeric strings to numbers so downstream charts /
|
|
13
|
+
* aggregations don't have to.
|
|
14
|
+
* - {@link STATEMENT_ROW_CAP}: hard cap callers (notably the
|
|
15
|
+
* `/statements/:statementId` route) clamp `limit` to so a
|
|
16
|
+
* runaway result set can't hose a response.
|
|
17
|
+
* - {@link isStatementNotFoundError}: structural detector that
|
|
18
|
+
* normalizes the SDK's two error classes plus the loose
|
|
19
|
+
* `does not exist` / `not found` message shapes into a single
|
|
20
|
+
* boolean - lets the route map upstream 404s to a clean
|
|
21
|
+
* HTTP 404 without coupling to SDK error-class identity.
|
|
22
|
+
*
|
|
23
|
+
* Not Genie-specific: a Databricks `statement_id` is workspace
|
|
24
|
+
* scoped and lives in the Statement Execution API regardless of
|
|
25
|
+
* which producer (Genie, a tool, a notebook, etc.) submitted the
|
|
26
|
+
* query. Co-located here so consumers can fetch / cap / handle
|
|
27
|
+
* 404s without reaching into the Genie tool module.
|
|
28
|
+
*/
|
|
29
|
+
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
30
|
+
import type { GenieDatasetData } from "@dbx-tools/appkit-mastra-shared";
|
|
31
|
+
/**
|
|
32
|
+
* Hard server-side cap on rows returned by the
|
|
33
|
+
* `/statements/:statementId` route. Sized to keep responses small
|
|
34
|
+
* enough for inline tables to render snappily; the route surfaces
|
|
35
|
+
* a `truncated` flag whenever the upstream `rowCount` exceeds
|
|
36
|
+
* this so end users know they're seeing a sample.
|
|
37
|
+
*/
|
|
38
|
+
export declare const STATEMENT_ROW_CAP = 500;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch a single statement's rows via the Statement Execution API
|
|
41
|
+
* and reshape into the shared {@link GenieDatasetData} shape
|
|
42
|
+
* (column array + row records).
|
|
43
|
+
*
|
|
44
|
+
* Optional `limit` slices the returned `rows` client-side so the
|
|
45
|
+
* agent can scan a small sample without paging the full result
|
|
46
|
+
* set into context. `rowCount` always reflects the upstream total
|
|
47
|
+
* so callers know when the slice truncated.
|
|
48
|
+
*
|
|
49
|
+
* Exported because every consumer in the plugin (the
|
|
50
|
+
* `get_statement` tool, the `prepare_chart` dataset resolver, and
|
|
51
|
+
* the `/statements/:statementId` route) needs the exact same
|
|
52
|
+
* fetch + coercion pipeline so LLM-side `get_statement` output
|
|
53
|
+
* and UI-side `[data:<id>]` rendering stay shape-identical for
|
|
54
|
+
* the same `statement_id`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function fetchStatementData(client: WorkspaceClient, statementId: string, options?: {
|
|
57
|
+
limit?: number;
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}): Promise<GenieDatasetData>;
|
|
60
|
+
/**
|
|
61
|
+
* True when `err` looks like the Databricks SDK's "statement not
|
|
62
|
+
* found" error. Matches the typed {@link ApiError} 404 /
|
|
63
|
+
* `RESOURCE_DOES_NOT_EXIST` shape first, then falls back to the
|
|
64
|
+
* lower-level {@link HttpError} 404, then to a loose `does not
|
|
65
|
+
* exist` / `not found` message sniff for SDK shapes we haven't
|
|
66
|
+
* catalogued.
|
|
67
|
+
*
|
|
68
|
+
* Pulled into its own helper so callers (notably the
|
|
69
|
+
* `/statements/:statementId` route) stay decoupled from SDK
|
|
70
|
+
* error-class identity, and the conversion logic stays testable
|
|
71
|
+
* in isolation.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isStatementNotFoundError(err: unknown): boolean;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Databricks Statement Execution helpers for the Mastra plugin.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `client.statementExecution.getStatement` with the shape
|
|
5
|
+
* + size + error handling the plugin's tools and the
|
|
6
|
+
* `/statements/:statementId` route both need:
|
|
7
|
+
*
|
|
8
|
+
* - {@link fetchStatementData}: low-level fetch that returns the
|
|
9
|
+
* raw `{columns, rows, rowCount}` shape used by the
|
|
10
|
+
* `get_statement` tool's output, the `prepare_chart` tool's
|
|
11
|
+
* dataset resolver, and the route's response body. Coerces
|
|
12
|
+
* numeric strings to numbers so downstream charts /
|
|
13
|
+
* aggregations don't have to.
|
|
14
|
+
* - {@link STATEMENT_ROW_CAP}: hard cap callers (notably the
|
|
15
|
+
* `/statements/:statementId` route) clamp `limit` to so a
|
|
16
|
+
* runaway result set can't hose a response.
|
|
17
|
+
* - {@link isStatementNotFoundError}: structural detector that
|
|
18
|
+
* normalizes the SDK's two error classes plus the loose
|
|
19
|
+
* `does not exist` / `not found` message shapes into a single
|
|
20
|
+
* boolean - lets the route map upstream 404s to a clean
|
|
21
|
+
* HTTP 404 without coupling to SDK error-class identity.
|
|
22
|
+
*
|
|
23
|
+
* Not Genie-specific: a Databricks `statement_id` is workspace
|
|
24
|
+
* scoped and lives in the Statement Execution API regardless of
|
|
25
|
+
* which producer (Genie, a tool, a notebook, etc.) submitted the
|
|
26
|
+
* query. Co-located here so consumers can fetch / cap / handle
|
|
27
|
+
* 404s without reaching into the Genie tool module.
|
|
28
|
+
*/
|
|
29
|
+
import { ApiError, HttpError, WorkspaceClient } from "@databricks/sdk-experimental";
|
|
30
|
+
import { apiUtils } from "@dbx-tools/shared";
|
|
31
|
+
/**
|
|
32
|
+
* Hard server-side cap on rows returned by the
|
|
33
|
+
* `/statements/:statementId` route. Sized to keep responses small
|
|
34
|
+
* enough for inline tables to render snappily; the route surfaces
|
|
35
|
+
* a `truncated` flag whenever the upstream `rowCount` exceeds
|
|
36
|
+
* this so end users know they're seeing a sample.
|
|
37
|
+
*/
|
|
38
|
+
export const STATEMENT_ROW_CAP = 500;
|
|
39
|
+
/**
|
|
40
|
+
* Best-effort numeric coercion for the Statement Execution API's
|
|
41
|
+
* all-strings cells. Leaves non-numeric strings (and explicit
|
|
42
|
+
* `null`s) intact; everything else flows through `Number`.
|
|
43
|
+
*/
|
|
44
|
+
function coerceCell(cell) {
|
|
45
|
+
if (cell === null)
|
|
46
|
+
return null;
|
|
47
|
+
if (/^-?\d+(\.\d+)?$/.test(cell)) {
|
|
48
|
+
const n = Number(cell);
|
|
49
|
+
if (Number.isFinite(n))
|
|
50
|
+
return n;
|
|
51
|
+
}
|
|
52
|
+
return cell;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Fetch a single statement's rows via the Statement Execution API
|
|
56
|
+
* and reshape into the shared {@link GenieDatasetData} shape
|
|
57
|
+
* (column array + row records).
|
|
58
|
+
*
|
|
59
|
+
* Optional `limit` slices the returned `rows` client-side so the
|
|
60
|
+
* agent can scan a small sample without paging the full result
|
|
61
|
+
* set into context. `rowCount` always reflects the upstream total
|
|
62
|
+
* so callers know when the slice truncated.
|
|
63
|
+
*
|
|
64
|
+
* Exported because every consumer in the plugin (the
|
|
65
|
+
* `get_statement` tool, the `prepare_chart` dataset resolver, and
|
|
66
|
+
* the `/statements/:statementId` route) needs the exact same
|
|
67
|
+
* fetch + coercion pipeline so LLM-side `get_statement` output
|
|
68
|
+
* and UI-side `[data:<id>]` rendering stay shape-identical for
|
|
69
|
+
* the same `statement_id`.
|
|
70
|
+
*/
|
|
71
|
+
export async function fetchStatementData(client, statementId, options) {
|
|
72
|
+
const ctx = options?.signal ? apiUtils.toContext(options.signal) : undefined;
|
|
73
|
+
const r = await client.statementExecution.getStatement({ statement_id: statementId }, ctx);
|
|
74
|
+
const columns = (r.manifest?.schema?.columns ?? []).map((c) => c.name ?? "");
|
|
75
|
+
const dataArray = (r.result?.data_array ?? []);
|
|
76
|
+
const sliced = options?.limit !== undefined && options.limit >= 0
|
|
77
|
+
? dataArray.slice(0, options.limit)
|
|
78
|
+
: dataArray;
|
|
79
|
+
const rows = sliced.map((row) => {
|
|
80
|
+
const obj = {};
|
|
81
|
+
columns.forEach((col, i) => {
|
|
82
|
+
obj[col] = coerceCell(row[i] ?? null);
|
|
83
|
+
});
|
|
84
|
+
return obj;
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
columns,
|
|
88
|
+
rows,
|
|
89
|
+
rowCount: r.manifest?.total_row_count ?? dataArray.length,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* True when `err` looks like the Databricks SDK's "statement not
|
|
94
|
+
* found" error. Matches the typed {@link ApiError} 404 /
|
|
95
|
+
* `RESOURCE_DOES_NOT_EXIST` shape first, then falls back to the
|
|
96
|
+
* lower-level {@link HttpError} 404, then to a loose `does not
|
|
97
|
+
* exist` / `not found` message sniff for SDK shapes we haven't
|
|
98
|
+
* catalogued.
|
|
99
|
+
*
|
|
100
|
+
* Pulled into its own helper so callers (notably the
|
|
101
|
+
* `/statements/:statementId` route) stay decoupled from SDK
|
|
102
|
+
* error-class identity, and the conversion logic stays testable
|
|
103
|
+
* in isolation.
|
|
104
|
+
*/
|
|
105
|
+
export function isStatementNotFoundError(err) {
|
|
106
|
+
if (err instanceof ApiError) {
|
|
107
|
+
if (err.statusCode === 404)
|
|
108
|
+
return true;
|
|
109
|
+
if (err.errorCode === "RESOURCE_DOES_NOT_EXIST")
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (err instanceof HttpError && err.code === 404)
|
|
113
|
+
return true;
|
|
114
|
+
if (err instanceof Error && /does not exist|not found/i.test(err.message)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
package/dist/src/tools/email.js
CHANGED
|
@@ -30,40 +30,40 @@ import { createTool } from "@mastra/core/tools";
|
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
const log = logUtils.logger("mastra/tool/send-email");
|
|
32
32
|
const emailInputSchema = z.object({
|
|
33
|
-
to: z.string().describe(stringUtils.toDescription
|
|
33
|
+
to: z.string().describe(stringUtils.toDescription(`
|
|
34
34
|
Single recipient email address (e.g. "alice@example.com"). For
|
|
35
35
|
multiple recipients, comma-separate them yourself.
|
|
36
|
-
`),
|
|
37
|
-
subject: z.string().describe(stringUtils.toDescription
|
|
36
|
+
`)),
|
|
37
|
+
subject: z.string().describe(stringUtils.toDescription(`
|
|
38
38
|
Subject line.
|
|
39
|
-
`),
|
|
40
|
-
body: z.string().describe(stringUtils.toDescription
|
|
41
|
-
Email body. Plain text or markdown; the renderer downstream
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`),
|
|
39
|
+
`)),
|
|
40
|
+
body: z.string().describe(stringUtils.toDescription(`
|
|
41
|
+
Email body. Plain text or markdown; the renderer downstream decides
|
|
42
|
+
which to honour. Be specific - the recipient may not have any
|
|
43
|
+
context the model has from prior chat turns.
|
|
44
|
+
`)),
|
|
45
45
|
cc: z
|
|
46
46
|
.array(z.string())
|
|
47
47
|
.optional()
|
|
48
|
-
.describe(stringUtils.toDescription
|
|
48
|
+
.describe(stringUtils.toDescription(`
|
|
49
49
|
Optional CC recipients.
|
|
50
|
-
`),
|
|
50
|
+
`)),
|
|
51
51
|
bcc: z
|
|
52
52
|
.array(z.string())
|
|
53
53
|
.optional()
|
|
54
|
-
.describe(stringUtils.toDescription
|
|
54
|
+
.describe(stringUtils.toDescription(`
|
|
55
55
|
Optional BCC recipients.
|
|
56
|
-
`),
|
|
56
|
+
`)),
|
|
57
57
|
});
|
|
58
58
|
const emailOutputSchema = z.object({
|
|
59
|
-
sent: z.boolean().describe(stringUtils.toDescription
|
|
59
|
+
sent: z.boolean().describe(stringUtils.toDescription(`
|
|
60
60
|
True when the email was dispatched. The current implementation
|
|
61
|
-
always returns true after console-logging the would-be email;
|
|
62
|
-
|
|
63
|
-
`),
|
|
64
|
-
recipient: z.string().describe(stringUtils.toDescription
|
|
61
|
+
always returns true after console-logging the would-be email; swap
|
|
62
|
+
in a real provider to make this meaningful.
|
|
63
|
+
`)),
|
|
64
|
+
recipient: z.string().describe(stringUtils.toDescription(`
|
|
65
65
|
Echo of the \`to\` field for confirmation.
|
|
66
|
-
`),
|
|
66
|
+
`)),
|
|
67
67
|
});
|
|
68
68
|
/**
|
|
69
69
|
* Build the `send_email` tool. Approval-gated by default; the
|
|
@@ -88,15 +88,14 @@ const emailOutputSchema = z.object({
|
|
|
88
88
|
export function buildEmailTool(opts = {}) {
|
|
89
89
|
return createTool({
|
|
90
90
|
id: opts.id ?? "send_email",
|
|
91
|
-
description: stringUtils.toDescription
|
|
92
|
-
Send an email on the user's behalf. Pass a recipient
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`,
|
|
91
|
+
description: stringUtils.toDescription(`
|
|
92
|
+
Send an email on the user's behalf. Pass a recipient address,
|
|
93
|
+
subject, and body; the user will be prompted to approve the send
|
|
94
|
+
before it goes out (the tool is approval-gated). Use this when
|
|
95
|
+
the user explicitly asks to send / forward / share something via
|
|
96
|
+
email - never autonomously. Keep subjects short and bodies
|
|
97
|
+
focused; the recipient may not have any of the chat context.
|
|
98
|
+
`),
|
|
100
99
|
inputSchema: emailInputSchema,
|
|
101
100
|
outputSchema: emailOutputSchema,
|
|
102
101
|
requireApproval: true,
|