@dbx-tools/appkit-mastra 0.3.29 → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -15
- package/index.ts +3 -0
- package/package.json +10 -10
- package/src/agents.ts +12 -11
- package/src/chart.ts +69 -38
- package/src/config.ts +170 -18
- package/src/defaults.ts +133 -0
- package/src/filesystems.ts +17 -5
- package/src/genie.ts +80 -39
- package/src/history.ts +5 -2
- package/src/model.ts +7 -0
- package/src/plugin.ts +469 -194
- package/src/rest.ts +5 -2
- package/src/server.ts +2 -1
- package/src/serving-sanitize.ts +15 -5
- package/src/storage-schema.ts +4 -1
- package/src/threads.ts +10 -3
- package/src/validation.ts +22 -0
- package/test/config.test.ts +64 -0
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ await createApp({
|
|
|
80
80
|
plugin.mastra({
|
|
81
81
|
agents: { analyst },
|
|
82
82
|
defaultAgent: "analyst",
|
|
83
|
-
|
|
83
|
+
genieSpaces: { sales: "01ef..." },
|
|
84
84
|
}),
|
|
85
85
|
],
|
|
86
86
|
});
|
|
@@ -153,6 +153,31 @@ Tool calls dispatch back through the owning AppKit plugin, preserving OBO auth
|
|
|
153
153
|
and AppKit telemetry behavior. Optional plugins should be guarded with `?.` when
|
|
154
154
|
you spread their tools.
|
|
155
155
|
|
|
156
|
+
### Tools Flow In, Not Out
|
|
157
|
+
|
|
158
|
+
The plugin is a tool _consumer_, not an AppKit `ToolProvider`: it deliberately
|
|
159
|
+
implements neither `getAgentTools()` nor `executeAgentTool()`, so its built-in
|
|
160
|
+
tools (`ask_genie`, `get_space_description`, `get_space_serialized`,
|
|
161
|
+
`get_statement`, `prepare_chart`, `render_data`, `summarize`) are reachable only
|
|
162
|
+
from a Mastra agent turn this plugin serves.
|
|
163
|
+
|
|
164
|
+
That is a property of the tools, not a gap. Each one reads the per-request
|
|
165
|
+
Mastra execution context - the AppKit user stamped on `RequestContext`, the
|
|
166
|
+
`writer` that streams Genie progress events to the chat, the per-call
|
|
167
|
+
`abortSignal` - and refuses to run without it. An AppKit `ToolProvider` call
|
|
168
|
+
carries none of that, so exposing these through one would advertise tools that
|
|
169
|
+
cannot work. Reach for
|
|
170
|
+
[native AppKit Agents](https://developers.databricks.com/docs/appkit/v0) when you
|
|
171
|
+
want your agent tools callable by other AppKit hosts.
|
|
172
|
+
|
|
173
|
+
Nothing here can be auto-inherited by another host as a side effect: with no
|
|
174
|
+
AppKit `ToolRegistry`, there is no `autoInheritable` surface to opt in or out
|
|
175
|
+
of. Every built-in tool is also read-only (Genie questions, statement reads,
|
|
176
|
+
chart planning, summarization), and the ambient tools stay off the MCP server
|
|
177
|
+
unless `mcp: { tools: true }` names them explicitly. Approval-gated tools you
|
|
178
|
+
register yourself are enforced separately: boot fails if one is registered
|
|
179
|
+
without Mastra storage to persist the suspended run.
|
|
180
|
+
|
|
156
181
|
## Memory And Storage
|
|
157
182
|
|
|
158
183
|
The `memory` and `storage` config fields can be `false`, `true`, or a concrete
|
|
@@ -232,19 +257,24 @@ record, resolves the data in the background, and stores a terminal chart or
|
|
|
232
257
|
error. `chart.fetchChart()` long-polls that cache for route handlers and custom
|
|
233
258
|
clients.
|
|
234
259
|
|
|
260
|
+
Both take a `userKey`: the chart cache is namespaced by the caller's identity,
|
|
261
|
+
so a chart id lifted from another user's transcript resolves to nothing and the
|
|
262
|
+
embed route answers `404`. Use `config.resolveUserKey()`, which reads the AppKit
|
|
263
|
+
user off the Mastra request context and falls back to the ambient execution
|
|
264
|
+
context.
|
|
265
|
+
|
|
235
266
|
```ts
|
|
267
|
+
const userKey = config.resolveUserKey(requestContext);
|
|
268
|
+
|
|
236
269
|
const { chartId } = await chart.prepareChart({
|
|
237
|
-
config,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
data: rows,
|
|
243
|
-
},
|
|
244
|
-
resolveData: async () => rows,
|
|
270
|
+
config: pluginConfig,
|
|
271
|
+
userKey,
|
|
272
|
+
title: "Revenue by region",
|
|
273
|
+
description: "Compare total revenue by region.",
|
|
274
|
+
resolveData: async () => ({ rows }),
|
|
245
275
|
});
|
|
246
276
|
|
|
247
|
-
const resolved = await chart.fetchChart(chartId);
|
|
277
|
+
const resolved = await chart.fetchChart(chartId, { userKey });
|
|
248
278
|
```
|
|
249
279
|
|
|
250
280
|
Agents can return `[chart:<id>]` and `[data:<statement_id>]` markers in prose.
|
|
@@ -302,7 +332,9 @@ returning `{ agentId, model, displayName }` - the static serving-endpoint an
|
|
|
302
332
|
agent falls back to when the client pins no model, plus its humanized label.
|
|
303
333
|
`model` / `displayName` are `null` when the agent resolves its model
|
|
304
334
|
dynamically at call time. This lets a model picker label its default option
|
|
305
|
-
without waiting on the `/models` catalogue (so it never flashes a raw id).
|
|
335
|
+
without waiting on the `/models` catalogue (so it never flashes a raw id). A
|
|
336
|
+
`:agentId` that is not registered returns `404` with the registered ids, the
|
|
337
|
+
same as the history and threads routes.
|
|
306
338
|
|
|
307
339
|
## Threads, History, And Suggestions
|
|
308
340
|
|
|
@@ -366,6 +398,40 @@ client needs. Use `apiAccess: "full"` only for a trusted first-party console.
|
|
|
366
398
|
`server.isMastraRequestAllowed()` is exported for tests and custom dispatch
|
|
367
399
|
logic that need the same allowlist.
|
|
368
400
|
|
|
401
|
+
## Routes
|
|
402
|
+
|
|
403
|
+
Mounted under the plugin base path, which is `/api/mastra` unless you override
|
|
404
|
+
`name`. Every route below is registered through AppKit's `route()` helper, so it
|
|
405
|
+
appears in the plugin's endpoint map and forwards handler errors to AppKit.
|
|
406
|
+
|
|
407
|
+
| Method | Path | Purpose |
|
|
408
|
+
| -------------------------- | --------------------------- | ------------------------------------------------------------------------------------------- |
|
|
409
|
+
| `GET` | `/models` | Serving-endpoint catalogue for a model picker. |
|
|
410
|
+
| `GET` | `/default-model[/:agentId]` | Static default model an agent falls back to, with its humanized label. `404` on unknown id. |
|
|
411
|
+
| `GET` | `/suggestions[/:agentId]` | Starter questions from the configured Genie spaces. Degrades to `[]`. |
|
|
412
|
+
| `GET` | `/embed/chart/:id` | Long-polls a `[chart:<id>]` marker's cached spec. `?timeoutMs=` up to 5 minutes. |
|
|
413
|
+
| `GET` | `/embed/data/:id` | Rows behind a `[data:<statement_id>]` marker. `?limit=` clamped server-side. |
|
|
414
|
+
| `GET` / `DELETE` | `/route/history[/:agentId]` | Load or clear the caller's thread messages. |
|
|
415
|
+
| `GET` / `DELETE` / `PATCH` | `/route/threads[/:agentId]` | List, delete, or rename the caller's conversations. |
|
|
416
|
+
| `POST` | `/route/feedback` | Log a thumbs / comment assessment to the turn's MLflow trace. `404` when feedback is off. |
|
|
417
|
+
| `POST` / `GET` | `/mcp`, `/sse`, `/messages` | MCP transports, when `mcp` is enabled. |
|
|
418
|
+
|
|
419
|
+
Agent inference itself rides the stock Mastra routes (`/agents/:id/stream`), so
|
|
420
|
+
`@mastra/client-js` and `@dbx-tools/ui-mastra` work without a bespoke protocol.
|
|
421
|
+
|
|
422
|
+
## Environment Variables
|
|
423
|
+
|
|
424
|
+
Every value can also be set through plugin config, which wins. These are the
|
|
425
|
+
fallbacks, so a deployment that already follows AppKit's Databricks env naming
|
|
426
|
+
needs no extra wiring.
|
|
427
|
+
|
|
428
|
+
| Variable | Effect |
|
|
429
|
+
| ------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
430
|
+
| `DATABRICKS_SERVING_ENDPOINT_NAME` | Model used when neither the agent nor `defaultModel` names one. |
|
|
431
|
+
| `DATABRICKS_GENIE_SPACE_ID` | Genie space registered under the `default` alias. |
|
|
432
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Presence of either turns Mastra tracing on when `observability` is unset. |
|
|
433
|
+
| `MLFLOW_EXPERIMENT_ID`, `MLFLOW_EXPERIMENT_NAME` | With an OTLP endpoint, turns MLflow feedback on when `feedback` is unset. |
|
|
434
|
+
|
|
369
435
|
## Configuration Reference
|
|
370
436
|
|
|
371
437
|
The plugin config is intentionally centered on the AppKit lifecycle instead of
|
|
@@ -377,8 +443,11 @@ requiring callers to assemble a Mastra server by hand.
|
|
|
377
443
|
name an agent explicitly.
|
|
378
444
|
- `storage` and `memory` accept `true`, `false`, or concrete Mastra Postgres /
|
|
379
445
|
PgVector options. `true` resolves from `lakebase()` when present.
|
|
380
|
-
- `
|
|
381
|
-
|
|
446
|
+
- `genieSpaces` maps aliases to Genie Space IDs (or to
|
|
447
|
+
`{ spaceId, hint }` objects). Those aliases flow into tool names,
|
|
448
|
+
suggestions, and chart/data workflows. An alias present with no space id is a
|
|
449
|
+
wiring contradiction and fails at construction rather than silently
|
|
450
|
+
registering no Genie tools.
|
|
382
451
|
- `defaultModel`, `modelOverride`, and `modelFuzzyMatch` control how loose model
|
|
383
452
|
names are resolved through Databricks Model Serving.
|
|
384
453
|
- `feedback` controls whether MLflow feedback routes are exposed. The automatic
|
|
@@ -403,8 +472,10 @@ client that talks to these routes.
|
|
|
403
472
|
- `genie` - Genie prompt, space normalization, Genie toolkits, and suggestions.
|
|
404
473
|
- `chart` / `statement` / `writer` - chart cache, statement row fetches, and
|
|
405
474
|
safe writer events.
|
|
406
|
-
- `history` / `threads` / `pagination` - conversation persistence
|
|
407
|
-
route handlers.
|
|
475
|
+
- `history` / `threads` / `pagination` / `validation` - conversation persistence
|
|
476
|
+
helpers, route handlers, and request-body validation.
|
|
477
|
+
- `defaults` - cache / retry / timeout settings for the plugin's own outbound
|
|
478
|
+
calls, one constant per call site with its reasoning.
|
|
408
479
|
- `memory` / `storageSchema` - Lakebase-backed Mastra store/vector setup.
|
|
409
480
|
- `workspaces` / `filesystems` - Mastra workspace creation and Databricks
|
|
410
481
|
Workspace file adapters.
|
package/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export * as agents from "./src/agents";
|
|
6
6
|
export * as chart from "./src/chart";
|
|
7
7
|
export * as config from "./src/config";
|
|
8
|
+
export * as defaults from "./src/defaults";
|
|
8
9
|
export * as filesystems from "./src/filesystems";
|
|
9
10
|
export * as genie from "./src/genie";
|
|
10
11
|
export * as history from "./src/history";
|
|
@@ -24,6 +25,7 @@ export * as statement from "./src/statement";
|
|
|
24
25
|
export * as storageSchema from "./src/storage-schema";
|
|
25
26
|
export * as summarize from "./src/summarize";
|
|
26
27
|
export * as threads from "./src/threads";
|
|
28
|
+
export * as validation from "./src/validation";
|
|
27
29
|
export * as workspaces from "./src/workspaces";
|
|
28
30
|
export * as writer from "./src/writer";
|
|
29
31
|
export type { MastraTools, AppKitToolOptions, ToolkitOptions, MastraPluginToolkitProvider, MastraPlugins, MastraToolsFn, MastraAgentWorkspaceResolver, MastraAgentDefinition, MastraStorageConfigOverride, MastraMemoryConfigOverride, BuiltAgents } from "./src/agents";
|
|
@@ -43,4 +45,5 @@ export type { ModelOverrideRequest } from "./src/serving";
|
|
|
43
45
|
export type { ServingChatMessage } from "./src/serving-sanitize";
|
|
44
46
|
export type { SummarizeOptions } from "./src/summarize";
|
|
45
47
|
export type { ListThreadsOptions, DeleteThreadOptions, RenameThreadOptions, ThreadsRouteOptions } from "./src/threads";
|
|
48
|
+
export type { SchemaIssues } from "./src/validation";
|
|
46
49
|
export type { CreateWorkspaceOptions } from "./src/workspaces";
|
package/package.json
CHANGED
|
@@ -27,22 +27,22 @@
|
|
|
27
27
|
"@opentelemetry/api": "^1.9.1",
|
|
28
28
|
"express": "^5.1.0",
|
|
29
29
|
"pg": "^8.22.0",
|
|
30
|
-
"zod": "
|
|
31
|
-
"@dbx-tools/appkit": "0.3.
|
|
32
|
-
"@dbx-tools/
|
|
33
|
-
"@dbx-tools/
|
|
34
|
-
"@dbx-tools/
|
|
35
|
-
"@dbx-tools/shared-core": "0.3.
|
|
36
|
-
"@dbx-tools/shared-mastra": "0.3.
|
|
37
|
-
"@dbx-tools/shared-genie": "0.3.
|
|
38
|
-
"@dbx-tools/shared-model": "0.3.
|
|
30
|
+
"zod": "4.3.6",
|
|
31
|
+
"@dbx-tools/appkit": "0.3.31",
|
|
32
|
+
"@dbx-tools/genie": "0.3.31",
|
|
33
|
+
"@dbx-tools/core": "0.3.31",
|
|
34
|
+
"@dbx-tools/model": "0.3.31",
|
|
35
|
+
"@dbx-tools/shared-core": "0.3.31",
|
|
36
|
+
"@dbx-tools/shared-mastra": "0.3.31",
|
|
37
|
+
"@dbx-tools/shared-genie": "0.3.31",
|
|
38
|
+
"@dbx-tools/shared-model": "0.3.31"
|
|
39
39
|
},
|
|
40
40
|
"main": "index.ts",
|
|
41
41
|
"license": "UNLICENSED",
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
|
-
"version": "0.3.
|
|
45
|
+
"version": "0.3.31",
|
|
46
46
|
"types": "index.ts",
|
|
47
47
|
"type": "module",
|
|
48
48
|
"exports": {
|
package/src/agents.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @module
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { ConfigurationError } from "@databricks/appkit";
|
|
18
19
|
import { plugin } from "@dbx-tools/appkit";
|
|
19
20
|
import { fallback } from "@dbx-tools/model";
|
|
20
21
|
import { log, object, string } from "@dbx-tools/shared-core";
|
|
@@ -524,8 +525,9 @@ export async function buildAgents(opts: {
|
|
|
524
525
|
}
|
|
525
526
|
|
|
526
527
|
if (!agents[defaultAgentId]) {
|
|
527
|
-
throw
|
|
528
|
-
`mastra
|
|
528
|
+
throw ConfigurationError.resourceNotFound(
|
|
529
|
+
`mastra defaultAgent "${defaultAgentId}"`,
|
|
530
|
+
`Registered agents: ${ids.join(", ") || "none"}.`,
|
|
529
531
|
);
|
|
530
532
|
}
|
|
531
533
|
|
|
@@ -578,7 +580,7 @@ function assertApprovalGatedToolsHaveStorage(
|
|
|
578
580
|
const detail = gated
|
|
579
581
|
.map(({ agentId, toolIds }) => `${agentId}: ${toolIds.join(", ")}`)
|
|
580
582
|
.join("; ");
|
|
581
|
-
throw new
|
|
583
|
+
throw new ConfigurationError(
|
|
582
584
|
"mastra: approval-gated tools require plugin storage (PostgresStore) to persist suspended runs. " +
|
|
583
585
|
`Affected agents/tools: ${detail}. ` +
|
|
584
586
|
"Register lakebase() before mastra() so storage auto-enables, or pass storage: true explicitly.",
|
|
@@ -637,7 +639,7 @@ function resolveDefinitions(config: MastraPluginConfig): Record<string, MastraAg
|
|
|
637
639
|
input.forEach((def, i) => {
|
|
638
640
|
const key = deriveAgentKey(def, i);
|
|
639
641
|
if (out[key]) {
|
|
640
|
-
throw new
|
|
642
|
+
throw new ConfigurationError(
|
|
641
643
|
`mastra: duplicate agent id "${key}" derived from name "${def.name ?? ""}"; ` +
|
|
642
644
|
`set unique \`name\`s on each definition`,
|
|
643
645
|
);
|
|
@@ -749,7 +751,7 @@ function resolveAgentWorkspace(
|
|
|
749
751
|
* `genie` is special-cased to swap the generic AppKit toolkit (which
|
|
750
752
|
* runs `executeAgentTool` and only emits a single final `tool-result`
|
|
751
753
|
* chunk per call) for the streaming-aware tools built by
|
|
752
|
-
* {@link
|
|
754
|
+
* {@link buildGenieToolkitProvider}. The streaming variant forwards each
|
|
753
755
|
* Genie wire event (status, SQL, row counts, errors) out through the
|
|
754
756
|
* Mastra `ctx.writer`, so the UI gets `tool-output` chunks in real
|
|
755
757
|
* time instead of staring at a spinner for the full Genie round-trip.
|
|
@@ -782,12 +784,11 @@ function buildPluginsMap(
|
|
|
782
784
|
* Genie agent inherits the same model resolver / fallback
|
|
783
785
|
* ladder the calling agents use.
|
|
784
786
|
*
|
|
785
|
-
* The Genie
|
|
786
|
-
* (`genieEventChat`) and the workspace
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
* wiring keep working without change.
|
|
787
|
+
* The Genie tools talk to Genie directly through `@dbx-tools/genie`
|
|
788
|
+
* (`genieEventChat`) and the workspace `statementExecution.getStatement`
|
|
789
|
+
* API; AppKit's stock `genie` plugin contributes only its resource
|
|
790
|
+
* manifest and `spaces` config, so an `app.yaml` resource binding and a
|
|
791
|
+
* `genie({ spaces })` record have the same meaning here as they do there.
|
|
791
792
|
*/
|
|
792
793
|
function resolveProvider(
|
|
793
794
|
config: MastraPluginConfig,
|
package/src/chart.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* @module
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { CacheManager } from "@databricks/appkit";
|
|
30
|
+
import { AppKitError, CacheManager, ExecutionError } from "@databricks/appkit";
|
|
31
31
|
import { async, error, hash, log, string, type BrandContext } from "@dbx-tools/shared-core";
|
|
32
32
|
import { wire, type Chart, type ChartResult } from "@dbx-tools/shared-mastra";
|
|
33
33
|
import { model } from "@dbx-tools/shared-model";
|
|
@@ -36,7 +36,7 @@ import type { RequestContext } from "@mastra/core/request-context";
|
|
|
36
36
|
import { createTool } from "@mastra/core/tools";
|
|
37
37
|
import { z } from "zod";
|
|
38
38
|
|
|
39
|
-
import type
|
|
39
|
+
import { resolveUserKey, type MastraPluginConfig } from "./config";
|
|
40
40
|
import { buildModel } from "./model";
|
|
41
41
|
|
|
42
42
|
const logger = log.logger("mastra/chart");
|
|
@@ -55,20 +55,15 @@ const CHART_CACHE_TTL_SEC = 60 * 60;
|
|
|
55
55
|
/** Cache namespace; keeps the chart keyspace tidy. */
|
|
56
56
|
const CHART_CACHE_NAMESPACE = "mastra:chart";
|
|
57
57
|
|
|
58
|
-
/**
|
|
59
|
-
* `userKey` for `CacheManager.generateKey`. Chart ids are minted
|
|
60
|
-
* via `hash.id()` (v4 UUID) and are unguessable, so a
|
|
61
|
-
* constant user key is fine. The HTTP route can re-scope to the
|
|
62
|
-
* requesting user when policy demands it.
|
|
63
|
-
*/
|
|
64
|
-
const CHART_CACHE_USER_KEY = "mastra-chart";
|
|
65
|
-
|
|
66
58
|
/** Default server-side long-poll budget for {@link fetchChart}. */
|
|
67
59
|
const DEFAULT_FETCH_TIMEOUT_MS = 60_000;
|
|
68
60
|
|
|
69
61
|
/** Default inter-poll sleep for {@link fetchChart}. */
|
|
70
62
|
const DEFAULT_FETCH_INTERVAL_MS = 250;
|
|
71
63
|
|
|
64
|
+
/** Stable text stored on a chart entry whose planner run failed. */
|
|
65
|
+
const CHART_FAILED_MESSAGE = "Chart generation failed";
|
|
66
|
+
|
|
72
67
|
/* ------------------------------- schemas ------------------------------- */
|
|
73
68
|
|
|
74
69
|
/**
|
|
@@ -359,23 +354,27 @@ async function runChartPlanner(
|
|
|
359
354
|
|
|
360
355
|
/* ------------------------------ cache helpers ------------------------------ */
|
|
361
356
|
|
|
362
|
-
/**
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Build the canonical cache key for a `chartId` owned by `userKey`.
|
|
359
|
+
*
|
|
360
|
+
* The identity is part of the key, not a filter applied after the read, so a
|
|
361
|
+
* chart id guessed or copied from another user's transcript resolves to a
|
|
362
|
+
* different key and simply misses. That miss is what the HTTP route turns into
|
|
363
|
+
* a 404, which is also the answer that leaks the least.
|
|
364
|
+
*/
|
|
365
|
+
async function chartCacheKey(chartId: string, userKey: string): Promise<string> {
|
|
366
|
+
return (await CacheManager.getInstance()).generateKey([CHART_CACHE_NAMESPACE, chartId], userKey);
|
|
368
367
|
}
|
|
369
368
|
|
|
370
369
|
/**
|
|
371
|
-
* Persist a {@link Chart} entry under its `chartId`.
|
|
372
|
-
* the TTL on every write. Cache-layer failures are logged and
|
|
370
|
+
* Persist a {@link Chart} entry under its `chartId`, owned by `userKey`.
|
|
371
|
+
* Refreshes the TTL on every write. Cache-layer failures are logged and
|
|
373
372
|
* swallowed so background runners never throw into the
|
|
374
373
|
* unhandled-rejection stream.
|
|
375
374
|
*/
|
|
376
|
-
async function writeChart(entry: Chart): Promise<void> {
|
|
375
|
+
async function writeChart(entry: Chart, userKey: string): Promise<void> {
|
|
377
376
|
try {
|
|
378
|
-
const key = await chartCacheKey(entry.chartId);
|
|
377
|
+
const key = await chartCacheKey(entry.chartId, userKey);
|
|
379
378
|
await CacheManager.getInstanceSync().set(key, entry, {
|
|
380
379
|
ttl: CHART_CACHE_TTL_SEC,
|
|
381
380
|
});
|
|
@@ -388,12 +387,13 @@ async function writeChart(entry: Chart): Promise<void> {
|
|
|
388
387
|
}
|
|
389
388
|
|
|
390
389
|
/**
|
|
391
|
-
* Look up a chart
|
|
392
|
-
* expiry, or when the cache layer is
|
|
390
|
+
* Look up a chart `userKey` owns. Returns `undefined` on miss, on
|
|
391
|
+
* expiry, when another identity owns the id, or when the cache layer is
|
|
392
|
+
* unhealthy - never throws.
|
|
393
393
|
*/
|
|
394
|
-
async function readChart(chartId: string): Promise<Chart | undefined> {
|
|
394
|
+
async function readChart(chartId: string, userKey: string): Promise<Chart | undefined> {
|
|
395
395
|
try {
|
|
396
|
-
const key = await chartCacheKey(chartId);
|
|
396
|
+
const key = await chartCacheKey(chartId, userKey);
|
|
397
397
|
const v = await CacheManager.getInstanceSync().get<Chart>(key);
|
|
398
398
|
return v ?? undefined;
|
|
399
399
|
} catch (err) {
|
|
@@ -411,6 +411,11 @@ async function readChart(chartId: string): Promise<Chart | undefined> {
|
|
|
411
411
|
export interface PrepareChartOptions {
|
|
412
412
|
/** Plugin config; resolves the planner agent's model. */
|
|
413
413
|
config: MastraPluginConfig;
|
|
414
|
+
/**
|
|
415
|
+
* Identity that owns the minted chart. Only a {@link fetchChart} call
|
|
416
|
+
* carrying the same key resolves it. Use {@link resolveUserKey}.
|
|
417
|
+
*/
|
|
418
|
+
userKey: string;
|
|
414
419
|
/** Display title forwarded to the planner agent. */
|
|
415
420
|
title?: string;
|
|
416
421
|
/** Optional intent hint forwarded to the planner agent. */
|
|
@@ -455,7 +460,7 @@ export interface PrepareChartOptions {
|
|
|
455
460
|
*/
|
|
456
461
|
export async function prepareChart(opts: PrepareChartOptions): Promise<{ chartId: string }> {
|
|
457
462
|
const chartId = hash.id();
|
|
458
|
-
await writeChart({ chartId });
|
|
463
|
+
await writeChart({ chartId }, opts.userKey);
|
|
459
464
|
logger.debug("queued", { chartId });
|
|
460
465
|
// Fire-and-forget. Failures land in the cache as `error` entries;
|
|
461
466
|
// never escape into an unhandled rejection.
|
|
@@ -468,7 +473,7 @@ async function runPrepareChart(chartId: string, opts: PrepareChartOptions): Prom
|
|
|
468
473
|
try {
|
|
469
474
|
const data = await opts.resolveData(opts.signal);
|
|
470
475
|
if (data.rows.length === 0) {
|
|
471
|
-
throw new
|
|
476
|
+
throw new ExecutionError("Dataset has no rows; nothing to chart");
|
|
472
477
|
}
|
|
473
478
|
const result = await runChartPlanner(
|
|
474
479
|
opts.config,
|
|
@@ -482,16 +487,18 @@ async function runPrepareChart(chartId: string, opts: PrepareChartOptions): Prom
|
|
|
482
487
|
...(opts.signal ? { abortSignal: opts.signal } : {}),
|
|
483
488
|
},
|
|
484
489
|
);
|
|
485
|
-
await writeChart({ chartId, result });
|
|
490
|
+
await writeChart({ chartId, result }, opts.userKey);
|
|
486
491
|
logger.info("done", {
|
|
487
492
|
chartId,
|
|
488
493
|
chartType: result.chartType,
|
|
489
494
|
elapsedMs: Date.now() - startedAt,
|
|
490
495
|
});
|
|
491
496
|
} catch (err) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
497
|
+
logger.warn("error", { chartId, error: error.errorMessage(err) });
|
|
498
|
+
// The entry's `error` is rendered in the chat, so only an AppKitError's
|
|
499
|
+
// own message travels; anything else could carry upstream provider detail.
|
|
500
|
+
const errText = err instanceof AppKitError ? err.message : CHART_FAILED_MESSAGE;
|
|
501
|
+
await writeChart({ chartId, error: errText }, opts.userKey);
|
|
495
502
|
}
|
|
496
503
|
}
|
|
497
504
|
|
|
@@ -499,6 +506,11 @@ async function runPrepareChart(chartId: string, opts: PrepareChartOptions): Prom
|
|
|
499
506
|
|
|
500
507
|
/** Inputs to {@link fetchChart}. */
|
|
501
508
|
export interface FetchChartOptions {
|
|
509
|
+
/**
|
|
510
|
+
* Identity the chart must belong to. A chart minted under a different key
|
|
511
|
+
* is indistinguishable from an unknown id. Use {@link resolveUserKey}.
|
|
512
|
+
*/
|
|
513
|
+
userKey: string;
|
|
502
514
|
/**
|
|
503
515
|
* Server-side polling budget in ms. When the entry stays in
|
|
504
516
|
* the processing state past this window, the helper returns the
|
|
@@ -524,8 +536,8 @@ export interface FetchChartOptions {
|
|
|
524
536
|
* - the resolved {@link Chart} when it settled, errored, or
|
|
525
537
|
* stayed in processing past `timeoutMs` (so the client can
|
|
526
538
|
* re-poll);
|
|
527
|
-
* - `undefined` when the entry is missing or
|
|
528
|
-
* consumer should treat as 404).
|
|
539
|
+
* - `undefined` when the entry is missing, expired, or owned by
|
|
540
|
+
* another identity (the consumer should treat as 404).
|
|
529
541
|
*
|
|
530
542
|
* `signal` lets the caller cancel ahead of timeout (e.g. the HTTP
|
|
531
543
|
* request closed). Cancellation propagates to the inter-poll sleep
|
|
@@ -533,7 +545,7 @@ export interface FetchChartOptions {
|
|
|
533
545
|
*/
|
|
534
546
|
export async function fetchChart(
|
|
535
547
|
chartId: string,
|
|
536
|
-
options: FetchChartOptions
|
|
548
|
+
options: FetchChartOptions,
|
|
537
549
|
): Promise<Chart | undefined> {
|
|
538
550
|
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
539
551
|
const intervalMs = options.intervalMs ?? DEFAULT_FETCH_INTERVAL_MS;
|
|
@@ -542,7 +554,7 @@ export async function fetchChart(
|
|
|
542
554
|
let last: Chart | undefined;
|
|
543
555
|
while (true) {
|
|
544
556
|
options.signal?.throwIfAborted();
|
|
545
|
-
last = await readChart(chartId);
|
|
557
|
+
last = await readChart(chartId, options.userKey);
|
|
546
558
|
if (!last) return undefined;
|
|
547
559
|
if (last.result !== undefined || last.error !== undefined) return last;
|
|
548
560
|
const remaining = deadline - Date.now();
|
|
@@ -564,16 +576,34 @@ interface ChartTheme {
|
|
|
564
576
|
textStyle: { fontFamily: string; color: string };
|
|
565
577
|
}
|
|
566
578
|
|
|
579
|
+
/**
|
|
580
|
+
* AppKit's categorical chart palette (`--chart-cat-1` .. `--chart-cat-8` from
|
|
581
|
+
* `@databricks/appkit-ui`), as hex. Server-rendered chart specs cannot read the
|
|
582
|
+
* browser's CSS custom properties, so the values are mirrored here to keep a
|
|
583
|
+
* generated chart visually consistent with the AppKit-styled UI around it.
|
|
584
|
+
* Keep in sync with AppKit's stylesheet if it changes.
|
|
585
|
+
*/
|
|
586
|
+
const APPKIT_CHART_PALETTE = [
|
|
587
|
+
"#2463EB", // blue
|
|
588
|
+
"#2EB88A", // teal
|
|
589
|
+
"#AB47BD", // purple
|
|
590
|
+
"#F69E23", // amber
|
|
591
|
+
"#DD2C4D", // rose
|
|
592
|
+
"#1BA3BB", // cyan
|
|
593
|
+
"#9B61D1", // lavender
|
|
594
|
+
"#34B262", // emerald
|
|
595
|
+
] as const;
|
|
596
|
+
|
|
567
597
|
/**
|
|
568
598
|
* Expand two brand hex colors into a legible categorical cycle. Echarts
|
|
569
599
|
* repeats the `color` array across series / categories, so a good default
|
|
570
600
|
* needs several distinct hues, not two. We seed with the brand primary and
|
|
571
|
-
* accent (the identity colors) and follow with
|
|
572
|
-
*
|
|
573
|
-
* one or two marks
|
|
601
|
+
* accent (the identity colors) and follow with AppKit's own categorical chart
|
|
602
|
+
* palette, so multi-series / many-slice charts stay readable and match the
|
|
603
|
+
* surrounding AppKit UI while the first one or two marks land on-brand.
|
|
574
604
|
*/
|
|
575
605
|
function brandColorCycle(primary: string, accent: string): string[] {
|
|
576
|
-
const spread =
|
|
606
|
+
const spread = APPKIT_CHART_PALETTE;
|
|
577
607
|
const seen = new Set<string>();
|
|
578
608
|
return [primary, accent, ...spread].filter((c) => {
|
|
579
609
|
const key = c.toLowerCase();
|
|
@@ -742,6 +772,7 @@ export function buildRenderDataTool(config: MastraPluginConfig) {
|
|
|
742
772
|
const ctx = ctxRaw as { requestContext?: RequestContext } | undefined;
|
|
743
773
|
return prepareChart({
|
|
744
774
|
config,
|
|
775
|
+
userKey: resolveUserKey(ctx?.requestContext),
|
|
745
776
|
title,
|
|
746
777
|
...(description ? { description } : {}),
|
|
747
778
|
resolveData: () => Promise.resolve({ rows: data }),
|