@dbx-tools/appkit-mastra 0.9.131 → 0.9.136

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/src/plugin.ts CHANGED
@@ -57,6 +57,7 @@ import {
57
57
  type ResourceRequirement,
58
58
  } from "@databricks/appkit";
59
59
  import { lakebaseResolver, pluginRegistry, telemetry as appkitTelemetry } from "@dbx-tools/appkit";
60
+ import { sendGenieMessageFeedback } from "@dbx-tools/genie/feedback";
60
61
  import { modelCatalog } from "@dbx-tools/model";
61
62
  import { asyncUtils, errorUtils, log, object, stringUtils } from "@dbx-tools/shared-core";
62
63
  import {
@@ -66,13 +67,14 @@ import {
66
67
  type Chart,
67
68
  type GenieChatRequest,
68
69
  type MastraClientConfig,
69
- type MastraFeedbackRequest,
70
70
  type StatementData,
71
71
  } from "@dbx-tools/shared-mastra";
72
72
  import { display, type ServingEndpointSummary } from "@dbx-tools/shared-model";
73
+ import { workflowRoute } from "@mastra/ai-sdk";
73
74
  import type { Agent } from "@mastra/core/agent";
74
75
  import { Mastra } from "@mastra/core/mastra";
75
- import type { RequestContext } from "@mastra/core/request-context";
76
+ import { RequestContext } from "@mastra/core/request-context";
77
+ import type { AnyWorkflow } from "@mastra/core/workflows";
76
78
  import { pipeUIMessageStreamToResponse } from "ai";
77
79
  import express from "express";
78
80
  import type { Pool } from "pg";
@@ -100,6 +102,7 @@ import {
100
102
  modelCatalogueDefaults,
101
103
  statementDataDefaults,
102
104
  } from "./defaults.ts";
105
+ import { resolveFeedbackMode } from "./feedback-mode.ts";
103
106
  import {
104
107
  deleteGenieThread,
105
108
  deleteGenieThreadMessage,
@@ -120,9 +123,8 @@ import { createMemoryBuilder, needsLakebase } from "./memory.ts";
120
123
  import {
121
124
  flushDirectMlflowTracing,
122
125
  logFeedback,
126
+ mlflowEnabled,
123
127
  mlflowExperimentManagerUrl,
124
- resolveFeedbackEnabled,
125
- validateFeedbackConfig,
126
128
  } from "./mlflow.ts";
127
129
  import { resolveDefaultModelId } from "./model.ts";
128
130
  import { buildObservability, configureOtelPropagation } from "./observability.ts";
@@ -130,9 +132,11 @@ import { provisionRemoteSkills } from "./remote-skills.ts";
130
132
  import {
131
133
  attachRoutePatchMiddleware,
132
134
  attachStaleResumeRecovery,
135
+ clearTrustedRequestContext,
133
136
  createRequestContext,
134
137
  isMastraRequestAllowed,
135
138
  MastraServer,
139
+ stampRequestContextUser,
136
140
  } from "./server.ts";
137
141
  import { resolveServingConfig } from "./serving.ts";
138
142
  import { fetchStatementEmbedData, STATEMENT_ROW_CAP } from "./statement.ts";
@@ -244,12 +248,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
244
248
  private mastraApp: express.Express | null = null;
245
249
  private mastraServer: MastraServer | null = null;
246
250
  private readonly outputProcessorFactories: MastraOutputProcessorFactory[] = [];
251
+ /** Code-defined workflows registered before the Mastra instance is built. */
252
+ private readonly workflows: Record<string, AnyWorkflow> = {
253
+ ...(this.config.workflows ?? {}),
254
+ };
247
255
  /**
248
256
  * The optional MCP server exposing this plugin's agents / tools, or
249
257
  * `null` when `config.mcp` is disabled (the default). Built in
250
258
  * {@link buildAgentAndServer} and registered on the Mastra instance.
251
259
  */
252
260
  private mcp: ResolvedMcp | null = null;
261
+ /** Resolved feedback destination published to server and browser clients. */
262
+ private resolvedFeedbackMode: feedback.FeedbackMode = "none";
253
263
  /**
254
264
  * Resolved {@link MastraIdentityMode}: the identity the chat path's Databricks
255
265
  * calls run as. Read once at setup so an invalid `genieIdentity` fails the app
@@ -260,7 +270,12 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
260
270
  // Resolve the identity mode up front so an invalid `genieIdentity` fails the
261
271
  // app boot with a clear message rather than 500-ing the first chat request.
262
272
  this.identityMode = resolveIdentityMode(this.config.genieIdentity);
263
- validateFeedbackConfig(this.config.feedback);
273
+ const spaces = resolveGenieSpaces(this.config, this.context);
274
+ this.resolvedFeedbackMode = resolveFeedbackMode(this.config.feedbackMode, {
275
+ genie: this.config.genieAgentMode !== false && Object.keys(spaces).length > 0,
276
+ mlflow: mlflowEnabled(),
277
+ });
278
+ this.config.feedbackMode = this.resolvedFeedbackMode;
264
279
  if (this.config.observability !== false) {
265
280
  await registerAppMlflowTraceInfoProcessor();
266
281
  }
@@ -358,6 +373,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
358
373
  }
359
374
  this.outputProcessorFactories.push(factory);
360
375
  },
376
+ /** Register a workflow before the hosted Mastra instance is built. */
377
+ registerWorkflow: (key: string, workflow: AnyWorkflow): void => {
378
+ if (this.mastra) {
379
+ throw new Error("Mastra workflows must be registered before the instance is built");
380
+ }
381
+ if (this.workflows[key]) {
382
+ throw new Error(`Mastra workflow key "${key}" is already registered`);
383
+ }
384
+ this.workflows[key] = workflow;
385
+ },
386
+ /** Registered workflow keys in insertion order. */
387
+ listWorkflows: (): string[] => Object.keys(this.workflows),
361
388
  /** Underlying Mastra instance for advanced use (custom routes etc.). */
362
389
  getMastra: () => this.mastra,
363
390
  /**
@@ -452,7 +479,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
452
479
  basePath: this.basePath,
453
480
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
454
481
  agents: Object.keys(this.built?.agents ?? {}),
455
- feedbackEnabled: this.feedbackEnabled(),
482
+ feedbackMode: this.feedbackMode(),
456
483
  backgroundTurns: this.config.backgroundTurns !== false,
457
484
  // In `service-principal` mode the chat works for any caller who can open
458
485
  // the app, so a UI that hides chat behind an OBO-token probe should show
@@ -462,13 +489,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
462
489
  return config as unknown as Record<string, unknown>;
463
490
  }
464
491
 
465
- /**
466
- * Whether user feedback can be logged to MLflow. Delegates to
467
- * {@link resolveFeedbackEnabled} so the client-config flag and the
468
- * feedback route share the same gate as the server's trace-id header.
469
- */
470
- private feedbackEnabled(): boolean {
471
- return resolveFeedbackEnabled(this.config.feedback);
492
+ /** Resolve the only feedback destination exposed for this plugin mode. */
493
+ private feedbackMode(): feedback.FeedbackMode {
494
+ return this.resolvedFeedbackMode;
472
495
  }
473
496
 
474
497
  override injectRoutes(router: IAppRouter): void {
@@ -897,12 +920,29 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
897
920
  ): Promise<void> {
898
921
  const context = getExecutionContext();
899
922
  const client = context.client;
923
+ const requestContext = new RequestContext<unknown>(
924
+ Object.entries(request.requestContext ?? {}),
925
+ );
926
+ clearTrustedRequestContext(requestContext);
927
+ const applicationContext = requestContext.toJSON();
928
+ await stampRequestContextUser(requestContext);
929
+ const memory = await this.built?.agents[this.built.defaultAgentId]?.getMemory({
930
+ requestContext,
931
+ });
900
932
  await pipeUIMessageStreamToResponse({
901
933
  response: res,
902
934
  stream: genieChatStream({
903
935
  client,
904
936
  space,
905
- request,
937
+ request: {
938
+ question: request.question,
939
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
940
+ ...(Object.keys(applicationContext).length > 0
941
+ ? { requestContext: applicationContext }
942
+ : {}),
943
+ },
944
+ ...(memory ? { memory } : {}),
945
+ requestContext,
906
946
  signal,
907
947
  }),
908
948
  });
@@ -924,24 +964,15 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
924
964
  return this.execute(() => write(client), genieConversationWriteDefaults);
925
965
  }
926
966
 
927
- /** Register MLflow feedback submission when configured. */
967
+ /** Register the single feedback destination selected by plugin mode. */
928
968
  private registerFeedbackRoutes(router: IAppRouter): void {
929
- // `POST /route/feedback` logs a thumbs / comment assessment against
930
- // a turn's MLflow trace (the `traceId` the client captured from the
931
- // stream response's trace-id header). Registered on the AppKit
932
- // router (like `/models`) rather than the Mastra subapp so it runs
933
- // under the same OBO scope - the feedback is attributed to the
934
- // signed-in user. Returns 404 when feedback is disabled so the
935
- // client treats the capability as absent; 400 on a malformed body.
936
- // A recorded assessment yields `{ ok: true }`; a soft failure (most
937
- // often the trace hasn't finished exporting to MLflow yet) yields
938
- // `{ ok: false }` without a 5xx so the UI can prompt a retry.
939
969
  this.route(router, {
940
970
  name: "feedback",
941
971
  method: "post",
942
972
  path: routes.MASTRA_ROUTES.feedback,
943
973
  handler: async (req, res) => {
944
- if (!this.feedbackEnabled()) {
974
+ const mode = this.feedbackMode();
975
+ if (mode === "none") {
945
976
  res.status(404).json({ ok: false });
946
977
  return;
947
978
  }
@@ -957,6 +988,19 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
957
988
  });
958
989
  return;
959
990
  }
991
+ if (parsed.data.mode !== mode) {
992
+ res.status(400).json({ ok: false, error: INVALID_FEEDBACK_MESSAGE });
993
+ return;
994
+ }
995
+ if (parsed.data.mode === "genie") {
996
+ const result = await this.scopedSelf(req).sendGenieFeedback(parsed.data);
997
+ if (!result.ok) {
998
+ this.sendFailure(res, result, "genie-feedback", "Feedback could not be recorded");
999
+ return;
1000
+ }
1001
+ res.json({ ok: true });
1002
+ return;
1003
+ }
960
1004
  const result = await this.scopedSelf(req).logFeedback(parsed.data);
961
1005
  if (!result.ok) {
962
1006
  this.sendFailure(res, result, "feedback", "Feedback could not be recorded");
@@ -978,7 +1022,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
978
1022
  method: "get",
979
1023
  path: routes.MASTRA_ROUTES.mlflowExperiment,
980
1024
  handler: async (req, res) => {
981
- if (!this.feedbackEnabled()) {
1025
+ if (this.feedbackMode() !== "mlflow") {
982
1026
  res.json({ url: null });
983
1027
  return;
984
1028
  }
@@ -1120,7 +1164,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1120
1164
  * (see {@link logFeedback} in `./mlflow.js`).
1121
1165
  */
1122
1166
  private async logFeedback(
1123
- feedback: MastraFeedbackRequest,
1167
+ feedback: feedback.MlflowFeedbackRequest,
1124
1168
  ): Promise<ExecutionResult<string | undefined>> {
1125
1169
  const ctx = getExecutionContext();
1126
1170
  const sourceId =
@@ -1139,6 +1183,28 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1139
1183
  );
1140
1184
  }
1141
1185
 
1186
+ /** Send Agent Mode rating and comment directly to the originating Genie message. */
1187
+ private sendGenieFeedback(input: feedback.GenieFeedbackRequest): Promise<ExecutionResult<void>> {
1188
+ const client = getExecutionContext().client;
1189
+ return this.execute(
1190
+ (signal) =>
1191
+ sendGenieMessageFeedback(
1192
+ {
1193
+ space_id: input.spaceId,
1194
+ conversation_id: input.conversationId,
1195
+ message_id: input.messageId,
1196
+ rating: input.rating,
1197
+ ...(input.comment ? { comment: input.comment } : {}),
1198
+ },
1199
+ {
1200
+ workspaceClient: client,
1201
+ ...(signal ? { context: signal } : {}),
1202
+ },
1203
+ ),
1204
+ feedbackWriteDefaults,
1205
+ );
1206
+ }
1207
+
1142
1208
  /**
1143
1209
  * Resolve an experiment link under the active request identity. The helper
1144
1210
  * returns no URL unless the viewer has effective `CAN_MANAGE` permission.
@@ -1353,6 +1419,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1353
1419
  });
1354
1420
  this.mastra = new Mastra({
1355
1421
  agents: this.built.agents,
1422
+ ...(Object.keys(this.workflows).length > 0 ? { workflows: this.workflows } : {}),
1356
1423
  ...(instanceStorage ? { storage: instanceStorage } : {}),
1357
1424
  ...(observability ? { observability } : {}),
1358
1425
  ...(this.mcp ? { mcpServers: { [this.mcp.serverId]: this.mcp.server } } : {}),
@@ -1372,18 +1439,25 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1372
1439
  app: this.mastraApp,
1373
1440
  mastra: this.mastra,
1374
1441
  prefix: "",
1375
- customApiRoutes: agentChatRoutes(),
1442
+ customApiRoutes: [
1443
+ ...agentChatRoutes(),
1444
+ workflowRoute({
1445
+ path: "/route/workflows/:workflowId/stream",
1446
+ version: "v7",
1447
+ }),
1448
+ ],
1376
1449
  });
1377
1450
  await this.mastraServer.init();
1378
1451
  attachStaleResumeRecovery(this.mastraApp);
1379
1452
  this.logger.info("ready", {
1380
1453
  agents: Object.keys(this.built.agents),
1454
+ workflows: Object.keys(this.workflows),
1381
1455
  defaultAgent: this.built.defaultAgentId,
1382
1456
  backgroundTurns: this.config.backgroundTurns !== false,
1383
1457
  apiAccess: this.config.apiAccess ?? "scoped",
1384
1458
  mcp: this.mcp ? `${this.basePath}${this.mcp.httpPath}` : "off",
1385
1459
  lakebase: memoryPool !== undefined,
1386
- feedback: this.feedbackEnabled(),
1460
+ feedback: this.feedbackMode(),
1387
1461
  observability: observability !== undefined ? "mlflow" : "off",
1388
1462
  });
1389
1463
  }
package/src/server.ts CHANGED
@@ -30,7 +30,6 @@ import {
30
30
  type User,
31
31
  } from "./config.ts";
32
32
  import { requestUserEmail, requestUserId } from "./identity.ts";
33
- import { resolveFeedbackEnabled } from "./mlflow.ts";
34
33
  import { MASTRA_ORIGIN_KEY } from "./request-context.ts";
35
34
  import {
36
35
  extractModelOverride,
@@ -202,10 +201,9 @@ export async function createRequestContext(
202
201
  export class MastraServer extends ExpressMastraServer {
203
202
  private log: log.Logger;
204
203
  /**
205
- * Whether to stamp the MLflow trace-id header on responses. Shares the
206
- * plugin's feedback gate via {@link resolveFeedbackEnabled}.
204
+ * Whether to stamp the MLflow trace-id header on responses.
207
205
  */
208
- private feedbackEnabled: boolean;
206
+ private mlflowFeedback: boolean;
209
207
 
210
208
  constructor(
211
209
  private config: MastraPluginConfig,
@@ -213,7 +211,7 @@ export class MastraServer extends ExpressMastraServer {
213
211
  ) {
214
212
  super(...args);
215
213
  this.log = log.logger(config);
216
- this.feedbackEnabled = resolveFeedbackEnabled(config.feedback);
214
+ this.mlflowFeedback = config.feedbackMode === "mlflow";
217
215
  }
218
216
 
219
217
  override registerAuthMiddleware(): void {
@@ -324,7 +322,7 @@ export class MastraServer extends ExpressMastraServer {
324
322
  * degrading gracefully rather than emitting a bogus trace id.
325
323
  */
326
324
  configureMlflowTraceId(res: express.Response) {
327
- if (!this.feedbackEnabled || res.headersSent) return;
325
+ if (!this.mlflowFeedback || res.headersSent) return;
328
326
  if (res.getHeader(feedback.MLFLOW_TRACE_ID_HEADER)) return;
329
327
  const traceId = trace.getActiveSpan()?.spanContext().traceId;
330
328
  if (!traceId || traceId === INVALID_TRACE_ID) return;