@dbx-tools/appkit-mastra 0.1.5 → 0.1.13
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 +735 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/agents.js +18 -8
- package/dist/src/chart.d.ts +101 -35
- package/dist/src/chart.js +178 -62
- package/dist/src/config.d.ts +13 -0
- package/dist/src/genie.d.ts +23 -8
- package/dist/src/genie.js +137 -101
- package/dist/src/history.js +14 -0
- package/dist/src/memory.d.ts +21 -0
- package/dist/src/memory.js +47 -2
- package/dist/src/model.js +18 -14
- package/dist/src/observability.d.ts +33 -0
- package/dist/src/observability.js +71 -0
- package/dist/src/plugin.d.ts +1 -1
- package/dist/src/plugin.js +32 -4
- package/dist/src/processors/strip-stale-charts.d.ts +29 -0
- package/dist/src/processors/strip-stale-charts.js +96 -0
- package/dist/src/server.js +10 -0
- package/dist/src/serving.js +19 -2
- package/dist/src/tools/email.d.ts +74 -0
- package/dist/src/tools/email.js +122 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.ts +1 -0
- package/package.json +23 -25
- package/src/agents.ts +19 -6
- package/src/chart.ts +232 -64
- package/src/config.ts +13 -0
- package/src/genie.ts +179 -116
- package/src/history.ts +19 -7
- package/src/memory.ts +55 -2
- package/src/model.ts +18 -13
- package/src/observability.ts +92 -0
- package/src/plugin.ts +33 -4
- package/src/processors/strip-stale-charts.ts +105 -0
- package/src/server.ts +11 -0
- package/src/serving.ts +21 -2
- package/src/tools/email.ts +147 -0
- package/dist/src/render-chart-route.d.ts +0 -33
- package/dist/src/render-chart-route.js +0 -120
- package/src/render-chart-route.ts +0 -141
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra observability wiring for the `@dbx-tools/appkit-phoenix`
|
|
3
|
+
* sibling plugin.
|
|
4
|
+
*
|
|
5
|
+
* Mastra's `Observability` registry accepts any
|
|
6
|
+
* `@mastra/observability` `BaseExporter`. We use `OtelExporter` from
|
|
7
|
+
* `@mastra/otel-exporter` (Mastra's first-party OTLP shim) with the
|
|
8
|
+
* `custom` provider pointed at Phoenix's local collector URL. No
|
|
9
|
+
* Arize-specific wrapper is needed - Phoenix is a vanilla
|
|
10
|
+
* OpenInference-compatible OTLP/HTTP receiver.
|
|
11
|
+
*
|
|
12
|
+
* Discovery is structural so this module doesn't depend on
|
|
13
|
+
* `@dbx-tools/appkit-phoenix` at compile time: we look up the
|
|
14
|
+
* registered plugin by its registered name (`"phoenix"`) and read its
|
|
15
|
+
* `exports().collectorEndpoint()` if it is shaped like the phoenix
|
|
16
|
+
* plugin. The phoenix package is therefore an *optional* sibling -
|
|
17
|
+
* apps that don't install it just get an undefined observability
|
|
18
|
+
* config and Mastra runs without OTLP export.
|
|
19
|
+
*/
|
|
20
|
+
import type { pluginUtils } from "@dbx-tools/appkit-shared";
|
|
21
|
+
import { Observability } from "@mastra/observability";
|
|
22
|
+
/**
|
|
23
|
+
* If the sibling `phoenix` plugin is registered AND has booted with a
|
|
24
|
+
* usable collector URL, return a Mastra `Observability` configured to
|
|
25
|
+
* stream traces + logs there. Otherwise return `undefined` so the
|
|
26
|
+
* caller can omit the field on the `new Mastra({...})` constructor.
|
|
27
|
+
*
|
|
28
|
+
* The exporter uses `provider.custom` with `http/protobuf`, which is
|
|
29
|
+
* what Phoenix's `/v1/traces` endpoint speaks natively. Switching
|
|
30
|
+
* Phoenix to gRPC would be a one-line `protocol: "grpc"` change and
|
|
31
|
+
* a different exported URL.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildPhoenixObservability(context: pluginUtils.PluginContextLike | undefined, serviceName: string): Observability | undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra observability wiring for the `@dbx-tools/appkit-phoenix`
|
|
3
|
+
* sibling plugin.
|
|
4
|
+
*
|
|
5
|
+
* Mastra's `Observability` registry accepts any
|
|
6
|
+
* `@mastra/observability` `BaseExporter`. We use `OtelExporter` from
|
|
7
|
+
* `@mastra/otel-exporter` (Mastra's first-party OTLP shim) with the
|
|
8
|
+
* `custom` provider pointed at Phoenix's local collector URL. No
|
|
9
|
+
* Arize-specific wrapper is needed - Phoenix is a vanilla
|
|
10
|
+
* OpenInference-compatible OTLP/HTTP receiver.
|
|
11
|
+
*
|
|
12
|
+
* Discovery is structural so this module doesn't depend on
|
|
13
|
+
* `@dbx-tools/appkit-phoenix` at compile time: we look up the
|
|
14
|
+
* registered plugin by its registered name (`"phoenix"`) and read its
|
|
15
|
+
* `exports().collectorEndpoint()` if it is shaped like the phoenix
|
|
16
|
+
* plugin. The phoenix package is therefore an *optional* sibling -
|
|
17
|
+
* apps that don't install it just get an undefined observability
|
|
18
|
+
* config and Mastra runs without OTLP export.
|
|
19
|
+
*/
|
|
20
|
+
import { Observability } from "@mastra/observability";
|
|
21
|
+
import { OtelExporter } from "@mastra/otel-exporter";
|
|
22
|
+
/** Plugin name the phoenix plugin registers under (matches `phoenix()`). */
|
|
23
|
+
const PHOENIX_PLUGIN_NAME = "phoenix";
|
|
24
|
+
/**
|
|
25
|
+
* If the sibling `phoenix` plugin is registered AND has booted with a
|
|
26
|
+
* usable collector URL, return a Mastra `Observability` configured to
|
|
27
|
+
* stream traces + logs there. Otherwise return `undefined` so the
|
|
28
|
+
* caller can omit the field on the `new Mastra({...})` constructor.
|
|
29
|
+
*
|
|
30
|
+
* The exporter uses `provider.custom` with `http/protobuf`, which is
|
|
31
|
+
* what Phoenix's `/v1/traces` endpoint speaks natively. Switching
|
|
32
|
+
* Phoenix to gRPC would be a one-line `protocol: "grpc"` change and
|
|
33
|
+
* a different exported URL.
|
|
34
|
+
*/
|
|
35
|
+
export function buildPhoenixObservability(context, serviceName) {
|
|
36
|
+
const endpoint = readPhoenixEndpoint(context);
|
|
37
|
+
if (!endpoint)
|
|
38
|
+
return undefined;
|
|
39
|
+
return new Observability({
|
|
40
|
+
configs: {
|
|
41
|
+
phoenix: {
|
|
42
|
+
serviceName,
|
|
43
|
+
exporters: [
|
|
44
|
+
new OtelExporter({
|
|
45
|
+
provider: {
|
|
46
|
+
custom: {
|
|
47
|
+
endpoint,
|
|
48
|
+
protocol: "http/protobuf",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pull the OTLP collector URL out of the registered `phoenix` plugin.
|
|
59
|
+
* Tolerant of the plugin being absent (returns `undefined`) and of a
|
|
60
|
+
* future shape change in its exports (anything that's not a string
|
|
61
|
+
* is ignored). The lookup is keyed off the registered plugin *name*
|
|
62
|
+
* so this file does not depend on `@dbx-tools/appkit-phoenix`.
|
|
63
|
+
*/
|
|
64
|
+
function readPhoenixEndpoint(context) {
|
|
65
|
+
if (!context)
|
|
66
|
+
return undefined;
|
|
67
|
+
const plugin = context.getPlugins().get(PHOENIX_PLUGIN_NAME);
|
|
68
|
+
const exports_ = plugin?.exports?.();
|
|
69
|
+
const url = exports_?.collectorEndpoint?.();
|
|
70
|
+
return typeof url === "string" ? url : undefined;
|
|
71
|
+
}
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
88
88
|
*/
|
|
89
89
|
getDefault: () => Agent | null;
|
|
90
90
|
/** Underlying Mastra instance for advanced use (custom routes etc.). */
|
|
91
|
-
getMastra: () => Mastra<Record<string, Agent<any, import("@mastra/core/agent").ToolsInput, undefined, unknown>>, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>> | null;
|
|
91
|
+
getMastra: () => Mastra<Record<string, Agent<any, import("@mastra/core/agent").ToolsInput, undefined, unknown, import("@mastra/core/agent").AgentEditorConfig | undefined>>, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>> | null;
|
|
92
92
|
/** Express subapp Mastra is mounted on; mostly for tests. */
|
|
93
93
|
getMastraServer: () => MastraServer | null;
|
|
94
94
|
/**
|
package/dist/src/plugin.js
CHANGED
|
@@ -33,8 +33,8 @@ import { Mastra } from "@mastra/core/mastra";
|
|
|
33
33
|
import express from "express";
|
|
34
34
|
import { buildAgents, FALLBACK_AGENT_ID } from "./agents.js";
|
|
35
35
|
import { historyRoute } from "./history.js";
|
|
36
|
-
import { renderChartRoute } from "./render-chart-route.js";
|
|
37
36
|
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
37
|
+
import { buildPhoenixObservability } from "./observability.js";
|
|
38
38
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
39
39
|
import { clearServingEndpointsCache, listServingEndpoints, resolveServingConfig, } from "./serving.js";
|
|
40
40
|
const GENIE_MANIFEST = pluginUtils.data(genie).plugin.manifest;
|
|
@@ -163,7 +163,6 @@ export class MastraPlugin extends Plugin {
|
|
|
163
163
|
modelsPath: `${basePath}/models`,
|
|
164
164
|
historyPath: `${basePath}/route/history`,
|
|
165
165
|
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
166
|
-
renderChartPath: `${basePath}/route/render-chart`,
|
|
167
166
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
168
167
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
169
168
|
};
|
|
@@ -218,6 +217,10 @@ export class MastraPlugin extends Plugin {
|
|
|
218
217
|
const memoryBuilder = needsLakebase(this.config)
|
|
219
218
|
? createMemoryBuilder(this.config, this.context)
|
|
220
219
|
: undefined;
|
|
220
|
+
this.log.debug("build:start", {
|
|
221
|
+
lakebase: memoryBuilder !== undefined,
|
|
222
|
+
stripStaleCharts: this.config.stripStaleCharts !== false,
|
|
223
|
+
});
|
|
221
224
|
// Build every agent declared in `config.agents` (or the built-in
|
|
222
225
|
// fallback when none are declared). Each agent's `model` resolves
|
|
223
226
|
// workspace URL + bearer at call time so concurrent requests get
|
|
@@ -234,7 +237,26 @@ export class MastraPlugin extends Plugin {
|
|
|
234
237
|
// dev server. Since we're hosting Mastra inside our own Express
|
|
235
238
|
// subapp via `@mastra/express`, custom routes must be passed to
|
|
236
239
|
// the `MastraServer` constructor directly.
|
|
237
|
-
|
|
240
|
+
//
|
|
241
|
+
// `storage` here is *Mastra-instance-level* and persists workflow
|
|
242
|
+
// snapshots (where suspended `requireApproval` tool calls live).
|
|
243
|
+
// It's separate from each agent's `Memory.storage`, which only
|
|
244
|
+
// covers thread / message history. Without it,
|
|
245
|
+
// `agent.resumeStream()` errors with "could not find a suspended
|
|
246
|
+
// run" and the approval UI hangs after the user clicks Approve.
|
|
247
|
+
const instanceStorage = memoryBuilder?.instanceStorage();
|
|
248
|
+
// Auto-wire OTLP trace export to the sibling `phoenix` plugin if
|
|
249
|
+
// it's registered. Returns undefined when phoenix isn't around so
|
|
250
|
+
// the field stays off the constructor and Mastra keeps its noop
|
|
251
|
+
// observability default. The serviceName is the plugin's bound
|
|
252
|
+
// name so multiple mastra instances in one process stay
|
|
253
|
+
// distinguishable in Phoenix.
|
|
254
|
+
const observability = buildPhoenixObservability(this.context, this.name);
|
|
255
|
+
this.mastra = new Mastra({
|
|
256
|
+
agents: this.built.agents,
|
|
257
|
+
...(instanceStorage ? { storage: instanceStorage } : {}),
|
|
258
|
+
...(observability ? { observability } : {}),
|
|
259
|
+
});
|
|
238
260
|
this.mastraApp = express();
|
|
239
261
|
attachRoutePatchMiddleware(this.mastraApp);
|
|
240
262
|
this.mastraServer = new MastraServer(this.config, {
|
|
@@ -246,10 +268,16 @@ export class MastraPlugin extends Plugin {
|
|
|
246
268
|
chatRoute({ path: "/route/chat/:agentId" }),
|
|
247
269
|
historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
|
|
248
270
|
historyRoute({ path: "/route/history/:agentId" }),
|
|
249
|
-
renderChartRoute({ path: "/route/render-chart", config: this.config }),
|
|
250
271
|
],
|
|
251
272
|
});
|
|
252
273
|
await this.mastraServer.init();
|
|
274
|
+
this.log.debug("build:done", {
|
|
275
|
+
agents: Object.keys(this.built.agents),
|
|
276
|
+
defaultAgent: this.built.defaultAgentId,
|
|
277
|
+
routes: ["/route/chat", "/route/history", "/models"],
|
|
278
|
+
instanceStorage: instanceStorage !== undefined,
|
|
279
|
+
observability: observability !== undefined ? "phoenix" : "off",
|
|
280
|
+
});
|
|
253
281
|
}
|
|
254
282
|
}
|
|
255
283
|
export const mastra = toPlugin(MastraPlugin);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra input processor that strips `chartId` fields from every
|
|
3
|
+
* tool-invocation result in prior assistant messages before they
|
|
4
|
+
* reach the model.
|
|
5
|
+
*
|
|
6
|
+
* Why: chartIds are only meaningful within the assistant turn that
|
|
7
|
+
* minted them - the writer events backing them are gone after the
|
|
8
|
+
* stream closes. When the model sees old chartIds in memory recall
|
|
9
|
+
* (Mastra Memory persists tool results), it's tempted to type
|
|
10
|
+
* those ids into the new turn's `[[chart:<id>]]` markers, leaving
|
|
11
|
+
* the chat client's chart slots stuck with no matching event. This
|
|
12
|
+
* processor removes the temptation by deleting `chartId` keys from
|
|
13
|
+
* every assistant message's tool results before the prompt is
|
|
14
|
+
* built. The current turn's tool results don't exist yet at
|
|
15
|
+
* `processInput` time, so they pass through unmodified.
|
|
16
|
+
*
|
|
17
|
+
* The strip is recursive - any nested `chartId` field is removed,
|
|
18
|
+
* regardless of which tool produced the result. This covers Genie's
|
|
19
|
+
* `datasets[].chartId` and `render_data`'s top-level `chartId`
|
|
20
|
+
* uniformly without coupling to specific tool ids.
|
|
21
|
+
*/
|
|
22
|
+
import type { InputProcessor } from "@mastra/core/processors";
|
|
23
|
+
/**
|
|
24
|
+
* Input processor that scrubs `chartId` from every tool-invocation
|
|
25
|
+
* result in the message list. Wired onto every agent by default
|
|
26
|
+
* via {@link buildAgents}; opt out with
|
|
27
|
+
* `MastraPluginConfig.stripStaleCharts: false`.
|
|
28
|
+
*/
|
|
29
|
+
export declare const stripStaleChartsProcessor: InputProcessor;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra input processor that strips `chartId` fields from every
|
|
3
|
+
* tool-invocation result in prior assistant messages before they
|
|
4
|
+
* reach the model.
|
|
5
|
+
*
|
|
6
|
+
* Why: chartIds are only meaningful within the assistant turn that
|
|
7
|
+
* minted them - the writer events backing them are gone after the
|
|
8
|
+
* stream closes. When the model sees old chartIds in memory recall
|
|
9
|
+
* (Mastra Memory persists tool results), it's tempted to type
|
|
10
|
+
* those ids into the new turn's `[[chart:<id>]]` markers, leaving
|
|
11
|
+
* the chat client's chart slots stuck with no matching event. This
|
|
12
|
+
* processor removes the temptation by deleting `chartId` keys from
|
|
13
|
+
* every assistant message's tool results before the prompt is
|
|
14
|
+
* built. The current turn's tool results don't exist yet at
|
|
15
|
+
* `processInput` time, so they pass through unmodified.
|
|
16
|
+
*
|
|
17
|
+
* The strip is recursive - any nested `chartId` field is removed,
|
|
18
|
+
* regardless of which tool produced the result. This covers Genie's
|
|
19
|
+
* `datasets[].chartId` and `render_data`'s top-level `chartId`
|
|
20
|
+
* uniformly without coupling to specific tool ids.
|
|
21
|
+
*/
|
|
22
|
+
import { logUtils } from "@dbx-tools/appkit-shared";
|
|
23
|
+
const log = logUtils.logger("mastra/processor/strip-stale-charts");
|
|
24
|
+
/**
|
|
25
|
+
* Recursively clone `value`, omitting any property whose key is
|
|
26
|
+
* `chartId`. Arrays are mapped element-wise; primitives are
|
|
27
|
+
* returned as-is. The result is structurally identical to the
|
|
28
|
+
* input minus chartIds, so downstream message-shape consumers
|
|
29
|
+
* keep working.
|
|
30
|
+
*/
|
|
31
|
+
function stripChartIds(value) {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.map(stripChartIds);
|
|
34
|
+
}
|
|
35
|
+
if (value && typeof value === "object") {
|
|
36
|
+
const obj = value;
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
39
|
+
if (key === "chartId")
|
|
40
|
+
continue;
|
|
41
|
+
out[key] = stripChartIds(val);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Input processor that scrubs `chartId` from every tool-invocation
|
|
49
|
+
* result in the message list. Wired onto every agent by default
|
|
50
|
+
* via {@link buildAgents}; opt out with
|
|
51
|
+
* `MastraPluginConfig.stripStaleCharts: false`.
|
|
52
|
+
*/
|
|
53
|
+
export const stripStaleChartsProcessor = {
|
|
54
|
+
id: "strip-stale-charts",
|
|
55
|
+
description: "Removes chartId fields from prior tool-invocation results so the model can't reuse turn-scoped ids from memory.",
|
|
56
|
+
processInput(args) {
|
|
57
|
+
let stripped = 0;
|
|
58
|
+
for (const message of args.messages) {
|
|
59
|
+
if (message.role !== "assistant")
|
|
60
|
+
continue;
|
|
61
|
+
const parts = message.content?.parts;
|
|
62
|
+
if (!Array.isArray(parts))
|
|
63
|
+
continue;
|
|
64
|
+
for (const part of parts) {
|
|
65
|
+
// Tool-invocation parts hold the persisted tool result.
|
|
66
|
+
// We don't scrub the input args (`rawInput` / `args`) because
|
|
67
|
+
// the chartId there is the model's outgoing claim, not
|
|
68
|
+
// anything it could re-reference; only `result` carries
|
|
69
|
+
// ids that subsequent turns might copy.
|
|
70
|
+
if (part.type !== "tool-invocation") {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const inv = part
|
|
74
|
+
.toolInvocation;
|
|
75
|
+
if (!inv || inv.result === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
const before = inv.result;
|
|
78
|
+
const after = stripChartIds(before);
|
|
79
|
+
// Cheap structural check via JSON length - the actual
|
|
80
|
+
// strip writes a fresh object only when chartId keys
|
|
81
|
+
// existed, so different stringification length is a
|
|
82
|
+
// reliable signal that something was removed.
|
|
83
|
+
if (typeof before === "object" &&
|
|
84
|
+
before !== null &&
|
|
85
|
+
JSON.stringify(before).length !== JSON.stringify(after).length) {
|
|
86
|
+
inv.result = after;
|
|
87
|
+
stripped += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (stripped > 0) {
|
|
92
|
+
log.debug("stripped", { results: stripped });
|
|
93
|
+
}
|
|
94
|
+
return args.messages;
|
|
95
|
+
},
|
|
96
|
+
};
|
package/dist/src/server.js
CHANGED
|
@@ -31,6 +31,16 @@ export class MastraServer extends MastraServerExpress {
|
|
|
31
31
|
this.configureRequestContextUser(requestContext);
|
|
32
32
|
this.configureRequestContextThreadId(req, res, requestContext);
|
|
33
33
|
this.configureRequestContextModelOverride(req, requestContext);
|
|
34
|
+
this.log.debug("auth:middleware", {
|
|
35
|
+
method: req.method,
|
|
36
|
+
path: req.path,
|
|
37
|
+
threadId: requestContext.get(MASTRA_THREAD_ID_KEY),
|
|
38
|
+
resourceId: requestContext.get(MASTRA_RESOURCE_ID_KEY),
|
|
39
|
+
modelOverride: requestContext.get(
|
|
40
|
+
// imported below; logged so a misrouted request shows
|
|
41
|
+
// up alongside its model selection in `LOG_LEVEL=debug`.
|
|
42
|
+
"mastra__model_override"),
|
|
43
|
+
});
|
|
34
44
|
next();
|
|
35
45
|
});
|
|
36
46
|
}
|
package/dist/src/serving.js
CHANGED
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* `plugin.ts` exposes the cached list at `GET /models`.
|
|
21
21
|
*/
|
|
22
22
|
import { CacheManager } from "@databricks/appkit";
|
|
23
|
-
import { stringUtils } from "@dbx-tools/appkit-shared";
|
|
23
|
+
import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
|
|
24
24
|
import Fuse from "fuse.js";
|
|
25
|
+
const log = logUtils.logger("mastra/serving");
|
|
25
26
|
/**
|
|
26
27
|
* `RequestContext` key under which {@link MastraServer} stores the
|
|
27
28
|
* per-request model override (header / query / body). `model.ts`
|
|
@@ -76,6 +77,7 @@ export async function listServingEndpoints(client, host, opts = {}) {
|
|
|
76
77
|
return CacheManager.getInstanceSync().getOrExecute([CACHE_KEY_NAMESPACE, host], () => fetchEndpoints(client), SHARED_USER_KEY, { ttl: ttlSec });
|
|
77
78
|
}
|
|
78
79
|
async function fetchEndpoints(client) {
|
|
80
|
+
const startedAt = Date.now();
|
|
79
81
|
const out = [];
|
|
80
82
|
for await (const ep of client.servingEndpoints.list()) {
|
|
81
83
|
if (!ep.name)
|
|
@@ -87,6 +89,7 @@ async function fetchEndpoints(client) {
|
|
|
87
89
|
...(ep.description !== undefined ? { description: ep.description } : {}),
|
|
88
90
|
});
|
|
89
91
|
}
|
|
92
|
+
log.debug("listed", { count: out.length, elapsedMs: Date.now() - startedAt });
|
|
90
93
|
return out;
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
@@ -127,10 +130,12 @@ export async function clearServingEndpointsCache(host) {
|
|
|
127
130
|
*/
|
|
128
131
|
export function resolveModelId(input, endpoints, opts = {}) {
|
|
129
132
|
if (endpoints.length === 0) {
|
|
133
|
+
log.debug("resolve:no-endpoints", { input });
|
|
130
134
|
return { modelId: input, matched: false };
|
|
131
135
|
}
|
|
132
136
|
for (const ep of endpoints) {
|
|
133
137
|
if (ep.name === input) {
|
|
138
|
+
log.debug("resolve:exact", { input });
|
|
134
139
|
return { modelId: ep.name, matched: true, score: 0 };
|
|
135
140
|
}
|
|
136
141
|
}
|
|
@@ -148,13 +153,25 @@ export function resolveModelId(input, endpoints, opts = {}) {
|
|
|
148
153
|
// lean on the shared tokenizer so the splitting rules stay
|
|
149
154
|
// consistent with the rest of the toolkit.
|
|
150
155
|
const query = Array.from(stringUtils.tokenizeWithOptions({ lowerCase: true, camelCase: false }, input)).join(" ");
|
|
151
|
-
if (!query)
|
|
156
|
+
if (!query) {
|
|
157
|
+
log.debug("resolve:empty-tokens", { input });
|
|
152
158
|
return { modelId: input, matched: false };
|
|
159
|
+
}
|
|
153
160
|
const results = fuse.search(query);
|
|
154
161
|
const best = results[0];
|
|
155
162
|
if (best?.item.name && (best.score ?? 0) <= threshold) {
|
|
163
|
+
log.debug("resolve:fuzzy-match", {
|
|
164
|
+
input,
|
|
165
|
+
modelId: best.item.name,
|
|
166
|
+
score: best.score,
|
|
167
|
+
});
|
|
156
168
|
return { modelId: best.item.name, matched: true, score: best.score };
|
|
157
169
|
}
|
|
170
|
+
log.debug("resolve:no-match", {
|
|
171
|
+
input,
|
|
172
|
+
bestScore: best?.score,
|
|
173
|
+
threshold,
|
|
174
|
+
});
|
|
158
175
|
return { modelId: input, matched: false };
|
|
159
176
|
}
|
|
160
177
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra tool: `send_email`. Gated behind {@link requireApproval}
|
|
3
|
+
* so the model can call it freely but execution is paused until a
|
|
4
|
+
* human approves via the chat UI.
|
|
5
|
+
*
|
|
6
|
+
* The execute body is a stub - it logs the would-be email to the
|
|
7
|
+
* server console (via `logUtils.logger`) and returns success. Swap
|
|
8
|
+
* in a real SMTP / SES / Resend / Workspace Mail call later by
|
|
9
|
+
* editing the `execute` body; the tool surface and approval gate
|
|
10
|
+
* stay the same.
|
|
11
|
+
*
|
|
12
|
+
* Approval flow (Mastra + AI SDK V5):
|
|
13
|
+
*
|
|
14
|
+
* 1. Model calls the tool with `{ to, subject, body, ... }`.
|
|
15
|
+
* 2. Mastra evaluates `requireApproval` (here always `true`),
|
|
16
|
+
* pauses the agent loop, and emits a `tool-call-approval`
|
|
17
|
+
* chunk on the response stream.
|
|
18
|
+
* 3. The chat client renders an approve/deny prompt against the
|
|
19
|
+
* `state: 'approval-requested'` tool part. On approve, it sends
|
|
20
|
+
* a `MastraToolApproval` response back; on deny, the tool call
|
|
21
|
+
* is rejected and the model sees an error.
|
|
22
|
+
* 4. On approve, this `execute` runs and logs the email.
|
|
23
|
+
*
|
|
24
|
+
* The tool is intentionally NOT auto-installed on every agent -
|
|
25
|
+
* email is domain-specific, not infrastructure. Spread it into the
|
|
26
|
+
* specific agents that should be able to draft emails.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
declare const emailInputSchema: z.ZodObject<{
|
|
30
|
+
to: z.ZodString;
|
|
31
|
+
subject: z.ZodString;
|
|
32
|
+
body: z.ZodString;
|
|
33
|
+
cc: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
34
|
+
bcc: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
/** Options accepted by {@link buildEmailTool}. */
|
|
37
|
+
export interface BuildEmailToolOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Override the tool id. Defaults to `"send_email"`. Useful if a
|
|
40
|
+
* caller wants `send_internal_email` / `send_external_email`
|
|
41
|
+
* variants.
|
|
42
|
+
*/
|
|
43
|
+
id?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Replace the default execute body with a real provider call.
|
|
46
|
+
* Receives the validated input and must return `{sent, recipient}`.
|
|
47
|
+
* The console-log default is meant for demos / dev; production
|
|
48
|
+
* deployments should wire SMTP / SES / Resend / Workspace Mail
|
|
49
|
+
* here.
|
|
50
|
+
*/
|
|
51
|
+
send?: (input: z.infer<typeof emailInputSchema>) => Promise<void> | void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the `send_email` tool. Approval-gated by default; the
|
|
55
|
+
* execute body either calls the supplied {@link send} hook or
|
|
56
|
+
* logs the email to the server console as a demo stub.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { buildEmailTool, createAgent, mastra } from "@dbx-tools/appkit-mastra";
|
|
61
|
+
*
|
|
62
|
+
* const support = createAgent({
|
|
63
|
+
* instructions: "...",
|
|
64
|
+
* tools(plugins) {
|
|
65
|
+
* return {
|
|
66
|
+
* ...(plugins.genie?.toolkit() ?? {}),
|
|
67
|
+
* send_email: buildEmailTool(),
|
|
68
|
+
* };
|
|
69
|
+
* },
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildEmailTool(opts?: BuildEmailToolOptions): import("@mastra/core/tools").Tool<any, any, any, any, import("@mastra/core/tools").ToolExecutionContext<any, any, unknown>, string, unknown>;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra tool: `send_email`. Gated behind {@link requireApproval}
|
|
3
|
+
* so the model can call it freely but execution is paused until a
|
|
4
|
+
* human approves via the chat UI.
|
|
5
|
+
*
|
|
6
|
+
* The execute body is a stub - it logs the would-be email to the
|
|
7
|
+
* server console (via `logUtils.logger`) and returns success. Swap
|
|
8
|
+
* in a real SMTP / SES / Resend / Workspace Mail call later by
|
|
9
|
+
* editing the `execute` body; the tool surface and approval gate
|
|
10
|
+
* stay the same.
|
|
11
|
+
*
|
|
12
|
+
* Approval flow (Mastra + AI SDK V5):
|
|
13
|
+
*
|
|
14
|
+
* 1. Model calls the tool with `{ to, subject, body, ... }`.
|
|
15
|
+
* 2. Mastra evaluates `requireApproval` (here always `true`),
|
|
16
|
+
* pauses the agent loop, and emits a `tool-call-approval`
|
|
17
|
+
* chunk on the response stream.
|
|
18
|
+
* 3. The chat client renders an approve/deny prompt against the
|
|
19
|
+
* `state: 'approval-requested'` tool part. On approve, it sends
|
|
20
|
+
* a `MastraToolApproval` response back; on deny, the tool call
|
|
21
|
+
* is rejected and the model sees an error.
|
|
22
|
+
* 4. On approve, this `execute` runs and logs the email.
|
|
23
|
+
*
|
|
24
|
+
* The tool is intentionally NOT auto-installed on every agent -
|
|
25
|
+
* email is domain-specific, not infrastructure. Spread it into the
|
|
26
|
+
* specific agents that should be able to draft emails.
|
|
27
|
+
*/
|
|
28
|
+
import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
|
|
29
|
+
import { createTool } from "@mastra/core/tools";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
const log = logUtils.logger("mastra/tool/send-email");
|
|
32
|
+
const emailInputSchema = z.object({
|
|
33
|
+
to: z.string().describe(stringUtils.toDescription `
|
|
34
|
+
Single recipient email address (e.g. "alice@example.com"). For
|
|
35
|
+
multiple recipients, comma-separate them yourself.
|
|
36
|
+
`),
|
|
37
|
+
subject: z.string().describe(stringUtils.toDescription `
|
|
38
|
+
Subject line.
|
|
39
|
+
`),
|
|
40
|
+
body: z.string().describe(stringUtils.toDescription `
|
|
41
|
+
Email body. Plain text or markdown; the renderer downstream
|
|
42
|
+
decides which to honour. Be specific - the recipient may not
|
|
43
|
+
have any context the model has from prior chat turns.
|
|
44
|
+
`),
|
|
45
|
+
cc: z
|
|
46
|
+
.array(z.string())
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(stringUtils.toDescription `
|
|
49
|
+
Optional CC recipients.
|
|
50
|
+
`),
|
|
51
|
+
bcc: z
|
|
52
|
+
.array(z.string())
|
|
53
|
+
.optional()
|
|
54
|
+
.describe(stringUtils.toDescription `
|
|
55
|
+
Optional BCC recipients.
|
|
56
|
+
`),
|
|
57
|
+
});
|
|
58
|
+
const emailOutputSchema = z.object({
|
|
59
|
+
sent: z.boolean().describe(stringUtils.toDescription `
|
|
60
|
+
True when the email was dispatched. The current implementation
|
|
61
|
+
always returns true after console-logging the would-be email;
|
|
62
|
+
swap in a real provider to make this meaningful.
|
|
63
|
+
`),
|
|
64
|
+
recipient: z.string().describe(stringUtils.toDescription `
|
|
65
|
+
Echo of the \`to\` field for confirmation.
|
|
66
|
+
`),
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Build the `send_email` tool. Approval-gated by default; the
|
|
70
|
+
* execute body either calls the supplied {@link send} hook or
|
|
71
|
+
* logs the email to the server console as a demo stub.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { buildEmailTool, createAgent, mastra } from "@dbx-tools/appkit-mastra";
|
|
76
|
+
*
|
|
77
|
+
* const support = createAgent({
|
|
78
|
+
* instructions: "...",
|
|
79
|
+
* tools(plugins) {
|
|
80
|
+
* return {
|
|
81
|
+
* ...(plugins.genie?.toolkit() ?? {}),
|
|
82
|
+
* send_email: buildEmailTool(),
|
|
83
|
+
* };
|
|
84
|
+
* },
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function buildEmailTool(opts = {}) {
|
|
89
|
+
return createTool({
|
|
90
|
+
id: opts.id ?? "send_email",
|
|
91
|
+
description: stringUtils.toDescription `
|
|
92
|
+
Send an email on the user's behalf. Pass a recipient
|
|
93
|
+
address, subject, and body; the user will be prompted to
|
|
94
|
+
approve the send before it goes out (the tool is
|
|
95
|
+
approval-gated). Use this when the user explicitly asks
|
|
96
|
+
to send / forward / share something via email - never
|
|
97
|
+
autonomously. Keep subjects short and bodies focused; the
|
|
98
|
+
recipient may not have any of the chat context.
|
|
99
|
+
`,
|
|
100
|
+
inputSchema: emailInputSchema,
|
|
101
|
+
outputSchema: emailOutputSchema,
|
|
102
|
+
requireApproval: true,
|
|
103
|
+
execute: async (input) => {
|
|
104
|
+
const { to, subject, body, cc, bcc } = input;
|
|
105
|
+
// Default behaviour: dump the email to the server console so
|
|
106
|
+
// demos can see the gate fire end-to-end without a real
|
|
107
|
+
// provider. Replace by passing `opts.send`.
|
|
108
|
+
log.info("send", {
|
|
109
|
+
to,
|
|
110
|
+
...(cc && cc.length > 0 ? { cc } : {}),
|
|
111
|
+
...(bcc && bcc.length > 0 ? { bcc } : {}),
|
|
112
|
+
subject,
|
|
113
|
+
bodyLength: body.length,
|
|
114
|
+
body,
|
|
115
|
+
});
|
|
116
|
+
if (opts.send) {
|
|
117
|
+
await opts.send(input);
|
|
118
|
+
}
|
|
119
|
+
return { sent: true, recipient: to };
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|