@dbx-tools/appkit-mastra 0.1.73 → 0.1.75
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 +67 -4
- package/dist/index.d.ts +74 -0
- package/dist/index.js +361 -126
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -13,10 +13,14 @@ is a one-line import change.
|
|
|
13
13
|
|
|
14
14
|
The implementation lives under `src/`: agent registration in
|
|
15
15
|
[`agents.ts`](src/agents.ts), the plugin + routes in
|
|
16
|
-
[`plugin.ts`](src/plugin.ts) / [`server.ts`](src/server.ts),
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[`
|
|
16
|
+
[`plugin.ts`](src/plugin.ts) / [`server.ts`](src/server.ts), thread
|
|
17
|
+
history / listing in [`history.ts`](src/history.ts) /
|
|
18
|
+
[`threads.ts`](src/threads.ts), Model Serving resolution in
|
|
19
|
+
[`model.ts`](src/model.ts) / [`serving.ts`](src/serving.ts), Genie
|
|
20
|
+
tooling in [`genie.ts`](src/genie.ts), the chart pipeline in
|
|
21
|
+
[`chart.ts`](src/chart.ts), and MLflow feedback logging in
|
|
22
|
+
[`mlflow.ts`](src/mlflow.ts) (over the shared REST helper in
|
|
23
|
+
[`rest.ts`](src/rest.ts)).
|
|
20
24
|
|
|
21
25
|
## Quick start
|
|
22
26
|
|
|
@@ -267,6 +271,35 @@ entry settles. Cache entries carry a 1h TTL; after expiry the route
|
|
|
267
271
|
placement contract are in [`chart.ts`](src/chart.ts); the wire shapes
|
|
268
272
|
are in [`@dbx-tools/appkit-mastra-shared`](../appkit-mastra-shared).
|
|
269
273
|
|
|
274
|
+
## Feedback (MLflow)
|
|
275
|
+
|
|
276
|
+
When MLflow tracing is wired for the deployment, the plugin lets users
|
|
277
|
+
rate an assistant turn (thumbs up / down) and leave a comment, logged as
|
|
278
|
+
a HUMAN [assessment](https://mlflow.org/docs/latest/genai/assessments/)
|
|
279
|
+
on that turn's trace and attributed to the signed-in (OBO) user.
|
|
280
|
+
|
|
281
|
+
Enablement is auto-detected: it turns on when an OTLP exporter endpoint
|
|
282
|
+
(`OTEL_EXPORTER_OTLP_ENDPOINT` / `..._TRACES_ENDPOINT`) **and** an MLflow
|
|
283
|
+
experiment (`MLFLOW_EXPERIMENT_ID` / `MLFLOW_EXPERIMENT_NAME`) are
|
|
284
|
+
configured - the two signals that traces actually materialize in MLflow.
|
|
285
|
+
Set `config.feedback` to `true` / `false` to force it on or off
|
|
286
|
+
regardless of the env.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
mastra({ agents: support, feedback: true }); // force-enable
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Under the hood the server derives MLflow's trace id from the active
|
|
293
|
+
OpenTelemetry span (`tr-<hex(otelTraceId)>`) and stamps it on each
|
|
294
|
+
turn's response (`MLFLOW_TRACE_ID_HEADER`); the client sends it back to
|
|
295
|
+
`POST ${basePath}/route/feedback`, which posts the assessment to the
|
|
296
|
+
Databricks MLflow REST API. Trace export is asynchronous, so a
|
|
297
|
+
just-finished trace may not exist yet - the log call retries briefly on
|
|
298
|
+
"not found" and otherwise fails softly (the chat stays usable). The
|
|
299
|
+
current state is published to the client as
|
|
300
|
+
`clientConfig().feedbackEnabled`; see [`mlflow.ts`](src/mlflow.ts) and
|
|
301
|
+
the UI wiring in [`@dbx-tools/appkit-mastra-ui`](../appkit-mastra-ui).
|
|
302
|
+
|
|
270
303
|
## Model resolution
|
|
271
304
|
|
|
272
305
|
Each agent call resolves a model lazily (so concurrent requests keep
|
|
@@ -338,6 +371,36 @@ resolves its model and tools as the calling user. The resolved endpoint
|
|
|
338
371
|
paths are available in-process via
|
|
339
372
|
`appkitUtils.require(this.context, mastra).exports().getMcp()`.
|
|
340
373
|
|
|
374
|
+
## Client API surface (`apiAccess`)
|
|
375
|
+
|
|
376
|
+
`@mastra/express` registers its full management route table under the
|
|
377
|
+
plugin mount - agent inference *plus* admin / mutating routes (direct
|
|
378
|
+
tool execution, workflow control, raw memory read/write, telemetry,
|
|
379
|
+
logs, scores). AppKit authenticates every request as the OBO user, but
|
|
380
|
+
does not restrict *which* of those a browser client may call.
|
|
381
|
+
|
|
382
|
+
`config.apiAccess` gates that surface at the plugin's dispatch point:
|
|
383
|
+
|
|
384
|
+
- `"scoped"` (default): only the routes the chat client needs are
|
|
385
|
+
dispatched to Mastra - agent inference (`stream` / `generate` /
|
|
386
|
+
`network`), read-only agent metadata (`GET /agents`,
|
|
387
|
+
`GET /agents/:id`), this plugin's own OBO- and resource-scoped
|
|
388
|
+
`/route/*` routes (history / threads / feedback), and, when `mcp` is
|
|
389
|
+
enabled, the MCP transport. Everything else is refused with `403`
|
|
390
|
+
before it reaches Mastra.
|
|
391
|
+
- `"full"`: dispatch the entire stock Mastra API. Use only for a trusted
|
|
392
|
+
first-party console that genuinely needs the management surface.
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
mastra({ agents: support }); // scoped by default
|
|
396
|
+
mastra({ agents: support, apiAccess: "full" }); // opt into the full API
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The gate is a pure allowlist (`isMastraRequestAllowed` in
|
|
400
|
+
[`server.ts`](src/server.ts)); the browser chat client only ever uses
|
|
401
|
+
the agent stream and the `/route/*` routes, so the default breaks
|
|
402
|
+
nothing.
|
|
403
|
+
|
|
341
404
|
## License
|
|
342
405
|
|
|
343
406
|
Apache-2.0
|
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
|
|
@@ -624,6 +642,28 @@ interface MastraPluginConfig extends BasePluginConfig {
|
|
|
624
642
|
* calling user.
|
|
625
643
|
*/
|
|
626
644
|
mcp?: boolean | MastraMcpConfig;
|
|
645
|
+
/**
|
|
646
|
+
* How much of the stock `@mastra/express` management API is reachable
|
|
647
|
+
* through the plugin mount. `@mastra/express` registers its full route
|
|
648
|
+
* table (agent inference plus admin / mutating routes: direct tool
|
|
649
|
+
* execution, workflow control, raw memory read/write, telemetry, logs,
|
|
650
|
+
* scores). AppKit already authenticates every request as the OBO user,
|
|
651
|
+
* but nothing there restricts *which* of those operations the browser
|
|
652
|
+
* client may invoke.
|
|
653
|
+
*
|
|
654
|
+
* - `"scoped"` (default): only the routes the chat client legitimately
|
|
655
|
+
* needs are dispatched to Mastra - agent inference
|
|
656
|
+
* (`stream` / `generate` / `network`), read-only agent metadata, this
|
|
657
|
+
* plugin's own OBO- and resource-scoped `/route/*` routes (history /
|
|
658
|
+
* threads), and, when {@link mcp} is enabled, the MCP transport.
|
|
659
|
+
* Everything else (tool execution, workflow control, raw memory,
|
|
660
|
+
* telemetry, logs, scores, and other mutations) is rejected with
|
|
661
|
+
* `403` before it reaches Mastra.
|
|
662
|
+
* - `"full"`: dispatch the entire stock Mastra API. Use only for a
|
|
663
|
+
* trusted first-party console that genuinely needs the management
|
|
664
|
+
* surface.
|
|
665
|
+
*/
|
|
666
|
+
apiAccess?: "scoped" | "full";
|
|
627
667
|
}
|
|
628
668
|
//#endregion
|
|
629
669
|
//#region packages/appkit-mastra/src/memory.d.ts
|
|
@@ -1300,6 +1340,11 @@ declare function buildMcpServer(opts: {
|
|
|
1300
1340
|
declare class MastraServer$1 extends MastraServer {
|
|
1301
1341
|
private config;
|
|
1302
1342
|
private log;
|
|
1343
|
+
/**
|
|
1344
|
+
* Whether to stamp the MLflow trace-id header on responses. Shares the
|
|
1345
|
+
* plugin's feedback gate via {@link resolveFeedbackEnabled}.
|
|
1346
|
+
*/
|
|
1347
|
+
private feedbackEnabled;
|
|
1303
1348
|
constructor(config: MastraPluginConfig, ...args: ConstructorParameters<typeof MastraServer>);
|
|
1304
1349
|
registerAuthMiddleware(): void;
|
|
1305
1350
|
configureRequestContextUser(requestContext: RequestContext): void;
|
|
@@ -1315,6 +1360,20 @@ declare class MastraServer$1 extends MastraServer {
|
|
|
1315
1360
|
* response header so dev tools can copy it from either side.
|
|
1316
1361
|
*/
|
|
1317
1362
|
configureRequestContextRequestId(req: express.Request, res: express.Response, requestContext: RequestContext): void;
|
|
1363
|
+
/**
|
|
1364
|
+
* Stamp the turn's MLflow trace id on the response so the chat client
|
|
1365
|
+
* can attach thumbs / comment feedback to it later. MLflow derives
|
|
1366
|
+
* its trace id from the OpenTelemetry trace id (`tr-<hex>`), and every
|
|
1367
|
+
* Mastra span for this request inherits the ambient OTel context (see
|
|
1368
|
+
* `observability.ts`), so the active span's trace id here is the id
|
|
1369
|
+
* MLflow will record for the turn.
|
|
1370
|
+
*
|
|
1371
|
+
* No-op unless feedback is enabled, and when no live OTel span is
|
|
1372
|
+
* active (e.g. the OTLP SDK isn't registered): in that case the header
|
|
1373
|
+
* is simply absent and the client hides feedback for that message,
|
|
1374
|
+
* degrading gracefully rather than emitting a bogus trace id.
|
|
1375
|
+
*/
|
|
1376
|
+
configureMlflowTraceId(res: express.Response): void;
|
|
1318
1377
|
/**
|
|
1319
1378
|
* Resolve the thread id this request targets and pin it on
|
|
1320
1379
|
* `RequestContext` (consumed by the agent stream for persistence and
|
|
@@ -1452,6 +1511,12 @@ declare class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1452
1511
|
clearModelsCache: (host?: string) => Promise<void>;
|
|
1453
1512
|
};
|
|
1454
1513
|
clientConfig(): Record<string, unknown>;
|
|
1514
|
+
/**
|
|
1515
|
+
* Whether user feedback can be logged to MLflow. Delegates to
|
|
1516
|
+
* {@link resolveFeedbackEnabled} so the client-config flag and the
|
|
1517
|
+
* feedback route share the same gate as the server's trace-id header.
|
|
1518
|
+
*/
|
|
1519
|
+
private feedbackEnabled;
|
|
1455
1520
|
injectRoutes(router: IAppRouter): void;
|
|
1456
1521
|
/**
|
|
1457
1522
|
* Invoke the Mastra express sub-app. Exists as a method (instead of reading
|
|
@@ -1472,6 +1537,15 @@ declare class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1472
1537
|
* shows a bare empty state instead of built-in example prompts.
|
|
1473
1538
|
*/
|
|
1474
1539
|
private fetchSuggestions;
|
|
1540
|
+
/**
|
|
1541
|
+
* Implementation backing the `/route/feedback` route. Runs inside the
|
|
1542
|
+
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
1543
|
+
* OBO-scoped client and the assessment is attributed to the signed-in
|
|
1544
|
+
* user (their email / id as the assessment source). Returns the
|
|
1545
|
+
* created assessment id on success, or `undefined` on a soft failure
|
|
1546
|
+
* (see {@link logFeedback} in `./mlflow.js`).
|
|
1547
|
+
*/
|
|
1548
|
+
private logFeedback;
|
|
1475
1549
|
/**
|
|
1476
1550
|
* Implementation backing the `data` embed resolver
|
|
1477
1551
|
* (`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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
665
|
+
log$9.warn("error", {
|
|
665
666
|
chartId,
|
|
666
667
|
error
|
|
667
668
|
});
|
|
@@ -848,8 +849,63 @@ function buildRenderDataTool(config) {
|
|
|
848
849
|
});
|
|
849
850
|
}
|
|
850
851
|
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region packages/appkit-mastra/src/rest.ts
|
|
854
|
+
/**
|
|
855
|
+
* Resolve the workspace host + an authenticated header set off the
|
|
856
|
+
* client and issue a `fetch` against `path` (mounted on the host).
|
|
857
|
+
* Runs as whatever identity the client carries - the per-request OBO
|
|
858
|
+
* user when called from a request scope, the service principal
|
|
859
|
+
* otherwise.
|
|
860
|
+
*/
|
|
861
|
+
async function databricksFetch(client, path, init) {
|
|
862
|
+
const host = (await client.config.getHost()).toString();
|
|
863
|
+
const headers = new Headers({
|
|
864
|
+
"Content-Type": "application/json",
|
|
865
|
+
...init.headers
|
|
866
|
+
});
|
|
867
|
+
await client.config.authenticate(headers);
|
|
868
|
+
const url = new URL(path, host).toString();
|
|
869
|
+
return fetch(url, {
|
|
870
|
+
method: init.method,
|
|
871
|
+
headers,
|
|
872
|
+
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
873
|
+
...init.signal ? { signal: init.signal } : {}
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
/** Read a response body as text, swallowing read errors (returns `""`). */
|
|
877
|
+
async function readResponseText(res) {
|
|
878
|
+
try {
|
|
879
|
+
return await res.text();
|
|
880
|
+
} catch {
|
|
881
|
+
return "";
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
|
|
885
|
+
async function readResponseJson(res) {
|
|
886
|
+
const text = await readResponseText(res);
|
|
887
|
+
if (!text) return {};
|
|
888
|
+
try {
|
|
889
|
+
return JSON.parse(text);
|
|
890
|
+
} catch {
|
|
891
|
+
return {};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
851
895
|
//#endregion
|
|
852
896
|
//#region packages/appkit-mastra/src/connectors/managed-memory/client.ts
|
|
897
|
+
/**
|
|
898
|
+
* Thin REST client over the Databricks Unity Catalog Managed Agent
|
|
899
|
+
* Memory API (Beta). There is no TS SDK for memory-stores yet, so this
|
|
900
|
+
* issues authenticated requests through the shared {@link databricksFetch}
|
|
901
|
+
* primitive (`rest.ts`) as whatever identity the caller's client carries
|
|
902
|
+
* - the per-request OBO user for tools / recall, the service principal
|
|
903
|
+
* for setup-time probes.
|
|
904
|
+
*
|
|
905
|
+
* Responses are parsed defensively: the Beta wire shape may still drift,
|
|
906
|
+
* so the search parser tolerates missing / renamed entry fields and the
|
|
907
|
+
* store probe maps a 404 to `null` rather than throwing.
|
|
908
|
+
*/
|
|
853
909
|
/** Base path for the Unity Catalog memory-stores REST surface. */
|
|
854
910
|
const MEMORY_STORES_PATH = "/api/2.1/unity-catalog/memory-stores";
|
|
855
911
|
/**
|
|
@@ -882,32 +938,14 @@ function parseStoreName(fullName) {
|
|
|
882
938
|
};
|
|
883
939
|
}
|
|
884
940
|
/**
|
|
885
|
-
* Resolve the workspace host and an authenticated header set off the
|
|
886
|
-
* client, then issue a `fetch`. Returns the raw `Response` so callers
|
|
887
|
-
* decide how to handle status codes (the store probe wants the 404, the
|
|
888
|
-
* mutating calls want a throw).
|
|
889
|
-
*/
|
|
890
|
-
async function rawFetch(client, path, init) {
|
|
891
|
-
const host = (await client.config.getHost()).toString();
|
|
892
|
-
const headers = new Headers({ "Content-Type": "application/json" });
|
|
893
|
-
await client.config.authenticate(headers);
|
|
894
|
-
const url = new URL(path, host).toString();
|
|
895
|
-
return fetch(url, {
|
|
896
|
-
method: init.method,
|
|
897
|
-
headers,
|
|
898
|
-
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
899
|
-
...init.signal ? { signal: init.signal } : {}
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
941
|
* Issue a request and parse the JSON body, throwing
|
|
904
942
|
* {@link ManagedMemoryError} on any non-2xx status. Used by the calls
|
|
905
943
|
* where anything but success is a real error (create / write / search).
|
|
906
944
|
*/
|
|
907
945
|
async function requestJson(client, path, init) {
|
|
908
|
-
const res = await
|
|
909
|
-
if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await
|
|
910
|
-
return
|
|
946
|
+
const res = await databricksFetch(client, path, init);
|
|
947
|
+
if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await readResponseText(res));
|
|
948
|
+
return readResponseJson(res);
|
|
911
949
|
}
|
|
912
950
|
/**
|
|
913
951
|
* Fetch a store by full name. Returns the raw store payload when it
|
|
@@ -916,13 +954,13 @@ async function requestJson(client, path, init) {
|
|
|
916
954
|
* Any other non-2xx status throws.
|
|
917
955
|
*/
|
|
918
956
|
async function getStore(client, fullName, signal) {
|
|
919
|
-
const res = await
|
|
957
|
+
const res = await databricksFetch(client, `${MEMORY_STORES_PATH}/${fullName}`, {
|
|
920
958
|
method: "GET",
|
|
921
959
|
...signal ? { signal } : {}
|
|
922
960
|
});
|
|
923
961
|
if (res.status === 404) return null;
|
|
924
|
-
if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await
|
|
925
|
-
return
|
|
962
|
+
if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await readResponseText(res));
|
|
963
|
+
return readResponseJson(res);
|
|
926
964
|
}
|
|
927
965
|
/**
|
|
928
966
|
* Probe for the store and create it when missing. Returns `true` when
|
|
@@ -989,10 +1027,14 @@ function parseEntries(body) {
|
|
|
989
1027
|
for (const raw of list) {
|
|
990
1028
|
if (!raw || typeof raw !== "object") continue;
|
|
991
1029
|
const obj = raw;
|
|
992
|
-
const contents =
|
|
1030
|
+
const contents = stringUtils.firstNonEmpty([
|
|
1031
|
+
obj["contents"],
|
|
1032
|
+
obj["content"],
|
|
1033
|
+
obj["text"]
|
|
1034
|
+
]);
|
|
993
1035
|
if (!contents) continue;
|
|
994
|
-
const path =
|
|
995
|
-
const description =
|
|
1036
|
+
const path = stringUtils.firstNonEmpty(obj["path"]);
|
|
1037
|
+
const description = stringUtils.firstNonEmpty([obj["description"], obj["summary"]]);
|
|
996
1038
|
const score = typeof obj["score"] === "number" ? obj["score"] : void 0;
|
|
997
1039
|
out.push({
|
|
998
1040
|
contents,
|
|
@@ -1003,28 +1045,6 @@ function parseEntries(body) {
|
|
|
1003
1045
|
}
|
|
1004
1046
|
return out;
|
|
1005
1047
|
}
|
|
1006
|
-
/** Return the first argument that is a non-empty string, else undefined. */
|
|
1007
|
-
function firstString(...values) {
|
|
1008
|
-
for (const v of values) if (typeof v === "string" && v.trim() !== "") return v;
|
|
1009
|
-
}
|
|
1010
|
-
/** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
|
|
1011
|
-
async function safeJson(res) {
|
|
1012
|
-
const text = await safeText(res);
|
|
1013
|
-
if (!text) return {};
|
|
1014
|
-
try {
|
|
1015
|
-
return JSON.parse(text);
|
|
1016
|
-
} catch {
|
|
1017
|
-
return {};
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
/** Read a response body as text, swallowing read errors. */
|
|
1021
|
-
async function safeText(res) {
|
|
1022
|
-
try {
|
|
1023
|
-
return await res.text();
|
|
1024
|
-
} catch {
|
|
1025
|
-
return "";
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
1048
|
|
|
1029
1049
|
//#endregion
|
|
1030
1050
|
//#region packages/appkit-mastra/src/connectors/managed-memory/context.ts
|
|
@@ -1084,7 +1104,7 @@ function resolveMemoryContext(requestContext) {
|
|
|
1084
1104
|
* failure degrades to a short error string rather than aborting the
|
|
1085
1105
|
* turn.
|
|
1086
1106
|
*/
|
|
1087
|
-
const log$
|
|
1107
|
+
const log$8 = logUtils.logger("mastra/managed-memory/tools");
|
|
1088
1108
|
const saveMemoryInput = z.object({
|
|
1089
1109
|
content: z.string().describe("The durable fact or preference to remember."),
|
|
1090
1110
|
summary: z.string().optional().describe("Optional one-line summary used to label the memory.")
|
|
@@ -1121,10 +1141,10 @@ function buildManagedMemoryTools(runtime) {
|
|
|
1121
1141
|
contents: content,
|
|
1122
1142
|
...summary ? { description: summary } : {}
|
|
1123
1143
|
});
|
|
1124
|
-
log$
|
|
1144
|
+
log$8.debug("saved", { scope: ctx.scope });
|
|
1125
1145
|
return "Saved to memory.";
|
|
1126
1146
|
} catch (err) {
|
|
1127
|
-
log$
|
|
1147
|
+
log$8.warn("save:error", { error: commonUtils.errorMessage(err) });
|
|
1128
1148
|
return "Could not save to memory right now.";
|
|
1129
1149
|
}
|
|
1130
1150
|
}
|
|
@@ -1145,7 +1165,7 @@ function buildManagedMemoryTools(runtime) {
|
|
|
1145
1165
|
try {
|
|
1146
1166
|
return { memories: (await search(ctx.client, runtime.storeName, ctx.scope, query, runtime.topK)).map(renderEntry) };
|
|
1147
1167
|
} catch (err) {
|
|
1148
|
-
log$
|
|
1168
|
+
log$8.warn("search:error", { error: commonUtils.errorMessage(err) });
|
|
1149
1169
|
return { memories: [] };
|
|
1150
1170
|
}
|
|
1151
1171
|
}
|
|
@@ -1283,7 +1303,7 @@ async function safeWrite(log, writer, chunk, context = {}) {
|
|
|
1283
1303
|
* `instructions` when you want the canonical "how to drive the
|
|
1284
1304
|
* Genie tools" guidance.
|
|
1285
1305
|
*/
|
|
1286
|
-
const log$
|
|
1306
|
+
const log$7 = logUtils.logger("mastra/genie");
|
|
1287
1307
|
/** Default alias used when a single unnamed Genie space is wired up. */
|
|
1288
1308
|
const DEFAULT_GENIE_ALIAS = "default";
|
|
1289
1309
|
/**
|
|
@@ -1398,7 +1418,7 @@ async function readCachedConversationId(cacheKey) {
|
|
|
1398
1418
|
try {
|
|
1399
1419
|
return await CacheManager.getInstanceSync().get(cacheKey) ?? void 0;
|
|
1400
1420
|
} catch (err) {
|
|
1401
|
-
log$
|
|
1421
|
+
log$7.warn("conversation-cache:read-error", { error: commonUtils.errorMessage(err) });
|
|
1402
1422
|
return;
|
|
1403
1423
|
}
|
|
1404
1424
|
}
|
|
@@ -1414,7 +1434,7 @@ async function saveCachedConversationId(cacheKey, conversationId) {
|
|
|
1414
1434
|
try {
|
|
1415
1435
|
await CacheManager.getInstanceSync().set(cacheKey, conversationId, { ttl: CONVERSATION_TTL_SEC });
|
|
1416
1436
|
} catch (err) {
|
|
1417
|
-
log$
|
|
1437
|
+
log$7.warn("conversation-cache:write-error", { error: commonUtils.errorMessage(err) });
|
|
1418
1438
|
}
|
|
1419
1439
|
}
|
|
1420
1440
|
/** Force-evict a cached conversation id. Used on the stale-id recovery path. */
|
|
@@ -1423,7 +1443,7 @@ async function evictCachedConversationId(cacheKey) {
|
|
|
1423
1443
|
try {
|
|
1424
1444
|
await CacheManager.getInstanceSync().delete(cacheKey);
|
|
1425
1445
|
} catch (err) {
|
|
1426
|
-
log$
|
|
1446
|
+
log$7.warn("conversation-cache:delete-error", { error: commonUtils.errorMessage(err) });
|
|
1427
1447
|
}
|
|
1428
1448
|
}
|
|
1429
1449
|
/**
|
|
@@ -1537,7 +1557,7 @@ function buildAskGenieTool(opts) {
|
|
|
1537
1557
|
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
1558
|
const cacheKey = await conversationCacheKey(spaceId, threadId);
|
|
1539
1559
|
await ensureConversationSeeded(requestContext, spaceId, cacheKey);
|
|
1540
|
-
await safeWrite(log$
|
|
1560
|
+
await safeWrite(log$7, writer, {
|
|
1541
1561
|
type: "started",
|
|
1542
1562
|
spaceId,
|
|
1543
1563
|
content: question
|
|
@@ -1550,7 +1570,7 @@ function buildAskGenieTool(opts) {
|
|
|
1550
1570
|
...seedConversationId ? { conversationId: seedConversationId } : {},
|
|
1551
1571
|
...signal ? { context: signal } : {}
|
|
1552
1572
|
})) {
|
|
1553
|
-
if (event.type !== "message") await safeWrite(log$
|
|
1573
|
+
if (event.type !== "message") await safeWrite(log$7, writer, event);
|
|
1554
1574
|
const eventConversationId = event.conversation_id;
|
|
1555
1575
|
if (eventConversationId) writeContextConversationId(requestContext, spaceId, eventConversationId);
|
|
1556
1576
|
if (event.type === "result") finalMessage = event.message;
|
|
@@ -1564,7 +1584,7 @@ function buildAskGenieTool(opts) {
|
|
|
1564
1584
|
} catch (err) {
|
|
1565
1585
|
const seeded = readContextConversationId(requestContext, spaceId);
|
|
1566
1586
|
if (seeded && apiUtils.isNotFoundError(err)) {
|
|
1567
|
-
log$
|
|
1587
|
+
log$7.warn("conversation-cache:stale, resetting", {
|
|
1568
1588
|
spaceId,
|
|
1569
1589
|
conversationId: seeded,
|
|
1570
1590
|
error: commonUtils.errorMessage(err)
|
|
@@ -2049,7 +2069,7 @@ async function collectSpaceSuggestions(opts) {
|
|
|
2049
2069
|
}));
|
|
2050
2070
|
writeSuggestionCache(spaceId, questions);
|
|
2051
2071
|
} catch (err) {
|
|
2052
|
-
log$
|
|
2072
|
+
log$7.warn("suggestions:fetch-error", {
|
|
2053
2073
|
spaceId,
|
|
2054
2074
|
error: commonUtils.errorMessage(err)
|
|
2055
2075
|
});
|
|
@@ -2110,7 +2130,7 @@ var ResultProcessor = class {
|
|
|
2110
2130
|
* query, or any REST failure the processor passes the messages through
|
|
2111
2131
|
* unchanged so a recall hiccup never blocks the conversation.
|
|
2112
2132
|
*/
|
|
2113
|
-
const log$
|
|
2133
|
+
const log$6 = logUtils.logger("mastra/processor/managed-memory-recall");
|
|
2114
2134
|
/**
|
|
2115
2135
|
* Build the recall processor bound to a managed-memory runtime. Created
|
|
2116
2136
|
* once at setup (when managed memory is active) and wired onto every
|
|
@@ -2129,7 +2149,7 @@ function buildManagedMemoryRecallProcessor(runtime) {
|
|
|
2129
2149
|
const entries = await search(ctx.client, runtime.storeName, ctx.scope, query, runtime.topK, args.abortSignal);
|
|
2130
2150
|
if (entries.length === 0) return args.messages;
|
|
2131
2151
|
const block = ["Relevant memories about the user (from long-term memory):", ...entries.map((e) => `- ${renderEntry(e)}`)].join("\n");
|
|
2132
|
-
log$
|
|
2152
|
+
log$6.debug("recalled", {
|
|
2133
2153
|
scope: ctx.scope,
|
|
2134
2154
|
entries: entries.length
|
|
2135
2155
|
});
|
|
@@ -2141,7 +2161,7 @@ function buildManagedMemoryRecallProcessor(runtime) {
|
|
|
2141
2161
|
}]
|
|
2142
2162
|
};
|
|
2143
2163
|
} catch (err) {
|
|
2144
|
-
log$
|
|
2164
|
+
log$6.warn("recall:error", { error: commonUtils.errorMessage(err) });
|
|
2145
2165
|
return args.messages;
|
|
2146
2166
|
}
|
|
2147
2167
|
}
|
|
@@ -2191,7 +2211,7 @@ function latestUserText(messages) {
|
|
|
2191
2211
|
* legacy `datasets[].chartId` payloads uniformly without coupling
|
|
2192
2212
|
* to specific tool ids.
|
|
2193
2213
|
*/
|
|
2194
|
-
const log$
|
|
2214
|
+
const log$5 = logUtils.logger("mastra/processor/strip-stale-charts");
|
|
2195
2215
|
/**
|
|
2196
2216
|
* Recursively clone `value`, omitting any property whose key is
|
|
2197
2217
|
* `chartId`. Arrays are mapped element-wise; primitives are
|
|
@@ -2239,7 +2259,7 @@ const stripStaleChartsProcessor = {
|
|
|
2239
2259
|
}
|
|
2240
2260
|
}
|
|
2241
2261
|
}
|
|
2242
|
-
if (stripped > 0) log$
|
|
2262
|
+
if (stripped > 0) log$5.debug("stripped", { results: stripped });
|
|
2243
2263
|
return args.messages;
|
|
2244
2264
|
}
|
|
2245
2265
|
};
|
|
@@ -2897,9 +2917,30 @@ function buildMcpServer(opts) {
|
|
|
2897
2917
|
};
|
|
2898
2918
|
}
|
|
2899
2919
|
|
|
2920
|
+
//#endregion
|
|
2921
|
+
//#region packages/appkit-mastra/src/pagination.ts
|
|
2922
|
+
/** Coerce / clamp a `perPage` value, falling back to `bounds.fallback`. */
|
|
2923
|
+
function clampPerPage(value, bounds) {
|
|
2924
|
+
if (value === void 0 || Number.isNaN(value)) return bounds.fallback;
|
|
2925
|
+
const n = Math.trunc(value);
|
|
2926
|
+
if (n <= 0) return bounds.fallback;
|
|
2927
|
+
return Math.min(n, bounds.max);
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Coerce a Hono query value into a non-negative integer. Returns
|
|
2931
|
+
* `undefined` for empty / non-numeric / negative inputs so the caller
|
|
2932
|
+
* can apply its built-in defaults.
|
|
2933
|
+
*/
|
|
2934
|
+
function parseIntParam(value) {
|
|
2935
|
+
if (!value) return void 0;
|
|
2936
|
+
const n = Number(value);
|
|
2937
|
+
if (!Number.isFinite(n) || n < 0) return void 0;
|
|
2938
|
+
return Math.trunc(n);
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2900
2941
|
//#endregion
|
|
2901
2942
|
//#region packages/appkit-mastra/src/history.ts
|
|
2902
|
-
const log$
|
|
2943
|
+
const log$4 = logUtils.logger("mastra/history");
|
|
2903
2944
|
/** Default history page size; matches the Mastra storage default. */
|
|
2904
2945
|
const DEFAULT_PER_PAGE$1 = 20;
|
|
2905
2946
|
/** Hard cap so a misbehaving client can't fetch the whole thread at once. */
|
|
@@ -2920,11 +2961,14 @@ const MAX_PER_PAGE$1 = 200;
|
|
|
2920
2961
|
* without sorting locally.
|
|
2921
2962
|
*/
|
|
2922
2963
|
async function loadHistory(opts) {
|
|
2923
|
-
const perPage = clampPerPage
|
|
2964
|
+
const perPage = clampPerPage(opts.perPage, {
|
|
2965
|
+
fallback: DEFAULT_PER_PAGE$1,
|
|
2966
|
+
max: MAX_PER_PAGE$1
|
|
2967
|
+
});
|
|
2924
2968
|
const page = Math.max(0, Math.trunc(opts.page ?? 0));
|
|
2925
2969
|
const memory = await opts.agent.getMemory();
|
|
2926
2970
|
if (!memory) {
|
|
2927
|
-
log$
|
|
2971
|
+
log$4.debug("recall:no-memory", {
|
|
2928
2972
|
agentId: opts.agent.id,
|
|
2929
2973
|
threadId: opts.threadId
|
|
2930
2974
|
});
|
|
@@ -2948,7 +2992,7 @@ async function loadHistory(opts) {
|
|
|
2948
2992
|
}
|
|
2949
2993
|
});
|
|
2950
2994
|
const uiMessages = toAISdkV5Messages(sortChronological(result.messages));
|
|
2951
|
-
log$
|
|
2995
|
+
log$4.debug("recall:done", {
|
|
2952
2996
|
agentId: opts.agent.id,
|
|
2953
2997
|
threadId: opts.threadId,
|
|
2954
2998
|
page,
|
|
@@ -2982,7 +3026,7 @@ async function loadHistory(opts) {
|
|
|
2982
3026
|
async function clearHistory(opts) {
|
|
2983
3027
|
const memory = await opts.agent.getMemory();
|
|
2984
3028
|
if (!memory) {
|
|
2985
|
-
log$
|
|
3029
|
+
log$4.debug("clear:no-memory", {
|
|
2986
3030
|
agentId: opts.agent.id,
|
|
2987
3031
|
threadId: opts.threadId
|
|
2988
3032
|
});
|
|
@@ -2996,7 +3040,7 @@ async function clearHistory(opts) {
|
|
|
2996
3040
|
perPage: 1
|
|
2997
3041
|
})).total;
|
|
2998
3042
|
} catch (err) {
|
|
2999
|
-
log$
|
|
3043
|
+
log$4.debug("clear:probe-failed", {
|
|
3000
3044
|
agentId: opts.agent.id,
|
|
3001
3045
|
threadId: opts.threadId,
|
|
3002
3046
|
error: commonUtils.errorMessage(err)
|
|
@@ -3006,13 +3050,13 @@ async function clearHistory(opts) {
|
|
|
3006
3050
|
try {
|
|
3007
3051
|
await memory.deleteThread(opts.threadId);
|
|
3008
3052
|
} catch (err) {
|
|
3009
|
-
log$
|
|
3053
|
+
log$4.warn("clear:delete-soft-failed", {
|
|
3010
3054
|
agentId: opts.agent.id,
|
|
3011
3055
|
threadId: opts.threadId,
|
|
3012
3056
|
error: commonUtils.errorMessage(err)
|
|
3013
3057
|
});
|
|
3014
3058
|
}
|
|
3015
|
-
log$
|
|
3059
|
+
log$4.info("clear:done", {
|
|
3016
3060
|
agentId: opts.agent.id,
|
|
3017
3061
|
threadId: opts.threadId,
|
|
3018
3062
|
cleared,
|
|
@@ -3069,8 +3113,8 @@ function historyRoute(options) {
|
|
|
3069
3113
|
agent: ctx.agent,
|
|
3070
3114
|
threadId: ctx.threadId,
|
|
3071
3115
|
...ctx.resourceId ? { resourceId: ctx.resourceId } : {},
|
|
3072
|
-
page: parseIntParam
|
|
3073
|
-
perPage: parseIntParam
|
|
3116
|
+
page: parseIntParam(c.req.query("page")),
|
|
3117
|
+
perPage: parseIntParam(c.req.query("perPage"))
|
|
3074
3118
|
});
|
|
3075
3119
|
return c.json(payload);
|
|
3076
3120
|
}
|
|
@@ -3093,13 +3137,6 @@ function historyRoute(options) {
|
|
|
3093
3137
|
}
|
|
3094
3138
|
})];
|
|
3095
3139
|
}
|
|
3096
|
-
/** Coerce / clamp `perPage`; falls back to the page-size default. */
|
|
3097
|
-
function clampPerPage$1(value) {
|
|
3098
|
-
if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE$1;
|
|
3099
|
-
const n = Math.trunc(value);
|
|
3100
|
-
if (n <= 0) return DEFAULT_PER_PAGE$1;
|
|
3101
|
-
return Math.min(n, MAX_PER_PAGE$1);
|
|
3102
|
-
}
|
|
3103
3140
|
/**
|
|
3104
3141
|
* Sort messages oldest-first by `createdAt`, falling back to whatever
|
|
3105
3142
|
* order the storage returned them in. The native `recall` call honors
|
|
@@ -3119,17 +3156,6 @@ function toEpoch(value) {
|
|
|
3119
3156
|
}
|
|
3120
3157
|
return 0;
|
|
3121
3158
|
}
|
|
3122
|
-
/**
|
|
3123
|
-
* Coerce a Hono query value into a non-negative integer. Returns
|
|
3124
|
-
* `undefined` for empty / non-numeric / negative inputs so
|
|
3125
|
-
* {@link loadHistory} can apply its built-in defaults.
|
|
3126
|
-
*/
|
|
3127
|
-
function parseIntParam$1(value) {
|
|
3128
|
-
if (!value) return void 0;
|
|
3129
|
-
const n = Number(value);
|
|
3130
|
-
if (!Number.isFinite(n) || n < 0) return void 0;
|
|
3131
|
-
return Math.trunc(n);
|
|
3132
|
-
}
|
|
3133
3159
|
|
|
3134
3160
|
//#endregion
|
|
3135
3161
|
//#region packages/appkit-mastra/src/memory.ts
|
|
@@ -3161,7 +3187,7 @@ function parseIntParam$1(value) {
|
|
|
3161
3187
|
* (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
|
|
3162
3188
|
* is registered); per-agent settings cascade on top of that.
|
|
3163
3189
|
*/
|
|
3164
|
-
const log$
|
|
3190
|
+
const log$3 = logUtils.logger("mastra/memory");
|
|
3165
3191
|
/**
|
|
3166
3192
|
* Build a dedicated **service-principal** Lakebase pool for Mastra
|
|
3167
3193
|
* memory from the lakebase plugin's resolved SP pg config.
|
|
@@ -3253,10 +3279,10 @@ var MemoryBuilder = class {
|
|
|
3253
3279
|
const storage = this.buildStorage(agentId, storageSetting);
|
|
3254
3280
|
const vector = this.buildVector(memorySetting);
|
|
3255
3281
|
if (!storage && !vector) {
|
|
3256
|
-
log$
|
|
3282
|
+
log$3.debug("agent:stateless", { agentId });
|
|
3257
3283
|
return;
|
|
3258
3284
|
}
|
|
3259
|
-
log$
|
|
3285
|
+
log$3.debug("agent:configured", {
|
|
3260
3286
|
agentId,
|
|
3261
3287
|
storage: storage !== void 0,
|
|
3262
3288
|
vector: vector !== void 0,
|
|
@@ -3368,6 +3394,110 @@ function withId(value, fallback) {
|
|
|
3368
3394
|
};
|
|
3369
3395
|
}
|
|
3370
3396
|
|
|
3397
|
+
//#endregion
|
|
3398
|
+
//#region packages/appkit-mastra/src/mlflow.ts
|
|
3399
|
+
/**
|
|
3400
|
+
* MLflow user-feedback logging: detect whether MLflow tracing is wired
|
|
3401
|
+
* for this deployment, and log a thumbs / comment as a trace
|
|
3402
|
+
* *assessment* via the Databricks MLflow REST API.
|
|
3403
|
+
*
|
|
3404
|
+
* Feedback attaches to a trace, and the plugin's spans reach MLflow
|
|
3405
|
+
* through the same OTel pipeline as every other AppKit span (see
|
|
3406
|
+
* `observability.ts`). MLflow derives its trace id from the OpenTelemetry
|
|
3407
|
+
* trace id (`tr-<hex(otelTraceId)>`), so the server stamps the active
|
|
3408
|
+
* trace id on each turn's response and the client sends it back here.
|
|
3409
|
+
*
|
|
3410
|
+
* There is no MLflow JS SDK, so this posts to the assessments REST
|
|
3411
|
+
* endpoint directly using the OBO-scoped workspace client (the feedback
|
|
3412
|
+
* is thus attributed to the signed-in user). Trace export is
|
|
3413
|
+
* asynchronous, so the just-finished trace may not exist in MLflow yet
|
|
3414
|
+
* when the user reacts; the log call retries briefly on "not found"
|
|
3415
|
+
* before giving up softly.
|
|
3416
|
+
*/
|
|
3417
|
+
const log$2 = logUtils.logger("mastra/mlflow");
|
|
3418
|
+
/** Assessments REST path for a trace. `3.0` is the current MLflow API version. */
|
|
3419
|
+
const assessmentsPath = (traceId) => `/api/3.0/mlflow/traces/${encodeURIComponent(traceId)}/assessments`;
|
|
3420
|
+
/** Number of times to retry a "trace not found" response before giving up. */
|
|
3421
|
+
const NOT_FOUND_RETRIES = 3;
|
|
3422
|
+
/** Base backoff between "trace not found" retries, in ms (grows linearly). */
|
|
3423
|
+
const NOT_FOUND_BACKOFF_MS = 1200;
|
|
3424
|
+
/**
|
|
3425
|
+
* Whether MLflow feedback logging is available for this deployment.
|
|
3426
|
+
*
|
|
3427
|
+
* Enabled when an OTLP exporter endpoint is configured (traces are
|
|
3428
|
+
* actually shipped somewhere) AND an MLflow experiment is named - the
|
|
3429
|
+
* two signals that the OTLP backend is MLflow and traces will
|
|
3430
|
+
* materialize there. Both are standard env vars, so no plugin config is
|
|
3431
|
+
* required; a deployment opts in simply by wiring MLflow tracing.
|
|
3432
|
+
*/
|
|
3433
|
+
function mlflowEnabled() {
|
|
3434
|
+
const hasExporter = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim());
|
|
3435
|
+
const hasExperiment = Boolean(process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim());
|
|
3436
|
+
return hasExporter && hasExperiment;
|
|
3437
|
+
}
|
|
3438
|
+
/**
|
|
3439
|
+
* Resolve whether user feedback is enabled from the plugin's optional
|
|
3440
|
+
* `config.feedback` override: the explicit boolean wins, otherwise fall
|
|
3441
|
+
* back to auto-detecting MLflow tracing ({@link mlflowEnabled}). Shared
|
|
3442
|
+
* by the plugin's client-config gate and the server's trace-id header so
|
|
3443
|
+
* the two never disagree.
|
|
3444
|
+
*/
|
|
3445
|
+
function resolveFeedbackEnabled(explicit) {
|
|
3446
|
+
return explicit ?? mlflowEnabled();
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Log a HUMAN feedback assessment to a trace. Returns the created
|
|
3450
|
+
* assessment id on success, or `undefined` when the trace can't be
|
|
3451
|
+
* found (even after retrying for export lag) or the request otherwise
|
|
3452
|
+
* fails - callers surface that as a soft "not recorded" rather than an
|
|
3453
|
+
* error, keeping the chat usable.
|
|
3454
|
+
*/
|
|
3455
|
+
async function logFeedback(client, params) {
|
|
3456
|
+
const hasValue = params.value !== void 0;
|
|
3457
|
+
const name = params.name?.trim() || (hasValue ? DEFAULT_FEEDBACK_NAME : DEFAULT_COMMENT_NAME);
|
|
3458
|
+
const value = hasValue ? params.value : params.comment;
|
|
3459
|
+
const body = { assessment: {
|
|
3460
|
+
trace_id: params.traceId,
|
|
3461
|
+
assessment_name: name,
|
|
3462
|
+
source: {
|
|
3463
|
+
source_type: "HUMAN",
|
|
3464
|
+
source_id: params.sourceId?.trim() || "user"
|
|
3465
|
+
},
|
|
3466
|
+
feedback: { value },
|
|
3467
|
+
...hasValue && params.comment?.trim() ? { rationale: params.comment } : {}
|
|
3468
|
+
} };
|
|
3469
|
+
for (let attempt = 0; attempt <= NOT_FOUND_RETRIES; attempt++) {
|
|
3470
|
+
let res;
|
|
3471
|
+
try {
|
|
3472
|
+
res = await databricksFetch(client, assessmentsPath(params.traceId), {
|
|
3473
|
+
method: "POST",
|
|
3474
|
+
body
|
|
3475
|
+
});
|
|
3476
|
+
} catch (err) {
|
|
3477
|
+
log$2.warn("feedback request failed", {
|
|
3478
|
+
traceId: params.traceId,
|
|
3479
|
+
error: commonUtils.errorMessage(err)
|
|
3480
|
+
});
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
if (res.ok) {
|
|
3484
|
+
const parsed = await readResponseJson(res);
|
|
3485
|
+
const assessmentId = parsed?.assessment?.assessment_id ?? parsed?.assessment_id;
|
|
3486
|
+
return typeof assessmentId === "string" ? assessmentId : "";
|
|
3487
|
+
}
|
|
3488
|
+
if (res.status === 404 && attempt < NOT_FOUND_RETRIES) {
|
|
3489
|
+
await commonUtils.sleep(NOT_FOUND_BACKOFF_MS * (attempt + 1));
|
|
3490
|
+
continue;
|
|
3491
|
+
}
|
|
3492
|
+
log$2.warn("feedback not recorded", {
|
|
3493
|
+
traceId: params.traceId,
|
|
3494
|
+
status: res.status,
|
|
3495
|
+
body: await readResponseText(res)
|
|
3496
|
+
});
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3371
3501
|
//#endregion
|
|
3372
3502
|
//#region packages/appkit-mastra/src/observability.ts
|
|
3373
3503
|
/**
|
|
@@ -3442,16 +3572,29 @@ async function buildObservability(options) {
|
|
|
3442
3572
|
* behind an Express mount point.
|
|
3443
3573
|
*/
|
|
3444
3574
|
/**
|
|
3575
|
+
* OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
|
|
3576
|
+
* `trace.getActiveSpan()` returns a non-recording span with this id
|
|
3577
|
+
* when no SDK is registered, which must never be surfaced as a trace to
|
|
3578
|
+
* attach feedback to.
|
|
3579
|
+
*/
|
|
3580
|
+
const INVALID_TRACE_ID = "0".repeat(32);
|
|
3581
|
+
/**
|
|
3445
3582
|
* `@mastra/express` subclass that stamps `RequestContext` with the
|
|
3446
3583
|
* AppKit user, resource id, and a thread id backed by an HTTP-only
|
|
3447
3584
|
* session cookie (`appkit_<plugin-name>_session_id`).
|
|
3448
3585
|
*/
|
|
3449
3586
|
var MastraServer$1 = class extends MastraServer {
|
|
3450
3587
|
log;
|
|
3588
|
+
/**
|
|
3589
|
+
* Whether to stamp the MLflow trace-id header on responses. Shares the
|
|
3590
|
+
* plugin's feedback gate via {@link resolveFeedbackEnabled}.
|
|
3591
|
+
*/
|
|
3592
|
+
feedbackEnabled;
|
|
3451
3593
|
constructor(config, ...args) {
|
|
3452
3594
|
super(...args);
|
|
3453
3595
|
this.config = config;
|
|
3454
3596
|
this.log = logUtils.logger(config);
|
|
3597
|
+
this.feedbackEnabled = resolveFeedbackEnabled(config.feedback);
|
|
3455
3598
|
}
|
|
3456
3599
|
registerAuthMiddleware() {
|
|
3457
3600
|
super.registerAuthMiddleware();
|
|
@@ -3461,6 +3604,7 @@ var MastraServer$1 = class extends MastraServer {
|
|
|
3461
3604
|
this.configureRequestContextThreadId(req, res, requestContext);
|
|
3462
3605
|
this.configureRequestContextModelOverride(req, requestContext);
|
|
3463
3606
|
this.configureRequestContextRequestId(req, res, requestContext);
|
|
3607
|
+
this.configureMlflowTraceId(res);
|
|
3464
3608
|
this.log.debug("auth:middleware", {
|
|
3465
3609
|
method: req.method,
|
|
3466
3610
|
path: req.path,
|
|
@@ -3507,6 +3651,25 @@ var MastraServer$1 = class extends MastraServer {
|
|
|
3507
3651
|
res.setHeader("X-Request-Id", requestId);
|
|
3508
3652
|
}
|
|
3509
3653
|
/**
|
|
3654
|
+
* Stamp the turn's MLflow trace id on the response so the chat client
|
|
3655
|
+
* can attach thumbs / comment feedback to it later. MLflow derives
|
|
3656
|
+
* its trace id from the OpenTelemetry trace id (`tr-<hex>`), and every
|
|
3657
|
+
* Mastra span for this request inherits the ambient OTel context (see
|
|
3658
|
+
* `observability.ts`), so the active span's trace id here is the id
|
|
3659
|
+
* MLflow will record for the turn.
|
|
3660
|
+
*
|
|
3661
|
+
* No-op unless feedback is enabled, and when no live OTel span is
|
|
3662
|
+
* active (e.g. the OTLP SDK isn't registered): in that case the header
|
|
3663
|
+
* is simply absent and the client hides feedback for that message,
|
|
3664
|
+
* degrading gracefully rather than emitting a bogus trace id.
|
|
3665
|
+
*/
|
|
3666
|
+
configureMlflowTraceId(res) {
|
|
3667
|
+
if (!this.feedbackEnabled || res.headersSent) return;
|
|
3668
|
+
const traceId = trace.getActiveSpan()?.spanContext().traceId;
|
|
3669
|
+
if (!traceId || traceId === INVALID_TRACE_ID) return;
|
|
3670
|
+
res.setHeader(MLFLOW_TRACE_ID_HEADER, `tr-${traceId}`);
|
|
3671
|
+
}
|
|
3672
|
+
/**
|
|
3510
3673
|
* Resolve the thread id this request targets and pin it on
|
|
3511
3674
|
* `RequestContext` (consumed by the agent stream for persistence and
|
|
3512
3675
|
* by the history / threads routes). Resolution order:
|
|
@@ -3571,6 +3734,37 @@ var MastraServer$1 = class extends MastraServer {
|
|
|
3571
3734
|
}
|
|
3572
3735
|
};
|
|
3573
3736
|
/**
|
|
3737
|
+
* Mount-relative agent inference paths (`/agents/:id/<verb>...`). Matched
|
|
3738
|
+
* by prefix on the verb so future variants (`streamVNext`, `stream/vnext`,
|
|
3739
|
+
* `generateVNext`) stay covered without a code change. These are the only
|
|
3740
|
+
* *writes* the browser client is allowed to make against stock Mastra.
|
|
3741
|
+
*/
|
|
3742
|
+
const AGENT_INFERENCE = /^\/agents\/[^/]+\/(stream|generate|network)/i;
|
|
3743
|
+
/** Mount-relative read-only agent metadata (`/agents`, `/agents/:id`). */
|
|
3744
|
+
const AGENT_METADATA = /^\/agents(\/[^/]+)?$/;
|
|
3745
|
+
/**
|
|
3746
|
+
* Whether a request to the stock `@mastra/express` sub-app should be
|
|
3747
|
+
* dispatched, given the configured {@link MastraApiGateOptions.access}.
|
|
3748
|
+
*
|
|
3749
|
+
* `path` is mount-relative (what the plugin's catch-all sees, e.g.
|
|
3750
|
+
* `/agents/x/stream`, `/route/history/x`, `/mcp/...`). In `"scoped"`
|
|
3751
|
+
* mode the allowlist is deliberately tight - the chat client only ever
|
|
3752
|
+
* needs agent inference, read-only agent metadata, this plugin's own
|
|
3753
|
+
* OBO/resource-scoped `/route/*` routes, and (when enabled) MCP - so the
|
|
3754
|
+
* whole admin / mutating / bulk-export surface Mastra also exposes is
|
|
3755
|
+
* denied by default rather than enumerated.
|
|
3756
|
+
*/
|
|
3757
|
+
function isMastraRequestAllowed(method, path, opts) {
|
|
3758
|
+
if (opts.access === "full") return true;
|
|
3759
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
3760
|
+
if (p === "/route" || p.startsWith("/route/")) return true;
|
|
3761
|
+
if (opts.mcpEnabled && (p === "/mcp" || p.startsWith("/mcp/"))) return true;
|
|
3762
|
+
const m = method.toUpperCase();
|
|
3763
|
+
if (m === "POST" && AGENT_INFERENCE.test(p)) return true;
|
|
3764
|
+
if (m === "GET" && AGENT_METADATA.test(p)) return true;
|
|
3765
|
+
return false;
|
|
3766
|
+
}
|
|
3767
|
+
/**
|
|
3574
3768
|
* Patches around `@mastra/express`'s custom-route dispatcher so the
|
|
3575
3769
|
* plugin's custom API routes (e.g. `historyRoute`) work when
|
|
3576
3770
|
* `MastraServer` is hosted on an Express subapp mounted under a parent
|
|
@@ -3609,7 +3803,10 @@ const MAX_PER_PAGE = 200;
|
|
|
3609
3803
|
* empty page so callers don't have to special-case stateless agents.
|
|
3610
3804
|
*/
|
|
3611
3805
|
async function listThreads(opts) {
|
|
3612
|
-
const perPage = clampPerPage(opts.perPage
|
|
3806
|
+
const perPage = clampPerPage(opts.perPage, {
|
|
3807
|
+
fallback: DEFAULT_PER_PAGE,
|
|
3808
|
+
max: MAX_PER_PAGE
|
|
3809
|
+
});
|
|
3613
3810
|
const page = Math.max(0, Math.trunc(opts.page ?? 0));
|
|
3614
3811
|
const memory = await opts.agent.getMemory();
|
|
3615
3812
|
if (!memory) {
|
|
@@ -3773,24 +3970,6 @@ function toIso(value) {
|
|
|
3773
3970
|
const parsed = new Date(value);
|
|
3774
3971
|
return Number.isNaN(parsed.getTime()) ? (/* @__PURE__ */ new Date(0)).toISOString() : parsed.toISOString();
|
|
3775
3972
|
}
|
|
3776
|
-
/** Coerce / clamp `perPage`; falls back to the page-size default. */
|
|
3777
|
-
function clampPerPage(value) {
|
|
3778
|
-
if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE;
|
|
3779
|
-
const n = Math.trunc(value);
|
|
3780
|
-
if (n <= 0) return DEFAULT_PER_PAGE;
|
|
3781
|
-
return Math.min(n, MAX_PER_PAGE);
|
|
3782
|
-
}
|
|
3783
|
-
/**
|
|
3784
|
-
* Coerce a Hono query value into a non-negative integer. Returns
|
|
3785
|
-
* `undefined` for empty / non-numeric / negative inputs so
|
|
3786
|
-
* {@link listThreads} can apply its built-in defaults.
|
|
3787
|
-
*/
|
|
3788
|
-
function parseIntParam(value) {
|
|
3789
|
-
if (!value) return void 0;
|
|
3790
|
-
const n = Number(value);
|
|
3791
|
-
if (!Number.isFinite(n) || n < 0) return void 0;
|
|
3792
|
-
return Math.trunc(n);
|
|
3793
|
-
}
|
|
3794
3973
|
|
|
3795
3974
|
//#endregion
|
|
3796
3975
|
//#region packages/appkit-mastra/src/plugin.ts
|
|
@@ -3821,8 +4000,13 @@ function parseIntParam(value) {
|
|
|
3821
4000
|
* Mastra agent routes, the plugin registers `/route/history`
|
|
3822
4001
|
* (load + clear a thread's messages), `/route/threads` (list the
|
|
3823
4002
|
* caller's conversations + delete one), `/models`, `/suggestions`,
|
|
3824
|
-
*
|
|
3825
|
-
*
|
|
4003
|
+
* `/route/feedback` (log a thumbs / comment to MLflow when feedback
|
|
4004
|
+
* is enabled), and the generic `/embed/:type/:id` resolver for inline
|
|
4005
|
+
* chart / data markers. The stock `@mastra/express` surface is gated
|
|
4006
|
+
* by `config.apiAccess` (default `"scoped"`): only agent inference,
|
|
4007
|
+
* read-only agent metadata, the `/route/*` routes, and (when enabled)
|
|
4008
|
+
* MCP are dispatched to Mastra; admin / mutating / bulk-export routes
|
|
4009
|
+
* are refused with `403`. See {@link isMastraRequestAllowed}.
|
|
3826
4010
|
* - MCP: opt in with `config.mcp` to expose the agents (and optionally
|
|
3827
4011
|
* tools) as a Mastra `MCPServer`. It is registered on the `Mastra`
|
|
3828
4012
|
* instance via `mcpServers`, so `@mastra/express` serves the stock
|
|
@@ -3938,9 +4122,18 @@ var MastraPlugin = class MastraPlugin extends Plugin {
|
|
|
3938
4122
|
return {
|
|
3939
4123
|
basePath: `/api/${this.name}`,
|
|
3940
4124
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
3941
|
-
agents: Object.keys(this.built?.agents ?? {})
|
|
4125
|
+
agents: Object.keys(this.built?.agents ?? {}),
|
|
4126
|
+
feedbackEnabled: this.feedbackEnabled()
|
|
3942
4127
|
};
|
|
3943
4128
|
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Whether user feedback can be logged to MLflow. Delegates to
|
|
4131
|
+
* {@link resolveFeedbackEnabled} so the client-config flag and the
|
|
4132
|
+
* feedback route share the same gate as the server's trace-id header.
|
|
4133
|
+
*/
|
|
4134
|
+
feedbackEnabled() {
|
|
4135
|
+
return resolveFeedbackEnabled(this.config.feedback);
|
|
4136
|
+
}
|
|
3944
4137
|
injectRoutes(router) {
|
|
3945
4138
|
router.get(MASTRA_ROUTES.models, (req, res, next) => {
|
|
3946
4139
|
this.userScopedSelf(req).listModels().then((endpoints) => res.json({ endpoints })).catch(next);
|
|
@@ -3993,8 +4186,33 @@ var MastraPlugin = class MastraPlugin extends Plugin {
|
|
|
3993
4186
|
};
|
|
3994
4187
|
router.get(MASTRA_ROUTES.suggestions, handleSuggestions);
|
|
3995
4188
|
router.get(`${MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
|
|
4189
|
+
router.post(MASTRA_ROUTES.feedback, (req, res, next) => {
|
|
4190
|
+
if (!this.feedbackEnabled()) {
|
|
4191
|
+
res.status(404).json({ ok: false });
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
const parsed = MastraFeedbackRequestSchema.safeParse(req.body);
|
|
4195
|
+
if (!parsed.success) {
|
|
4196
|
+
res.status(400).json({
|
|
4197
|
+
ok: false,
|
|
4198
|
+
error: parsed.error.message
|
|
4199
|
+
});
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
this.userScopedSelf(req).logFeedback(parsed.data).then((assessmentId) => res.json({
|
|
4203
|
+
ok: assessmentId !== void 0,
|
|
4204
|
+
...assessmentId ? { assessmentId } : {}
|
|
4205
|
+
})).catch(next);
|
|
4206
|
+
});
|
|
3996
4207
|
router.use((req, res, next) => {
|
|
3997
4208
|
if (!this.mastraApp) return res.status(503).end();
|
|
4209
|
+
if (!isMastraRequestAllowed(req.method, req.path, {
|
|
4210
|
+
access: this.config.apiAccess ?? "scoped",
|
|
4211
|
+
mcpEnabled: Boolean(this.config.mcp)
|
|
4212
|
+
})) {
|
|
4213
|
+
res.status(403).json({ error: "Endpoint not exposed to the client (apiAccess=scoped)" });
|
|
4214
|
+
return;
|
|
4215
|
+
}
|
|
3998
4216
|
return this.userScopedSelf(req).dispatchMastra(req, res, next);
|
|
3999
4217
|
});
|
|
4000
4218
|
}
|
|
@@ -4029,6 +4247,22 @@ var MastraPlugin = class MastraPlugin extends Plugin {
|
|
|
4029
4247
|
});
|
|
4030
4248
|
}
|
|
4031
4249
|
/**
|
|
4250
|
+
* Implementation backing the `/route/feedback` route. Runs inside the
|
|
4251
|
+
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
4252
|
+
* OBO-scoped client and the assessment is attributed to the signed-in
|
|
4253
|
+
* user (their email / id as the assessment source). Returns the
|
|
4254
|
+
* created assessment id on success, or `undefined` on a soft failure
|
|
4255
|
+
* (see {@link logFeedback} in `./mlflow.js`).
|
|
4256
|
+
*/
|
|
4257
|
+
async logFeedback(feedback) {
|
|
4258
|
+
const ctx = getExecutionContext();
|
|
4259
|
+
const sourceId = "userEmail" in ctx && ctx.userEmail ? ctx.userEmail : "userId" in ctx ? ctx.userId : ctx.serviceUserId;
|
|
4260
|
+
return logFeedback(ctx.client, {
|
|
4261
|
+
...feedback,
|
|
4262
|
+
...sourceId ? { sourceId } : {}
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
/**
|
|
4032
4266
|
* Implementation backing the `data` embed resolver
|
|
4033
4267
|
* (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
|
|
4034
4268
|
* `getExecutionContext()` returns the OBO-scoped workspace
|
|
@@ -4185,6 +4419,7 @@ var MastraPlugin = class MastraPlugin extends Plugin {
|
|
|
4185
4419
|
"/route/threads",
|
|
4186
4420
|
"/models",
|
|
4187
4421
|
"/suggestions",
|
|
4422
|
+
"/route/feedback",
|
|
4188
4423
|
"/embed/:type/:id"
|
|
4189
4424
|
],
|
|
4190
4425
|
instanceStorage: instanceStorage !== void 0,
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbx-tools/appkit-mastra",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.75",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@databricks/sdk-experimental": "^0.17",
|
|
6
|
-
"@dbx-tools/appkit-mastra-shared": "0.1.
|
|
7
|
-
"@dbx-tools/genie": "0.1.
|
|
8
|
-
"@dbx-tools/genie-shared": "0.1.
|
|
9
|
-
"@dbx-tools/model": "0.1.
|
|
10
|
-
"@dbx-tools/shared": "0.1.
|
|
6
|
+
"@dbx-tools/appkit-mastra-shared": "0.1.75",
|
|
7
|
+
"@dbx-tools/genie": "0.1.75",
|
|
8
|
+
"@dbx-tools/genie-shared": "0.1.75",
|
|
9
|
+
"@dbx-tools/model": "0.1.75",
|
|
10
|
+
"@dbx-tools/shared": "0.1.75",
|
|
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
|
},
|