@dbx-tools/appkit-mastra 0.1.5 → 0.1.12
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 +728 -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.js +15 -2
- package/dist/src/model.js +18 -14
- package/dist/src/plugin.d.ts +1 -1
- package/dist/src/plugin.js +9 -3
- 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 +21 -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 +19 -2
- package/src/model.ts +18 -13
- package/src/plugin.ts +10 -3
- 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,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
|
+
}
|