@dbx-tools/appkit-mastra 0.1.73 → 0.1.74

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/dist/index.d.ts CHANGED
@@ -599,6 +599,24 @@ interface MastraPluginConfig extends BasePluginConfig {
599
599
  * to drill deep into a dataset within a single turn.
600
600
  */
601
601
  agentMaxSteps?: number;
602
+ /**
603
+ * Log user feedback (thumbs up/down + freeform comments) to MLflow as
604
+ * trace assessments, and surface the feedback controls in the chat UI.
605
+ *
606
+ * - `undefined` (default, auto): enabled only when MLflow tracing is
607
+ * wired - an OTLP exporter endpoint is set and an MLflow experiment
608
+ * is named (the same signals the observability pipeline needs to
609
+ * ship traces to MLflow). Otherwise off, since there'd be no trace
610
+ * to attach feedback to.
611
+ * - `true`: force on. Feedback controls show and writes are attempted
612
+ * regardless of env detection (use when the env is configured in a
613
+ * way the auto-probe doesn't recognize).
614
+ * - `false`: force off. No trace-id header, no feedback route, no UI.
615
+ *
616
+ * Feedback attaches to a turn's MLflow trace via the OpenTelemetry
617
+ * trace id the server stamps on each response; see `mlflow.ts`.
618
+ */
619
+ feedback?: boolean;
602
620
  /**
603
621
  * Expose the plugin's agents (and optionally tools) as a Mastra MCP
604
622
  * server so external MCP clients - Claude Desktop, Cursor, the Mastra
@@ -1292,14 +1310,15 @@ declare function buildMcpServer(opts: {
1292
1310
  }): ResolvedMcp | null;
1293
1311
  //#endregion
1294
1312
  //#region packages/appkit-mastra/src/server.d.ts
1295
- /**
1296
- * `@mastra/express` subclass that stamps `RequestContext` with the
1297
- * AppKit user, resource id, and a thread id backed by an HTTP-only
1298
- * session cookie (`appkit_<plugin-name>_session_id`).
1299
- */
1300
1313
  declare class MastraServer$1 extends MastraServer {
1301
1314
  private config;
1302
1315
  private log;
1316
+ /**
1317
+ * Whether to stamp the MLflow trace-id header on responses. Mirrors
1318
+ * the plugin's feedback gate: the explicit `config.feedback` override
1319
+ * wins, else auto-detect from the MLflow tracing env.
1320
+ */
1321
+ private feedbackEnabled;
1303
1322
  constructor(config: MastraPluginConfig, ...args: ConstructorParameters<typeof MastraServer>);
1304
1323
  registerAuthMiddleware(): void;
1305
1324
  configureRequestContextUser(requestContext: RequestContext): void;
@@ -1315,6 +1334,20 @@ declare class MastraServer$1 extends MastraServer {
1315
1334
  * response header so dev tools can copy it from either side.
1316
1335
  */
1317
1336
  configureRequestContextRequestId(req: express.Request, res: express.Response, requestContext: RequestContext): void;
1337
+ /**
1338
+ * Stamp the turn's MLflow trace id on the response so the chat client
1339
+ * can attach thumbs / comment feedback to it later. MLflow derives
1340
+ * its trace id from the OpenTelemetry trace id (`tr-<hex>`), and every
1341
+ * Mastra span for this request inherits the ambient OTel context (see
1342
+ * `observability.ts`), so the active span's trace id here is the id
1343
+ * MLflow will record for the turn.
1344
+ *
1345
+ * No-op unless feedback is enabled, and when no live OTel span is
1346
+ * active (e.g. the OTLP SDK isn't registered): in that case the header
1347
+ * is simply absent and the client hides feedback for that message,
1348
+ * degrading gracefully rather than emitting a bogus trace id.
1349
+ */
1350
+ configureMlflowTraceId(res: express.Response): void;
1318
1351
  /**
1319
1352
  * Resolve the thread id this request targets and pin it on
1320
1353
  * `RequestContext` (consumed by the agent stream for persistence and
@@ -1452,6 +1485,13 @@ declare class MastraPlugin extends Plugin<MastraPluginConfig> {
1452
1485
  clearModelsCache: (host?: string) => Promise<void>;
1453
1486
  };
1454
1487
  clientConfig(): Record<string, unknown>;
1488
+ /**
1489
+ * Whether user feedback can be logged to MLflow. The explicit
1490
+ * `config.feedback` override wins; otherwise auto-detect from the
1491
+ * MLflow tracing env ({@link mlflowEnabled}). Gates both the client
1492
+ * config flag and the feedback route so the two never disagree.
1493
+ */
1494
+ private feedbackEnabled;
1455
1495
  injectRoutes(router: IAppRouter): void;
1456
1496
  /**
1457
1497
  * Invoke the Mastra express sub-app. Exists as a method (instead of reading
@@ -1472,6 +1512,15 @@ declare class MastraPlugin extends Plugin<MastraPluginConfig> {
1472
1512
  * shows a bare empty state instead of built-in example prompts.
1473
1513
  */
1474
1514
  private fetchSuggestions;
1515
+ /**
1516
+ * Implementation backing the `/route/feedback` route. Runs inside the
1517
+ * AppKit user-context proxy so `getExecutionContext()` returns the
1518
+ * OBO-scoped client and the assessment is attributed to the signed-in
1519
+ * user (their email / id as the assessment source). Returns the
1520
+ * created assessment id on success, or `undefined` on a soft failure
1521
+ * (see {@link logFeedback} in `./mlflow.js`).
1522
+ */
1523
+ private logFeedback;
1475
1524
  /**
1476
1525
  * Implementation backing the `data` embed resolver
1477
1526
  * (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ChartSchema, ChartTypeSchema, MASTRA_ROUTES, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY, THREAD_ID_HEADER, THREAD_ID_QUERY } from "@dbx-tools/appkit-mastra-shared";
1
+ import { ChartSchema, ChartTypeSchema, DEFAULT_COMMENT_NAME, DEFAULT_FEEDBACK_NAME, MASTRA_ROUTES, MLFLOW_TRACE_ID_HEADER, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY, MastraFeedbackRequestSchema, THREAD_ID_HEADER, THREAD_ID_QUERY } from "@dbx-tools/appkit-mastra-shared";
2
2
  import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS, FALLBACK_MODEL_IDS, ModelClass, ModelClass as ModelClass$1, clearServingEndpointsCache, listServingEndpoints, parseModelClass, selectModel } from "@dbx-tools/model";
3
3
  import { apiUtils, appkitUtils, commonUtils, httpUtils, logUtils, netUtils, projectUtils, stringUtils } from "@dbx-tools/shared";
4
4
  import { Agent } from "@mastra/core/agent";
@@ -22,6 +22,7 @@ import { Pool } from "pg";
22
22
  import { Observability } from "@mastra/observability";
23
23
  import { OtelBridge } from "@mastra/otel-bridge";
24
24
  import { MastraServer } from "@mastra/express";
25
+ import { trace } from "@opentelemetry/api";
25
26
 
26
27
  export * from "@dbx-tools/appkit-mastra-shared"
27
28
 
@@ -337,7 +338,7 @@ function sanitizeServingMessages(messages) {
337
338
  * the demo client and any other UI consumer share the exact same
338
339
  * shape this module reads and writes.
339
340
  */
340
- const log$8 = logUtils.logger("mastra/chart");
341
+ const log$9 = logUtils.logger("mastra/chart");
341
342
  /**
342
343
  * TTL for cached chart entries. One hour balances "long enough for
343
344
  * the host UI to fetch the chart well after the model finished
@@ -593,7 +594,7 @@ async function writeChart(entry) {
593
594
  const key = await chartCacheKey(entry.chartId);
594
595
  await CacheManager.getInstanceSync().set(key, entry, { ttl: CHART_CACHE_TTL_SEC });
595
596
  } catch (err) {
596
- log$8.warn("write-error", {
597
+ log$9.warn("write-error", {
597
598
  chartId: entry.chartId,
598
599
  error: commonUtils.errorMessage(err)
599
600
  });
@@ -608,7 +609,7 @@ async function readChart(chartId) {
608
609
  const key = await chartCacheKey(chartId);
609
610
  return await CacheManager.getInstanceSync().get(key) ?? void 0;
610
611
  } catch (err) {
611
- log$8.warn("read-error", {
612
+ log$9.warn("read-error", {
612
613
  chartId,
613
614
  error: commonUtils.errorMessage(err)
614
615
  });
@@ -633,7 +634,7 @@ async function readChart(chartId) {
633
634
  async function prepareChart(opts) {
634
635
  const chartId = commonUtils.id();
635
636
  await writeChart({ chartId });
636
- log$8.debug("queued", { chartId });
637
+ log$9.debug("queued", { chartId });
637
638
  runPrepareChart(chartId, opts);
638
639
  return { chartId };
639
640
  }
@@ -654,14 +655,14 @@ async function runPrepareChart(chartId, opts) {
654
655
  chartId,
655
656
  result
656
657
  });
657
- log$8.info("done", {
658
+ log$9.info("done", {
658
659
  chartId,
659
660
  chartType: result.chartType,
660
661
  elapsedMs: Date.now() - startedAt
661
662
  });
662
663
  } catch (err) {
663
664
  const error = commonUtils.errorMessage(err);
664
- log$8.warn("error", {
665
+ log$9.warn("error", {
665
666
  chartId,
666
667
  error
667
668
  });
@@ -906,8 +907,8 @@ async function rawFetch(client, path, init) {
906
907
  */
907
908
  async function requestJson(client, path, init) {
908
909
  const res = await rawFetch(client, path, init);
909
- if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText(res));
910
- return safeJson(res);
910
+ if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText$1(res));
911
+ return safeJson$1(res);
911
912
  }
912
913
  /**
913
914
  * Fetch a store by full name. Returns the raw store payload when it
@@ -921,8 +922,8 @@ async function getStore(client, fullName, signal) {
921
922
  ...signal ? { signal } : {}
922
923
  });
923
924
  if (res.status === 404) return null;
924
- if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText(res));
925
- return safeJson(res);
925
+ if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText$1(res));
926
+ return safeJson$1(res);
926
927
  }
927
928
  /**
928
929
  * Probe for the store and create it when missing. Returns `true` when
@@ -1008,8 +1009,8 @@ function firstString(...values) {
1008
1009
  for (const v of values) if (typeof v === "string" && v.trim() !== "") return v;
1009
1010
  }
1010
1011
  /** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
1011
- async function safeJson(res) {
1012
- const text = await safeText(res);
1012
+ async function safeJson$1(res) {
1013
+ const text = await safeText$1(res);
1013
1014
  if (!text) return {};
1014
1015
  try {
1015
1016
  return JSON.parse(text);
@@ -1018,7 +1019,7 @@ async function safeJson(res) {
1018
1019
  }
1019
1020
  }
1020
1021
  /** Read a response body as text, swallowing read errors. */
1021
- async function safeText(res) {
1022
+ async function safeText$1(res) {
1022
1023
  try {
1023
1024
  return await res.text();
1024
1025
  } catch {
@@ -1084,7 +1085,7 @@ function resolveMemoryContext(requestContext) {
1084
1085
  * failure degrades to a short error string rather than aborting the
1085
1086
  * turn.
1086
1087
  */
1087
- const log$7 = logUtils.logger("mastra/managed-memory/tools");
1088
+ const log$8 = logUtils.logger("mastra/managed-memory/tools");
1088
1089
  const saveMemoryInput = z.object({
1089
1090
  content: z.string().describe("The durable fact or preference to remember."),
1090
1091
  summary: z.string().optional().describe("Optional one-line summary used to label the memory.")
@@ -1121,10 +1122,10 @@ function buildManagedMemoryTools(runtime) {
1121
1122
  contents: content,
1122
1123
  ...summary ? { description: summary } : {}
1123
1124
  });
1124
- log$7.debug("saved", { scope: ctx.scope });
1125
+ log$8.debug("saved", { scope: ctx.scope });
1125
1126
  return "Saved to memory.";
1126
1127
  } catch (err) {
1127
- log$7.warn("save:error", { error: commonUtils.errorMessage(err) });
1128
+ log$8.warn("save:error", { error: commonUtils.errorMessage(err) });
1128
1129
  return "Could not save to memory right now.";
1129
1130
  }
1130
1131
  }
@@ -1145,7 +1146,7 @@ function buildManagedMemoryTools(runtime) {
1145
1146
  try {
1146
1147
  return { memories: (await search(ctx.client, runtime.storeName, ctx.scope, query, runtime.topK)).map(renderEntry) };
1147
1148
  } catch (err) {
1148
- log$7.warn("search:error", { error: commonUtils.errorMessage(err) });
1149
+ log$8.warn("search:error", { error: commonUtils.errorMessage(err) });
1149
1150
  return { memories: [] };
1150
1151
  }
1151
1152
  }
@@ -1283,7 +1284,7 @@ async function safeWrite(log, writer, chunk, context = {}) {
1283
1284
  * `instructions` when you want the canonical "how to drive the
1284
1285
  * Genie tools" guidance.
1285
1286
  */
1286
- const log$6 = logUtils.logger("mastra/genie");
1287
+ const log$7 = logUtils.logger("mastra/genie");
1287
1288
  /** Default alias used when a single unnamed Genie space is wired up. */
1288
1289
  const DEFAULT_GENIE_ALIAS = "default";
1289
1290
  /**
@@ -1398,7 +1399,7 @@ async function readCachedConversationId(cacheKey) {
1398
1399
  try {
1399
1400
  return await CacheManager.getInstanceSync().get(cacheKey) ?? void 0;
1400
1401
  } catch (err) {
1401
- log$6.warn("conversation-cache:read-error", { error: commonUtils.errorMessage(err) });
1402
+ log$7.warn("conversation-cache:read-error", { error: commonUtils.errorMessage(err) });
1402
1403
  return;
1403
1404
  }
1404
1405
  }
@@ -1414,7 +1415,7 @@ async function saveCachedConversationId(cacheKey, conversationId) {
1414
1415
  try {
1415
1416
  await CacheManager.getInstanceSync().set(cacheKey, conversationId, { ttl: CONVERSATION_TTL_SEC });
1416
1417
  } catch (err) {
1417
- log$6.warn("conversation-cache:write-error", { error: commonUtils.errorMessage(err) });
1418
+ log$7.warn("conversation-cache:write-error", { error: commonUtils.errorMessage(err) });
1418
1419
  }
1419
1420
  }
1420
1421
  /** Force-evict a cached conversation id. Used on the stale-id recovery path. */
@@ -1423,7 +1424,7 @@ async function evictCachedConversationId(cacheKey) {
1423
1424
  try {
1424
1425
  await CacheManager.getInstanceSync().delete(cacheKey);
1425
1426
  } catch (err) {
1426
- log$6.warn("conversation-cache:delete-error", { error: commonUtils.errorMessage(err) });
1427
+ log$7.warn("conversation-cache:delete-error", { error: commonUtils.errorMessage(err) });
1427
1428
  }
1428
1429
  }
1429
1430
  /**
@@ -1537,7 +1538,7 @@ function buildAskGenieTool(opts) {
1537
1538
  if (trimmed.length === 0 || PLACEHOLDER_QUESTIONS.has(trimmed.toLowerCase())) throw new Error(`${toolId}: refusing placeholder question "${question}" - call ${toolId} only with a real natural-language question, or skip the call entirely`);
1538
1539
  const cacheKey = await conversationCacheKey(spaceId, threadId);
1539
1540
  await ensureConversationSeeded(requestContext, spaceId, cacheKey);
1540
- await safeWrite(log$6, writer, {
1541
+ await safeWrite(log$7, writer, {
1541
1542
  type: "started",
1542
1543
  spaceId,
1543
1544
  content: question
@@ -1550,7 +1551,7 @@ function buildAskGenieTool(opts) {
1550
1551
  ...seedConversationId ? { conversationId: seedConversationId } : {},
1551
1552
  ...signal ? { context: signal } : {}
1552
1553
  })) {
1553
- if (event.type !== "message") await safeWrite(log$6, writer, event);
1554
+ if (event.type !== "message") await safeWrite(log$7, writer, event);
1554
1555
  const eventConversationId = event.conversation_id;
1555
1556
  if (eventConversationId) writeContextConversationId(requestContext, spaceId, eventConversationId);
1556
1557
  if (event.type === "result") finalMessage = event.message;
@@ -1564,7 +1565,7 @@ function buildAskGenieTool(opts) {
1564
1565
  } catch (err) {
1565
1566
  const seeded = readContextConversationId(requestContext, spaceId);
1566
1567
  if (seeded && apiUtils.isNotFoundError(err)) {
1567
- log$6.warn("conversation-cache:stale, resetting", {
1568
+ log$7.warn("conversation-cache:stale, resetting", {
1568
1569
  spaceId,
1569
1570
  conversationId: seeded,
1570
1571
  error: commonUtils.errorMessage(err)
@@ -2049,7 +2050,7 @@ async function collectSpaceSuggestions(opts) {
2049
2050
  }));
2050
2051
  writeSuggestionCache(spaceId, questions);
2051
2052
  } catch (err) {
2052
- log$6.warn("suggestions:fetch-error", {
2053
+ log$7.warn("suggestions:fetch-error", {
2053
2054
  spaceId,
2054
2055
  error: commonUtils.errorMessage(err)
2055
2056
  });
@@ -2110,7 +2111,7 @@ var ResultProcessor = class {
2110
2111
  * query, or any REST failure the processor passes the messages through
2111
2112
  * unchanged so a recall hiccup never blocks the conversation.
2112
2113
  */
2113
- const log$5 = logUtils.logger("mastra/processor/managed-memory-recall");
2114
+ const log$6 = logUtils.logger("mastra/processor/managed-memory-recall");
2114
2115
  /**
2115
2116
  * Build the recall processor bound to a managed-memory runtime. Created
2116
2117
  * once at setup (when managed memory is active) and wired onto every
@@ -2129,7 +2130,7 @@ function buildManagedMemoryRecallProcessor(runtime) {
2129
2130
  const entries = await search(ctx.client, runtime.storeName, ctx.scope, query, runtime.topK, args.abortSignal);
2130
2131
  if (entries.length === 0) return args.messages;
2131
2132
  const block = ["Relevant memories about the user (from long-term memory):", ...entries.map((e) => `- ${renderEntry(e)}`)].join("\n");
2132
- log$5.debug("recalled", {
2133
+ log$6.debug("recalled", {
2133
2134
  scope: ctx.scope,
2134
2135
  entries: entries.length
2135
2136
  });
@@ -2141,7 +2142,7 @@ function buildManagedMemoryRecallProcessor(runtime) {
2141
2142
  }]
2142
2143
  };
2143
2144
  } catch (err) {
2144
- log$5.warn("recall:error", { error: commonUtils.errorMessage(err) });
2145
+ log$6.warn("recall:error", { error: commonUtils.errorMessage(err) });
2145
2146
  return args.messages;
2146
2147
  }
2147
2148
  }
@@ -2191,7 +2192,7 @@ function latestUserText(messages) {
2191
2192
  * legacy `datasets[].chartId` payloads uniformly without coupling
2192
2193
  * to specific tool ids.
2193
2194
  */
2194
- const log$4 = logUtils.logger("mastra/processor/strip-stale-charts");
2195
+ const log$5 = logUtils.logger("mastra/processor/strip-stale-charts");
2195
2196
  /**
2196
2197
  * Recursively clone `value`, omitting any property whose key is
2197
2198
  * `chartId`. Arrays are mapped element-wise; primitives are
@@ -2239,7 +2240,7 @@ const stripStaleChartsProcessor = {
2239
2240
  }
2240
2241
  }
2241
2242
  }
2242
- if (stripped > 0) log$4.debug("stripped", { results: stripped });
2243
+ if (stripped > 0) log$5.debug("stripped", { results: stripped });
2243
2244
  return args.messages;
2244
2245
  }
2245
2246
  };
@@ -2899,7 +2900,7 @@ function buildMcpServer(opts) {
2899
2900
 
2900
2901
  //#endregion
2901
2902
  //#region packages/appkit-mastra/src/history.ts
2902
- const log$3 = logUtils.logger("mastra/history");
2903
+ const log$4 = logUtils.logger("mastra/history");
2903
2904
  /** Default history page size; matches the Mastra storage default. */
2904
2905
  const DEFAULT_PER_PAGE$1 = 20;
2905
2906
  /** Hard cap so a misbehaving client can't fetch the whole thread at once. */
@@ -2924,7 +2925,7 @@ async function loadHistory(opts) {
2924
2925
  const page = Math.max(0, Math.trunc(opts.page ?? 0));
2925
2926
  const memory = await opts.agent.getMemory();
2926
2927
  if (!memory) {
2927
- log$3.debug("recall:no-memory", {
2928
+ log$4.debug("recall:no-memory", {
2928
2929
  agentId: opts.agent.id,
2929
2930
  threadId: opts.threadId
2930
2931
  });
@@ -2948,7 +2949,7 @@ async function loadHistory(opts) {
2948
2949
  }
2949
2950
  });
2950
2951
  const uiMessages = toAISdkV5Messages(sortChronological(result.messages));
2951
- log$3.debug("recall:done", {
2952
+ log$4.debug("recall:done", {
2952
2953
  agentId: opts.agent.id,
2953
2954
  threadId: opts.threadId,
2954
2955
  page,
@@ -2982,7 +2983,7 @@ async function loadHistory(opts) {
2982
2983
  async function clearHistory(opts) {
2983
2984
  const memory = await opts.agent.getMemory();
2984
2985
  if (!memory) {
2985
- log$3.debug("clear:no-memory", {
2986
+ log$4.debug("clear:no-memory", {
2986
2987
  agentId: opts.agent.id,
2987
2988
  threadId: opts.threadId
2988
2989
  });
@@ -2996,7 +2997,7 @@ async function clearHistory(opts) {
2996
2997
  perPage: 1
2997
2998
  })).total;
2998
2999
  } catch (err) {
2999
- log$3.debug("clear:probe-failed", {
3000
+ log$4.debug("clear:probe-failed", {
3000
3001
  agentId: opts.agent.id,
3001
3002
  threadId: opts.threadId,
3002
3003
  error: commonUtils.errorMessage(err)
@@ -3006,13 +3007,13 @@ async function clearHistory(opts) {
3006
3007
  try {
3007
3008
  await memory.deleteThread(opts.threadId);
3008
3009
  } catch (err) {
3009
- log$3.warn("clear:delete-soft-failed", {
3010
+ log$4.warn("clear:delete-soft-failed", {
3010
3011
  agentId: opts.agent.id,
3011
3012
  threadId: opts.threadId,
3012
3013
  error: commonUtils.errorMessage(err)
3013
3014
  });
3014
3015
  }
3015
- log$3.info("clear:done", {
3016
+ log$4.info("clear:done", {
3016
3017
  agentId: opts.agent.id,
3017
3018
  threadId: opts.threadId,
3018
3019
  cleared,
@@ -3131,6 +3132,144 @@ function parseIntParam$1(value) {
3131
3132
  return Math.trunc(n);
3132
3133
  }
3133
3134
 
3135
+ //#endregion
3136
+ //#region packages/appkit-mastra/src/rest.ts
3137
+ /**
3138
+ * Resolve the workspace host + an authenticated header set off the
3139
+ * client and issue a `fetch` against `path` (mounted on the host).
3140
+ * Runs as whatever identity the client carries - the per-request OBO
3141
+ * user when called from a request scope, the service principal
3142
+ * otherwise.
3143
+ */
3144
+ async function databricksFetch(client, path, init) {
3145
+ const host = (await client.config.getHost()).toString();
3146
+ const headers = new Headers({
3147
+ "Content-Type": "application/json",
3148
+ ...init.headers
3149
+ });
3150
+ await client.config.authenticate(headers);
3151
+ const url = new URL(path, host).toString();
3152
+ return fetch(url, {
3153
+ method: init.method,
3154
+ headers,
3155
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
3156
+ ...init.signal ? { signal: init.signal } : {}
3157
+ });
3158
+ }
3159
+
3160
+ //#endregion
3161
+ //#region packages/appkit-mastra/src/mlflow.ts
3162
+ /**
3163
+ * MLflow user-feedback logging: detect whether MLflow tracing is wired
3164
+ * for this deployment, and log a thumbs / comment as a trace
3165
+ * *assessment* via the Databricks MLflow REST API.
3166
+ *
3167
+ * Feedback attaches to a trace, and the plugin's spans reach MLflow
3168
+ * through the same OTel pipeline as every other AppKit span (see
3169
+ * `observability.ts`). MLflow derives its trace id from the OpenTelemetry
3170
+ * trace id (`tr-<hex(otelTraceId)>`), so the server stamps the active
3171
+ * trace id on each turn's response and the client sends it back here.
3172
+ *
3173
+ * There is no MLflow JS SDK, so this posts to the assessments REST
3174
+ * endpoint directly using the OBO-scoped workspace client (the feedback
3175
+ * is thus attributed to the signed-in user). Trace export is
3176
+ * asynchronous, so the just-finished trace may not exist in MLflow yet
3177
+ * when the user reacts; the log call retries briefly on "not found"
3178
+ * before giving up softly.
3179
+ */
3180
+ const log$3 = logUtils.logger("mastra/mlflow");
3181
+ /** Assessments REST path for a trace. `3.0` is the current MLflow API version. */
3182
+ const assessmentsPath = (traceId) => `/api/3.0/mlflow/traces/${encodeURIComponent(traceId)}/assessments`;
3183
+ /** Number of times to retry a "trace not found" response before giving up. */
3184
+ const NOT_FOUND_RETRIES = 3;
3185
+ /** Base backoff between "trace not found" retries, in ms (grows linearly). */
3186
+ const NOT_FOUND_BACKOFF_MS = 1200;
3187
+ /**
3188
+ * Whether MLflow feedback logging is available for this deployment.
3189
+ *
3190
+ * Enabled when an OTLP exporter endpoint is configured (traces are
3191
+ * actually shipped somewhere) AND an MLflow experiment is named - the
3192
+ * two signals that the OTLP backend is MLflow and traces will
3193
+ * materialize there. Both are standard env vars, so no plugin config is
3194
+ * required; a deployment opts in simply by wiring MLflow tracing.
3195
+ */
3196
+ function mlflowEnabled() {
3197
+ const hasExporter = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim());
3198
+ const hasExperiment = Boolean(process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim());
3199
+ return hasExporter && hasExperiment;
3200
+ }
3201
+ /**
3202
+ * Log a HUMAN feedback assessment to a trace. Returns the created
3203
+ * assessment id on success, or `undefined` when the trace can't be
3204
+ * found (even after retrying for export lag) or the request otherwise
3205
+ * fails - callers surface that as a soft "not recorded" rather than an
3206
+ * error, keeping the chat usable.
3207
+ */
3208
+ async function logFeedback(client, params) {
3209
+ const hasValue = params.value !== void 0;
3210
+ const name = params.name?.trim() || (hasValue ? DEFAULT_FEEDBACK_NAME : DEFAULT_COMMENT_NAME);
3211
+ const value = hasValue ? params.value : params.comment;
3212
+ const body = { assessment: {
3213
+ trace_id: params.traceId,
3214
+ assessment_name: name,
3215
+ source: {
3216
+ source_type: "HUMAN",
3217
+ source_id: params.sourceId?.trim() || "user"
3218
+ },
3219
+ feedback: { value },
3220
+ ...hasValue && params.comment?.trim() ? { rationale: params.comment } : {}
3221
+ } };
3222
+ for (let attempt = 0; attempt <= NOT_FOUND_RETRIES; attempt++) {
3223
+ let res;
3224
+ try {
3225
+ res = await databricksFetch(client, assessmentsPath(params.traceId), {
3226
+ method: "POST",
3227
+ body
3228
+ });
3229
+ } catch (err) {
3230
+ log$3.warn("feedback request failed", {
3231
+ traceId: params.traceId,
3232
+ error: commonUtils.errorMessage(err)
3233
+ });
3234
+ return;
3235
+ }
3236
+ if (res.ok) {
3237
+ const parsed = await safeJson(res);
3238
+ const assessmentId = parsed?.assessment?.assessment_id ?? parsed?.assessment_id;
3239
+ return typeof assessmentId === "string" ? assessmentId : "";
3240
+ }
3241
+ if (res.status === 404 && attempt < NOT_FOUND_RETRIES) {
3242
+ await delay(NOT_FOUND_BACKOFF_MS * (attempt + 1));
3243
+ continue;
3244
+ }
3245
+ log$3.warn("feedback not recorded", {
3246
+ traceId: params.traceId,
3247
+ status: res.status,
3248
+ body: await safeText(res)
3249
+ });
3250
+ return;
3251
+ }
3252
+ }
3253
+ function delay(ms) {
3254
+ return new Promise((resolve) => setTimeout(resolve, ms));
3255
+ }
3256
+ async function safeJson(res) {
3257
+ const text = await safeText(res);
3258
+ if (!text) return {};
3259
+ try {
3260
+ return JSON.parse(text);
3261
+ } catch {
3262
+ return {};
3263
+ }
3264
+ }
3265
+ async function safeText(res) {
3266
+ try {
3267
+ return await res.text();
3268
+ } catch {
3269
+ return "";
3270
+ }
3271
+ }
3272
+
3134
3273
  //#endregion
3135
3274
  //#region packages/appkit-mastra/src/memory.ts
3136
3275
  /**
@@ -3446,12 +3585,26 @@ async function buildObservability(options) {
3446
3585
  * AppKit user, resource id, and a thread id backed by an HTTP-only
3447
3586
  * session cookie (`appkit_<plugin-name>_session_id`).
3448
3587
  */
3588
+ /**
3589
+ * OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
3590
+ * `trace.getActiveSpan()` returns a non-recording span with this id
3591
+ * when no SDK is registered, which must never be surfaced as a trace to
3592
+ * attach feedback to.
3593
+ */
3594
+ const INVALID_TRACE_ID = "0".repeat(32);
3449
3595
  var MastraServer$1 = class extends MastraServer {
3450
3596
  log;
3597
+ /**
3598
+ * Whether to stamp the MLflow trace-id header on responses. Mirrors
3599
+ * the plugin's feedback gate: the explicit `config.feedback` override
3600
+ * wins, else auto-detect from the MLflow tracing env.
3601
+ */
3602
+ feedbackEnabled;
3451
3603
  constructor(config, ...args) {
3452
3604
  super(...args);
3453
3605
  this.config = config;
3454
3606
  this.log = logUtils.logger(config);
3607
+ this.feedbackEnabled = config.feedback ?? mlflowEnabled();
3455
3608
  }
3456
3609
  registerAuthMiddleware() {
3457
3610
  super.registerAuthMiddleware();
@@ -3461,6 +3614,7 @@ var MastraServer$1 = class extends MastraServer {
3461
3614
  this.configureRequestContextThreadId(req, res, requestContext);
3462
3615
  this.configureRequestContextModelOverride(req, requestContext);
3463
3616
  this.configureRequestContextRequestId(req, res, requestContext);
3617
+ this.configureMlflowTraceId(res);
3464
3618
  this.log.debug("auth:middleware", {
3465
3619
  method: req.method,
3466
3620
  path: req.path,
@@ -3507,6 +3661,25 @@ var MastraServer$1 = class extends MastraServer {
3507
3661
  res.setHeader("X-Request-Id", requestId);
3508
3662
  }
3509
3663
  /**
3664
+ * Stamp the turn's MLflow trace id on the response so the chat client
3665
+ * can attach thumbs / comment feedback to it later. MLflow derives
3666
+ * its trace id from the OpenTelemetry trace id (`tr-<hex>`), and every
3667
+ * Mastra span for this request inherits the ambient OTel context (see
3668
+ * `observability.ts`), so the active span's trace id here is the id
3669
+ * MLflow will record for the turn.
3670
+ *
3671
+ * No-op unless feedback is enabled, and when no live OTel span is
3672
+ * active (e.g. the OTLP SDK isn't registered): in that case the header
3673
+ * is simply absent and the client hides feedback for that message,
3674
+ * degrading gracefully rather than emitting a bogus trace id.
3675
+ */
3676
+ configureMlflowTraceId(res) {
3677
+ if (!this.feedbackEnabled || res.headersSent) return;
3678
+ const traceId = trace.getActiveSpan()?.spanContext().traceId;
3679
+ if (!traceId || traceId === INVALID_TRACE_ID) return;
3680
+ res.setHeader(MLFLOW_TRACE_ID_HEADER, `tr-${traceId}`);
3681
+ }
3682
+ /**
3510
3683
  * Resolve the thread id this request targets and pin it on
3511
3684
  * `RequestContext` (consumed by the agent stream for persistence and
3512
3685
  * by the history / threads routes). Resolution order:
@@ -3938,9 +4111,19 @@ var MastraPlugin = class MastraPlugin extends Plugin {
3938
4111
  return {
3939
4112
  basePath: `/api/${this.name}`,
3940
4113
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
3941
- agents: Object.keys(this.built?.agents ?? {})
4114
+ agents: Object.keys(this.built?.agents ?? {}),
4115
+ feedbackEnabled: this.feedbackEnabled()
3942
4116
  };
3943
4117
  }
4118
+ /**
4119
+ * Whether user feedback can be logged to MLflow. The explicit
4120
+ * `config.feedback` override wins; otherwise auto-detect from the
4121
+ * MLflow tracing env ({@link mlflowEnabled}). Gates both the client
4122
+ * config flag and the feedback route so the two never disagree.
4123
+ */
4124
+ feedbackEnabled() {
4125
+ return this.config.feedback ?? mlflowEnabled();
4126
+ }
3944
4127
  injectRoutes(router) {
3945
4128
  router.get(MASTRA_ROUTES.models, (req, res, next) => {
3946
4129
  this.userScopedSelf(req).listModels().then((endpoints) => res.json({ endpoints })).catch(next);
@@ -3993,6 +4176,24 @@ var MastraPlugin = class MastraPlugin extends Plugin {
3993
4176
  };
3994
4177
  router.get(MASTRA_ROUTES.suggestions, handleSuggestions);
3995
4178
  router.get(`${MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
4179
+ router.post(MASTRA_ROUTES.feedback, (req, res, next) => {
4180
+ if (!this.feedbackEnabled()) {
4181
+ res.status(404).json({ ok: false });
4182
+ return;
4183
+ }
4184
+ const parsed = MastraFeedbackRequestSchema.safeParse(req.body);
4185
+ if (!parsed.success) {
4186
+ res.status(400).json({
4187
+ ok: false,
4188
+ error: parsed.error.message
4189
+ });
4190
+ return;
4191
+ }
4192
+ this.userScopedSelf(req).logFeedback(parsed.data).then((assessmentId) => res.json({
4193
+ ok: assessmentId !== void 0,
4194
+ ...assessmentId ? { assessmentId } : {}
4195
+ })).catch(next);
4196
+ });
3996
4197
  router.use((req, res, next) => {
3997
4198
  if (!this.mastraApp) return res.status(503).end();
3998
4199
  return this.userScopedSelf(req).dispatchMastra(req, res, next);
@@ -4029,6 +4230,22 @@ var MastraPlugin = class MastraPlugin extends Plugin {
4029
4230
  });
4030
4231
  }
4031
4232
  /**
4233
+ * Implementation backing the `/route/feedback` route. Runs inside the
4234
+ * AppKit user-context proxy so `getExecutionContext()` returns the
4235
+ * OBO-scoped client and the assessment is attributed to the signed-in
4236
+ * user (their email / id as the assessment source). Returns the
4237
+ * created assessment id on success, or `undefined` on a soft failure
4238
+ * (see {@link logFeedback} in `./mlflow.js`).
4239
+ */
4240
+ async logFeedback(feedback) {
4241
+ const ctx = getExecutionContext();
4242
+ const sourceId = "userEmail" in ctx && ctx.userEmail ? ctx.userEmail : "userId" in ctx ? ctx.userId : ctx.serviceUserId;
4243
+ return logFeedback(ctx.client, {
4244
+ ...feedback,
4245
+ ...sourceId ? { sourceId } : {}
4246
+ });
4247
+ }
4248
+ /**
4032
4249
  * Implementation backing the `data` embed resolver
4033
4250
  * (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
4034
4251
  * `getExecutionContext()` returns the OBO-scoped workspace
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@dbx-tools/appkit-mastra",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "dependencies": {
5
5
  "@databricks/sdk-experimental": "^0.17",
6
- "@dbx-tools/appkit-mastra-shared": "0.1.73",
7
- "@dbx-tools/genie": "0.1.73",
8
- "@dbx-tools/genie-shared": "0.1.73",
9
- "@dbx-tools/model": "0.1.73",
10
- "@dbx-tools/shared": "0.1.73",
6
+ "@dbx-tools/appkit-mastra-shared": "0.1.74",
7
+ "@dbx-tools/genie": "0.1.74",
8
+ "@dbx-tools/genie-shared": "0.1.74",
9
+ "@dbx-tools/model": "0.1.74",
10
+ "@dbx-tools/shared": "0.1.74",
11
11
  "@mastra/ai-sdk": "^1",
12
12
  "@mastra/core": "^1",
13
13
  "@mastra/express": "^1",
@@ -17,6 +17,7 @@
17
17
  "@mastra/observability": "^1",
18
18
  "@mastra/otel-bridge": "^1",
19
19
  "@mastra/pg": "^1",
20
+ "@opentelemetry/api": "^1.9.0",
20
21
  "pg": "^8.20.0",
21
22
  "zod": "^4.3.6"
22
23
  },