@dbx-tools/appkit-mastra 0.9.128 → 0.9.131
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 +90 -31
- package/index.ts +10 -8
- package/lib/index.d.ts +10 -8
- package/lib/index.js +8 -7
- package/lib/src/agents.d.ts +4 -0
- package/lib/src/agents.js +12 -3
- package/lib/src/config.d.ts +7 -2
- package/lib/src/config.js +6 -3
- package/lib/src/defaults.d.ts +9 -0
- package/lib/src/defaults.js +29 -1
- package/lib/src/genie-chat.d.ts +71 -0
- package/lib/src/genie-chat.js +272 -0
- package/lib/src/genie.d.ts +2 -0
- package/lib/src/genie.js +11 -8
- package/lib/src/memory.d.ts +5 -5
- package/lib/src/memory.js +10 -3
- package/lib/src/observability.d.ts +3 -0
- package/lib/src/observability.js +2 -2
- package/lib/src/plugin.d.ts +36 -7
- package/lib/src/plugin.js +220 -33
- package/lib/src/request-context.d.ts +2 -0
- package/lib/src/request-context.js +3 -0
- package/lib/src/server.d.ts +4 -0
- package/lib/src/server.js +40 -2
- package/lib/src/statement.d.ts +16 -1
- package/lib/src/statement.js +50 -18
- package/lib/src/telemetry.d.ts +22 -1
- package/lib/src/telemetry.js +174 -18
- package/lib/src/workspaces.d.ts +2 -2
- package/lib/src/workspaces.js +3 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +32 -25
- package/src/agents.ts +15 -1
- package/src/config.ts +10 -4
- package/src/defaults.ts +30 -0
- package/src/genie-chat.ts +341 -0
- package/src/genie.ts +11 -7
- package/src/memory.ts +9 -4
- package/src/observability.ts +6 -2
- package/src/plugin.ts +262 -31
- package/src/request-context.ts +2 -0
- package/src/server.ts +35 -0
- package/src/statement.ts +71 -19
- package/src/telemetry.ts +206 -16
- package/src/workspaces.ts +4 -3
- package/exports.ts +0 -7
- package/lib/exports.d.ts +0 -1
- package/lib/exports.js +0 -2
- package/lib/src/openapi-tool.d.ts +0 -29
- package/lib/src/openapi-tool.js +0 -175
- package/src/openapi-tool.ts +0 -245
package/src/plugin.ts
CHANGED
|
@@ -56,13 +56,15 @@ import {
|
|
|
56
56
|
type PluginManifest,
|
|
57
57
|
type ResourceRequirement,
|
|
58
58
|
} from "@databricks/appkit";
|
|
59
|
-
import { lakebaseResolver, pluginRegistry } from "@dbx-tools/appkit";
|
|
59
|
+
import { lakebaseResolver, pluginRegistry, telemetry as appkitTelemetry } from "@dbx-tools/appkit";
|
|
60
60
|
import { modelCatalog } from "@dbx-tools/model";
|
|
61
61
|
import { asyncUtils, errorUtils, log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
62
62
|
import {
|
|
63
63
|
feedback,
|
|
64
64
|
routes,
|
|
65
|
+
wire,
|
|
65
66
|
type Chart,
|
|
67
|
+
type GenieChatRequest,
|
|
66
68
|
type MastraClientConfig,
|
|
67
69
|
type MastraFeedbackRequest,
|
|
68
70
|
type StatementData,
|
|
@@ -71,10 +73,16 @@ import { display, type ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
|
71
73
|
import type { Agent } from "@mastra/core/agent";
|
|
72
74
|
import { Mastra } from "@mastra/core/mastra";
|
|
73
75
|
import type { RequestContext } from "@mastra/core/request-context";
|
|
76
|
+
import { pipeUIMessageStreamToResponse } from "ai";
|
|
74
77
|
import express from "express";
|
|
75
78
|
import type { Pool } from "pg";
|
|
76
79
|
|
|
77
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
buildAgents,
|
|
82
|
+
FALLBACK_AGENT_ID,
|
|
83
|
+
type BuiltAgents,
|
|
84
|
+
type MastraOutputProcessorFactory,
|
|
85
|
+
} from "./agents.ts";
|
|
78
86
|
import { fetchChart } from "./chart.ts";
|
|
79
87
|
import { agentChatRoutes } from "./chat.ts";
|
|
80
88
|
import {
|
|
@@ -86,11 +94,21 @@ import {
|
|
|
86
94
|
import {
|
|
87
95
|
chartFetchDefaults,
|
|
88
96
|
feedbackWriteDefaults,
|
|
97
|
+
genieConversationReadDefaults,
|
|
98
|
+
genieConversationWriteDefaults,
|
|
89
99
|
genieSuggestionDefaults,
|
|
90
100
|
modelCatalogueDefaults,
|
|
91
101
|
statementDataDefaults,
|
|
92
102
|
} from "./defaults.ts";
|
|
93
|
-
import {
|
|
103
|
+
import {
|
|
104
|
+
deleteGenieThread,
|
|
105
|
+
deleteGenieThreadMessage,
|
|
106
|
+
genieChatStream,
|
|
107
|
+
listGenieThreadMessages,
|
|
108
|
+
listGenieThreads,
|
|
109
|
+
type GenieChatSpace,
|
|
110
|
+
} from "./genie-chat.ts";
|
|
111
|
+
import { collectSpaceSuggestions, resolveGenieSpaces, type GenieSpaceConfig } from "./genie.ts";
|
|
94
112
|
import {
|
|
95
113
|
requestUserId,
|
|
96
114
|
resolveIdentityMode,
|
|
@@ -117,8 +135,8 @@ import {
|
|
|
117
135
|
MastraServer,
|
|
118
136
|
} from "./server.ts";
|
|
119
137
|
import { resolveServingConfig } from "./serving.ts";
|
|
120
|
-
import {
|
|
121
|
-
import { attachChatTurnTelemetry } from "./telemetry.ts";
|
|
138
|
+
import { fetchStatementEmbedData, STATEMENT_ROW_CAP } from "./statement.ts";
|
|
139
|
+
import { attachChatTurnTelemetry, registerAppMlflowTraceInfoProcessor } from "./telemetry.ts";
|
|
122
140
|
import { invalidFields } from "./validation.ts";
|
|
123
141
|
import { inlineSkillsFromLocalMounts } from "./workspaces.ts";
|
|
124
142
|
|
|
@@ -225,6 +243,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
225
243
|
private mastra: Mastra | null = null;
|
|
226
244
|
private mastraApp: express.Express | null = null;
|
|
227
245
|
private mastraServer: MastraServer | null = null;
|
|
246
|
+
private readonly outputProcessorFactories: MastraOutputProcessorFactory[] = [];
|
|
228
247
|
/**
|
|
229
248
|
* The optional MCP server exposing this plugin's agents / tools, or
|
|
230
249
|
* `null` when `config.mcp` is disabled (the default). Built in
|
|
@@ -242,6 +261,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
242
261
|
// app boot with a clear message rather than 500-ing the first chat request.
|
|
243
262
|
this.identityMode = resolveIdentityMode(this.config.genieIdentity);
|
|
244
263
|
validateFeedbackConfig(this.config.feedback);
|
|
264
|
+
if (this.config.observability !== false) {
|
|
265
|
+
await registerAppMlflowTraceInfoProcessor();
|
|
266
|
+
}
|
|
245
267
|
// Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
|
|
246
268
|
// the lakebase pool is valid when storage/memory are enabled.
|
|
247
269
|
this.context?.onLifecycle("setup:complete", async () => {
|
|
@@ -252,8 +274,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
252
274
|
}
|
|
253
275
|
|
|
254
276
|
/**
|
|
255
|
-
* The plugin instance
|
|
256
|
-
* `req`, honoring {@link identityMode}.
|
|
277
|
+
* The plugin instance agent-backed Databricks calls should run through for
|
|
278
|
+
* `req`, honoring {@link identityMode}. Direct Genie conversation routes use
|
|
279
|
+
* AppKit OBO through `asUser(req)` instead.
|
|
257
280
|
*
|
|
258
281
|
* In `"user"` mode (the default) this is `this.asUser(req)` - AppKit's own
|
|
259
282
|
* proxy that runs every call inside `runInUserContext`, so `getExecutionContext()`
|
|
@@ -261,9 +284,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
261
284
|
* unwrapped, so the same methods run in the ambient service context and
|
|
262
285
|
* `getExecutionContext()` returns the app service principal's client - the same
|
|
263
286
|
* "just don't enter `asUser`" pattern AppKit's own service-context calls use. One
|
|
264
|
-
* helper covers
|
|
265
|
-
*
|
|
266
|
-
*
|
|
287
|
+
* helper covers the agent-backed workspace calls (serving catalogue, general
|
|
288
|
+
* suggestions, dispatched `ask_genie`, statement fetch, feedback), so the
|
|
289
|
+
* identity decision lives in one place.
|
|
267
290
|
*/
|
|
268
291
|
private scopedSelf(req: express.Request): this {
|
|
269
292
|
return useServicePrincipal(this.identityMode, req) ? this : this.asUser(req);
|
|
@@ -323,6 +346,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
323
346
|
*/
|
|
324
347
|
getDefault: (): Agent | null =>
|
|
325
348
|
(this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
|
|
349
|
+
/**
|
|
350
|
+
* Register an output-processor factory before agents are built.
|
|
351
|
+
*
|
|
352
|
+
* Deferred sibling plugins use this generic extension point without
|
|
353
|
+
* adding provider-specific behavior to the Mastra plugin.
|
|
354
|
+
*/
|
|
355
|
+
registerOutputProcessor: (factory: MastraOutputProcessorFactory): void => {
|
|
356
|
+
if (this.built) {
|
|
357
|
+
throw new Error("Mastra output processors must be registered before agents are built");
|
|
358
|
+
}
|
|
359
|
+
this.outputProcessorFactories.push(factory);
|
|
360
|
+
},
|
|
326
361
|
/** Underlying Mastra instance for advanced use (custom routes etc.). */
|
|
327
362
|
getMastra: () => this.mastra,
|
|
328
363
|
/**
|
|
@@ -441,6 +476,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
441
476
|
this.registerModelRoutes(router);
|
|
442
477
|
this.registerEmbedRoutes(router);
|
|
443
478
|
this.registerSuggestionRoutes(router);
|
|
479
|
+
this.registerGenieChatRoutes(router);
|
|
444
480
|
this.registerFeedbackRoutes(router);
|
|
445
481
|
this.registerMlflowExperimentRoute(router);
|
|
446
482
|
this.registerAgentRoutes(router);
|
|
@@ -545,10 +581,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
545
581
|
// `GET /embed/:type/:id` is the single resolver for every embed
|
|
546
582
|
// marker the agent emits in prose (`[chart:<id>]`,
|
|
547
583
|
// `[data:<id>]`, ...). `:type` selects a resolver from the
|
|
548
|
-
// registry below; `:id` is that resolver's lookup key. The
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
// client or grammar change.
|
|
584
|
+
// registry below; `:id` is that resolver's lookup key. The marker grammar
|
|
585
|
+
// is type-agnostic; the client still chooses how each registered type
|
|
586
|
+
// renders.
|
|
552
587
|
//
|
|
553
588
|
// Status codes:
|
|
554
589
|
// - 200 with the resolver's JSON body when the id resolves.
|
|
@@ -563,7 +598,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
563
598
|
// settles (`result` / `error`) or the budget elapses (then
|
|
564
599
|
// returns the still-processing entry to poll again).
|
|
565
600
|
// `?timeoutMs=<n>` (default 60s, capped 5min) tunes it.
|
|
566
|
-
// - `data`: one
|
|
601
|
+
// - `data`: one Statement Execution fetch under `genieIdentity`.
|
|
602
|
+
// - `visualization`: the same payload under mandatory OBO identity for
|
|
603
|
+
// direct Genie chat, whose conversations belong to the signed-in user.
|
|
567
604
|
// `?limit=<n>` caps rows (clamped to STATEMENT_ROW_CAP).
|
|
568
605
|
//
|
|
569
606
|
// Built once (this handler is registered once) and keyed by the
|
|
@@ -587,6 +624,13 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
587
624
|
signal,
|
|
588
625
|
});
|
|
589
626
|
},
|
|
627
|
+
visualization: (req, id, signal) => {
|
|
628
|
+
const limit = parseStatementLimit(req.query.limit);
|
|
629
|
+
return this.asUser(req).fetchStatement(id, {
|
|
630
|
+
...(limit !== undefined ? { limit } : {}),
|
|
631
|
+
signal,
|
|
632
|
+
});
|
|
633
|
+
},
|
|
590
634
|
};
|
|
591
635
|
|
|
592
636
|
this.route(router, {
|
|
@@ -619,13 +663,10 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
619
663
|
res.status(400).json({ error: "id is required" });
|
|
620
664
|
return;
|
|
621
665
|
}
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
// unblocks any long-poll immediately and frees the request
|
|
625
|
-
// thread. The listener is GC'd with the request on normal
|
|
626
|
-
// completion.
|
|
666
|
+
// Bridge response close onto an AbortSignal so a client disconnect
|
|
667
|
+
// releases long-poll and workspace calls immediately.
|
|
627
668
|
const controller = new AbortController();
|
|
628
|
-
|
|
669
|
+
res.on("close", () => controller.abort());
|
|
629
670
|
const result = await resolve(req, id, controller.signal);
|
|
630
671
|
if (controller.signal.aborted) return;
|
|
631
672
|
if (!result.ok) {
|
|
@@ -663,14 +704,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
663
704
|
// empty state (no built-in example prompts). The `:agentId`
|
|
664
705
|
// segment is accepted for URL symmetry with the chat / history
|
|
665
706
|
// routes; Genie spaces are resolved per-plugin, not per-agent,
|
|
666
|
-
// so it doesn't change the result.
|
|
667
|
-
//
|
|
707
|
+
// so it doesn't change the result. The space lookup uses the configured
|
|
708
|
+
// agent-backed identity.
|
|
668
709
|
const handleSuggestions = async (
|
|
669
710
|
req: express.Request,
|
|
670
711
|
res: express.Response,
|
|
671
712
|
): Promise<void> => {
|
|
672
713
|
const controller = new AbortController();
|
|
673
|
-
|
|
714
|
+
res.on("close", () => controller.abort());
|
|
674
715
|
const result = await this.scopedSelf(req).fetchSuggestions(controller.signal);
|
|
675
716
|
if (controller.signal.aborted) return;
|
|
676
717
|
if (!result.ok) {
|
|
@@ -700,6 +741,189 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
700
741
|
});
|
|
701
742
|
}
|
|
702
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Register the direct Genie chat routes. Each route is scoped to one Genie
|
|
746
|
+
* space alias and sits directly on the Genie APIs under AppKit's OBO user
|
|
747
|
+
* context: conversations are threads, Genie messages are history, and a turn
|
|
748
|
+
* streams the same AI SDK chunks as an agent's `ask_genie` call. Nothing is
|
|
749
|
+
* written to Mastra Memory.
|
|
750
|
+
*/
|
|
751
|
+
private registerGenieChatRoutes(router: IAppRouter): void {
|
|
752
|
+
const path = (...parts: string[]) => routes.genieRoutePath(":alias", ...parts);
|
|
753
|
+
const { chat: chatSegment, messages, suggestions, threads } = routes.MASTRA_ROUTES;
|
|
754
|
+
/** Resolve the route's alias, answering 404 when no such space is configured. */
|
|
755
|
+
const spaceFor = (req: express.Request, res: express.Response): GenieChatSpace | undefined => {
|
|
756
|
+
const alias = stringUtils.firstNonEmpty(req.params.alias) ?? "";
|
|
757
|
+
const configured = resolveGenieSpaces(this.config, this.context)[alias];
|
|
758
|
+
if (configured) return { alias, spaceId: configured.spaceId };
|
|
759
|
+
res.status(404).json({ error: `Unknown Genie space alias "${alias}"` });
|
|
760
|
+
return undefined;
|
|
761
|
+
};
|
|
762
|
+
/** Bridge the connection `close` event onto an abort signal. */
|
|
763
|
+
const closeSignal = (res: express.Response): AbortSignal => {
|
|
764
|
+
const controller = new AbortController();
|
|
765
|
+
res.on("close", () => controller.abort());
|
|
766
|
+
return controller.signal;
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
this.route(router, {
|
|
770
|
+
name: "genieChat",
|
|
771
|
+
method: "post",
|
|
772
|
+
path: path(chatSegment),
|
|
773
|
+
handler: async (req, res) => {
|
|
774
|
+
const space = spaceFor(req, res);
|
|
775
|
+
if (!space) return;
|
|
776
|
+
const parsed = wire.GenieChatRequestSchema.safeParse(req.body);
|
|
777
|
+
if (!parsed.success) {
|
|
778
|
+
res
|
|
779
|
+
.status(400)
|
|
780
|
+
.json({ error: "question is required", fields: invalidFields(parsed.error) });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
await this.asUser(req).streamGenieChat(res, space, parsed.data, closeSignal(res));
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
this.route(router, {
|
|
787
|
+
name: "genieThreads",
|
|
788
|
+
method: "get",
|
|
789
|
+
path: path(threads),
|
|
790
|
+
handler: async (req, res) => {
|
|
791
|
+
const space = spaceFor(req, res);
|
|
792
|
+
if (!space) return;
|
|
793
|
+
const result = await this.asUser(req).genieConversationRead((client) =>
|
|
794
|
+
listGenieThreads(client, space),
|
|
795
|
+
);
|
|
796
|
+
if (!result.ok) {
|
|
797
|
+
this.sendFailure(res, result, "genie-threads", "Could not list Genie conversations");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
res.json({ threads: result.data });
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
this.route(router, {
|
|
804
|
+
name: "genieThreadMessages",
|
|
805
|
+
method: "get",
|
|
806
|
+
path: path(threads, ":threadId", messages),
|
|
807
|
+
handler: async (req, res) => {
|
|
808
|
+
const space = spaceFor(req, res);
|
|
809
|
+
if (!space) return;
|
|
810
|
+
const threadId = stringUtils.firstNonEmpty(req.params.threadId) ?? "";
|
|
811
|
+
const result = await this.asUser(req).genieConversationRead((client) =>
|
|
812
|
+
listGenieThreadMessages({
|
|
813
|
+
client,
|
|
814
|
+
space,
|
|
815
|
+
conversationId: threadId,
|
|
816
|
+
}),
|
|
817
|
+
);
|
|
818
|
+
if (!result.ok) {
|
|
819
|
+
this.sendFailure(res, result, "genie-messages", "Could not load the Genie conversation");
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
res.json({ messages: result.data });
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
this.route(router, {
|
|
826
|
+
name: "genieThreadDelete",
|
|
827
|
+
method: "delete",
|
|
828
|
+
path: path(threads, ":threadId"),
|
|
829
|
+
handler: async (req, res) => {
|
|
830
|
+
const space = spaceFor(req, res);
|
|
831
|
+
if (!space) return;
|
|
832
|
+
const threadId = stringUtils.firstNonEmpty(req.params.threadId) ?? "";
|
|
833
|
+
const result = await this.asUser(req).genieConversationWrite((client) =>
|
|
834
|
+
deleteGenieThread(client, space, threadId),
|
|
835
|
+
);
|
|
836
|
+
if (!result.ok) {
|
|
837
|
+
this.sendFailure(
|
|
838
|
+
res,
|
|
839
|
+
result,
|
|
840
|
+
"genie-thread-delete",
|
|
841
|
+
"Could not delete the Genie conversation",
|
|
842
|
+
);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
res.status(204).end();
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
this.route(router, {
|
|
849
|
+
name: "genieThreadMessageDelete",
|
|
850
|
+
method: "delete",
|
|
851
|
+
path: path(threads, ":threadId", messages, ":messageId"),
|
|
852
|
+
handler: async (req, res) => {
|
|
853
|
+
const space = spaceFor(req, res);
|
|
854
|
+
if (!space) return;
|
|
855
|
+
const threadId = stringUtils.firstNonEmpty(req.params.threadId) ?? "";
|
|
856
|
+
const messageId = stringUtils.firstNonEmpty(req.params.messageId) ?? "";
|
|
857
|
+
const result = await this.asUser(req).genieConversationWrite((client) =>
|
|
858
|
+
deleteGenieThreadMessage(client, space, threadId, messageId),
|
|
859
|
+
);
|
|
860
|
+
if (!result.ok) {
|
|
861
|
+
this.sendFailure(
|
|
862
|
+
res,
|
|
863
|
+
result,
|
|
864
|
+
"genie-message-delete",
|
|
865
|
+
"Could not delete the Genie message",
|
|
866
|
+
);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
res.status(204).end();
|
|
870
|
+
},
|
|
871
|
+
});
|
|
872
|
+
this.route(router, {
|
|
873
|
+
name: "genieSuggestions",
|
|
874
|
+
method: "get",
|
|
875
|
+
path: path(suggestions),
|
|
876
|
+
handler: async (req, res) => {
|
|
877
|
+
const space = spaceFor(req, res);
|
|
878
|
+
if (!space) return;
|
|
879
|
+
const result = await this.asUser(req).fetchSuggestions(undefined, {
|
|
880
|
+
[space.alias]: { spaceId: space.spaceId },
|
|
881
|
+
});
|
|
882
|
+
res.json({ questions: result.ok ? result.data : [] });
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Stream one direct Genie turn to `res` under AppKit's OBO user context.
|
|
889
|
+
* Direct conversations cannot use the shared service principal because Genie
|
|
890
|
+
* owns them under the Databricks caller.
|
|
891
|
+
*/
|
|
892
|
+
private async streamGenieChat(
|
|
893
|
+
res: express.Response,
|
|
894
|
+
space: GenieChatSpace,
|
|
895
|
+
request: GenieChatRequest,
|
|
896
|
+
signal: AbortSignal,
|
|
897
|
+
): Promise<void> {
|
|
898
|
+
const context = getExecutionContext();
|
|
899
|
+
const client = context.client;
|
|
900
|
+
await pipeUIMessageStreamToResponse({
|
|
901
|
+
response: res,
|
|
902
|
+
stream: genieChatStream({
|
|
903
|
+
client,
|
|
904
|
+
space,
|
|
905
|
+
request,
|
|
906
|
+
signal,
|
|
907
|
+
}),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** Run one Genie conversation read under the active request identity. */
|
|
912
|
+
private genieConversationRead<T>(
|
|
913
|
+
read: (client: ReturnType<typeof getExecutionContext>["client"]) => Promise<T>,
|
|
914
|
+
): Promise<ExecutionResult<T>> {
|
|
915
|
+
const client = getExecutionContext().client;
|
|
916
|
+
return this.execute(() => read(client), genieConversationReadDefaults);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Run one Genie conversation write under the active request identity. */
|
|
920
|
+
private genieConversationWrite(
|
|
921
|
+
write: (client: ReturnType<typeof getExecutionContext>["client"]) => Promise<void>,
|
|
922
|
+
): Promise<ExecutionResult<void>> {
|
|
923
|
+
const client = getExecutionContext().client;
|
|
924
|
+
return this.execute(() => write(client), genieConversationWriteDefaults);
|
|
925
|
+
}
|
|
926
|
+
|
|
703
927
|
/** Register MLflow feedback submission when configured. */
|
|
704
928
|
private registerFeedbackRoutes(router: IAppRouter): void {
|
|
705
929
|
// `POST /route/feedback` logs a thumbs / comment assessment against
|
|
@@ -866,11 +1090,15 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
866
1090
|
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
867
1091
|
* OBO-scoped client. Resolves the plugin's Genie spaces and merges
|
|
868
1092
|
* their curated `sample_questions` (see {@link collectSpaceSuggestions}).
|
|
869
|
-
*
|
|
870
|
-
*
|
|
1093
|
+
* `only` narrows the lookup to the given spaces (the direct Genie chat
|
|
1094
|
+
* passes its own). Returns `[]` when no Genie space is configured so the
|
|
1095
|
+
* client shows a bare empty state instead of built-in example prompts.
|
|
871
1096
|
*/
|
|
872
|
-
private async fetchSuggestions(
|
|
873
|
-
|
|
1097
|
+
private async fetchSuggestions(
|
|
1098
|
+
signal?: AbortSignal,
|
|
1099
|
+
only?: Record<string, GenieSpaceConfig>,
|
|
1100
|
+
): Promise<ExecutionResult<string[]>> {
|
|
1101
|
+
const spaces = only ?? resolveGenieSpaces(this.config, this.context);
|
|
874
1102
|
if (Object.keys(spaces).length === 0) return { ok: true, data: [] };
|
|
875
1103
|
const client = getExecutionContext().client;
|
|
876
1104
|
return this.execute((executeSignal) => {
|
|
@@ -942,7 +1170,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
942
1170
|
async (executeSignal) => {
|
|
943
1171
|
const combined = asyncUtils.combineAbortSignals(options.signal, executeSignal);
|
|
944
1172
|
try {
|
|
945
|
-
const data = await
|
|
1173
|
+
const { data, visualization } = await fetchStatementEmbedData(client, statementId, {
|
|
946
1174
|
limit,
|
|
947
1175
|
...(combined ? { signal: combined } : {}),
|
|
948
1176
|
});
|
|
@@ -951,6 +1179,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
951
1179
|
rows: data.rows,
|
|
952
1180
|
rowCount: data.rowCount,
|
|
953
1181
|
truncated: data.rows.length < data.rowCount,
|
|
1182
|
+
visualization,
|
|
954
1183
|
};
|
|
955
1184
|
} catch (err) {
|
|
956
1185
|
// The Databricks SDK throws on 404; surface as `undefined`
|
|
@@ -1045,7 +1274,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1045
1274
|
pluginRegistry.require(this.context, lakebase, this.config).exports().pool,
|
|
1046
1275
|
) as Pool)
|
|
1047
1276
|
: undefined;
|
|
1048
|
-
const memoryBuilder =
|
|
1277
|
+
const memoryBuilder = createMemoryBuilder(this.config, memoryPool);
|
|
1049
1278
|
|
|
1050
1279
|
// Build every agent declared in `config.agents` (or the built-in
|
|
1051
1280
|
// fallback when none are declared). Each agent's `model` resolves
|
|
@@ -1080,6 +1309,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1080
1309
|
config: this.config,
|
|
1081
1310
|
context: this.context,
|
|
1082
1311
|
memoryBuilder,
|
|
1312
|
+
outputProcessorFactories: this.outputProcessorFactories,
|
|
1083
1313
|
log: this.logger,
|
|
1084
1314
|
extraSkillMounts: localSkillMounts,
|
|
1085
1315
|
standardSkills,
|
|
@@ -1106,6 +1336,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1106
1336
|
const observability = await buildObservability({
|
|
1107
1337
|
serviceName: this.name,
|
|
1108
1338
|
enabled: this.config.observability,
|
|
1339
|
+
tracerProvider: await appkitTelemetry.deferredNativeTracerProvider(),
|
|
1109
1340
|
});
|
|
1110
1341
|
// Optional MCP exposure: build a Mastra MCP server from the
|
|
1111
1342
|
// registered agents (and, opt-in, the ambient tools) and register
|
|
@@ -1151,7 +1382,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1151
1382
|
backgroundTurns: this.config.backgroundTurns !== false,
|
|
1152
1383
|
apiAccess: this.config.apiAccess ?? "scoped",
|
|
1153
1384
|
mcp: this.mcp ? `${this.basePath}${this.mcp.httpPath}` : "off",
|
|
1154
|
-
lakebase:
|
|
1385
|
+
lakebase: memoryPool !== undefined,
|
|
1155
1386
|
feedback: this.feedbackEnabled(),
|
|
1156
1387
|
observability: observability !== undefined ? "mlflow" : "off",
|
|
1157
1388
|
});
|
package/src/server.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type express from "express";
|
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
attributedUserId,
|
|
23
|
+
MASTRA_AUTH_MODE_KEY,
|
|
23
24
|
MASTRA_REQUEST_ID_KEY,
|
|
24
25
|
MASTRA_SCOPES_KEY,
|
|
25
26
|
MASTRA_USER_EMAIL_KEY,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
} from "./config.ts";
|
|
31
32
|
import { requestUserEmail, requestUserId } from "./identity.ts";
|
|
32
33
|
import { resolveFeedbackEnabled } from "./mlflow.ts";
|
|
34
|
+
import { MASTRA_ORIGIN_KEY } from "./request-context.ts";
|
|
33
35
|
import {
|
|
34
36
|
extractModelOverride,
|
|
35
37
|
MASTRA_MODEL_OVERRIDE_KEY,
|
|
@@ -51,7 +53,9 @@ const TRUSTED_REQUEST_CONTEXT_KEYS = [
|
|
|
51
53
|
MASTRA_USER_KEY,
|
|
52
54
|
MASTRA_USER_NAME_KEY,
|
|
53
55
|
MASTRA_USER_EMAIL_KEY,
|
|
56
|
+
MASTRA_AUTH_MODE_KEY,
|
|
54
57
|
MASTRA_REQUEST_ID_KEY,
|
|
58
|
+
MASTRA_ORIGIN_KEY,
|
|
55
59
|
MASTRA_SCOPES_KEY,
|
|
56
60
|
MASTRA_MODEL_OVERRIDE_KEY,
|
|
57
61
|
MASTRA_RESOLVED_MODEL_KEY,
|
|
@@ -64,6 +68,29 @@ export function clearTrustedRequestContext(requestContext: RequestContext): void
|
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
/** Resolve the canonical browser origin from standard proxy and browser headers. */
|
|
72
|
+
export function requestOrigin(req: express.Request): string | undefined {
|
|
73
|
+
for (const value of [req.get("origin"), req.get("referer")]) {
|
|
74
|
+
if (!value) continue;
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(value);
|
|
77
|
+
if (url.protocol === "http:" || url.protocol === "https:") return url.origin;
|
|
78
|
+
} catch {
|
|
79
|
+
// Continue to proxy headers.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const host = req.get("x-forwarded-host")?.split(",", 1)[0]?.trim() || req.get("host")?.trim();
|
|
83
|
+
if (!host) return undefined;
|
|
84
|
+
const protocol =
|
|
85
|
+
req.get("x-forwarded-proto")?.split(",", 1)[0]?.trim() || req.protocol || "https";
|
|
86
|
+
if (protocol !== "http" && protocol !== "https") return undefined;
|
|
87
|
+
try {
|
|
88
|
+
return new URL(`${protocol}://${host}`).origin;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
67
94
|
/**
|
|
68
95
|
* Who a turn is ATTRIBUTED to, independent of which Databricks credential runs
|
|
69
96
|
* its calls. Supplied by the HTTP middleware from the forwarded user headers so
|
|
@@ -116,6 +143,7 @@ export async function stampRequestContextUser(
|
|
|
116
143
|
};
|
|
117
144
|
requestContext.set(MASTRA_USER_KEY, user);
|
|
118
145
|
requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
|
|
146
|
+
requestContext.set(MASTRA_AUTH_MODE_KEY, isUserContext ? "obo" : "service-principal");
|
|
119
147
|
// AppKit's `UserContext` surfaces display name / email only on
|
|
120
148
|
// OBO requests. Service-context calls (background tasks, server
|
|
121
149
|
// start-up) leave these undefined and we skip the stamp so
|
|
@@ -197,6 +225,7 @@ export class MastraServer extends ExpressMastraServer {
|
|
|
197
225
|
this.configureRequestContextThreadId(req, res, requestContext);
|
|
198
226
|
this.configureRequestContextModelOverride(req, requestContext);
|
|
199
227
|
this.configureRequestContextRequestId(req, res, requestContext);
|
|
228
|
+
this.configureRequestContextOrigin(req, requestContext);
|
|
200
229
|
this.configureRequestContextScopes(req, requestContext);
|
|
201
230
|
|
|
202
231
|
this.configureMlflowTraceId(res);
|
|
@@ -238,6 +267,12 @@ export class MastraServer extends ExpressMastraServer {
|
|
|
238
267
|
recordActiveTraceUser(traceUser);
|
|
239
268
|
}
|
|
240
269
|
|
|
270
|
+
/** Stamp the public browser origin so plugin tools can return canonical app links. */
|
|
271
|
+
configureRequestContextOrigin(req: express.Request, requestContext: RequestContext): void {
|
|
272
|
+
const origin = requestOrigin(req);
|
|
273
|
+
if (origin) requestContext.set(MASTRA_ORIGIN_KEY, origin);
|
|
274
|
+
}
|
|
275
|
+
|
|
241
276
|
/**
|
|
242
277
|
* Stamp a per-request id and echo it on the response so an upstream
|
|
243
278
|
* proxy / curl client / browser-side log line can pair its view of
|
package/src/statement.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import type { WorkspaceClient } from "@databricks/appkit";
|
|
23
23
|
import { databricks } from "@dbx-tools/appkit";
|
|
24
24
|
import { object } from "@dbx-tools/shared-core";
|
|
25
|
-
import type
|
|
25
|
+
import { wire, type GenieDatasetData, type GenieVisualizationData } from "@dbx-tools/shared-mastra";
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Hard server-side cap on rows returned by the
|
|
@@ -48,6 +48,60 @@ function coerceCell(cell: string | null): unknown {
|
|
|
48
48
|
return object.toNumber(cell, { separators: false, percent: false }) ?? cell;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
type StatementResponse = Awaited<ReturnType<WorkspaceClient["statementExecution"]["getStatement"]>>;
|
|
52
|
+
|
|
53
|
+
async function fetchStatementResponse(
|
|
54
|
+
client: WorkspaceClient,
|
|
55
|
+
statementId: string,
|
|
56
|
+
signal?: AbortSignal,
|
|
57
|
+
): Promise<StatementResponse> {
|
|
58
|
+
const context = signal ? databricks.toContext(signal) : undefined;
|
|
59
|
+
return client.statementExecution.getStatement({ statement_id: statementId }, context);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizedStatementData(response: StatementResponse, limit?: number): GenieDatasetData {
|
|
63
|
+
const columns = (response.manifest?.schema?.columns ?? []).map((column) => column.name ?? "");
|
|
64
|
+
const dataArray = (response.result?.data_array ?? []) as Array<Array<string | null>>;
|
|
65
|
+
const sliced = limit !== undefined && limit >= 0 ? dataArray.slice(0, limit) : dataArray;
|
|
66
|
+
const rows = sliced.map((row) => {
|
|
67
|
+
const record: Record<string, unknown> = {};
|
|
68
|
+
columns.forEach((column, index) => {
|
|
69
|
+
record[column] = coerceCell(row[index] ?? null);
|
|
70
|
+
});
|
|
71
|
+
return record;
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
columns,
|
|
75
|
+
rows,
|
|
76
|
+
rowCount: response.manifest?.total_row_count ?? dataArray.length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function visualizationData(response: StatementResponse, limit?: number): GenieVisualizationData {
|
|
81
|
+
const dataArray = (response.result?.data_array ?? []) as Array<Array<string | null>>;
|
|
82
|
+
return wire.GenieVisualizationDataSchema.parse({
|
|
83
|
+
manifest: {
|
|
84
|
+
schema: {
|
|
85
|
+
columns: (response.manifest?.schema?.columns ?? []).map((column) => ({
|
|
86
|
+
name: column.name,
|
|
87
|
+
type_name: column.type_name,
|
|
88
|
+
})),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
result: {
|
|
92
|
+
data_array: limit !== undefined && limit >= 0 ? dataArray.slice(0, limit) : dataArray,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Normalized and AppKit-native projections of one statement response. */
|
|
98
|
+
export interface StatementEmbedData {
|
|
99
|
+
/** Row records used by tools, sortable tables, and chart planners. */
|
|
100
|
+
data: GenieDatasetData;
|
|
101
|
+
/** Manifest and row arrays consumed by AppKit UI's Genie visualization. */
|
|
102
|
+
visualization: GenieVisualizationData;
|
|
103
|
+
}
|
|
104
|
+
|
|
51
105
|
/**
|
|
52
106
|
* Fetch a single statement's rows via the Statement Execution API
|
|
53
107
|
* and reshape into the shared {@link GenieDatasetData} shape
|
|
@@ -70,24 +124,22 @@ export async function fetchStatementData(
|
|
|
70
124
|
statementId: string,
|
|
71
125
|
options?: { limit?: number; signal?: AbortSignal },
|
|
72
126
|
): Promise<GenieDatasetData> {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
127
|
+
const response = await fetchStatementResponse(client, statementId, options?.signal);
|
|
128
|
+
return normalizedStatementData(response, options?.limit);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Fetch one statement once and project it for both the existing data table and
|
|
133
|
+
* AppKit UI's native Genie visualization.
|
|
134
|
+
*/
|
|
135
|
+
export async function fetchStatementEmbedData(
|
|
136
|
+
client: WorkspaceClient,
|
|
137
|
+
statementId: string,
|
|
138
|
+
options?: { limit?: number; signal?: AbortSignal },
|
|
139
|
+
): Promise<StatementEmbedData> {
|
|
140
|
+
const response = await fetchStatementResponse(client, statementId, options?.signal);
|
|
88
141
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
rowCount: r.manifest?.total_row_count ?? dataArray.length,
|
|
142
|
+
data: normalizedStatementData(response, options?.limit),
|
|
143
|
+
visualization: visualizationData(response, options?.limit),
|
|
92
144
|
};
|
|
93
145
|
}
|