@dbx-tools/appkit-mastra 0.1.25 → 0.1.27

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.
@@ -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
+ * `/embed/data/:id` 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
+ * `/embed/data/:id` 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
+ * `/embed/data/:id` 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 `/embed/data/:id` 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
+ * `/embed/data/:id` 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
+ }
@@ -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
- decides which to honour. Be specific - the recipient may not
43
- have any context the model has from prior chat turns.
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
- swap in a real provider to make this meaningful.
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
- address, subject, and body; the user will be prompted to
94
- approve the send before it goes out (the tool is
95
- approval-gated). Use this when the user explicitly asks
96
- to send / forward / share something via email - never
97
- autonomously. Keep subjects short and bodies focused; the
98
- recipient may not have any of the chat context.
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,