@anuma/sdk 6.33.0 → 6.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/design/index.cjs +0 -2
- package/dist/design/index.d.mts +0 -7
- package/dist/design/index.d.ts +0 -7
- package/dist/design/index.mjs +0 -2
- package/dist/expo/{chunk-XD6WOQ5I.mjs → chunk-GQBTAM6Y.mjs} +0 -70
- package/dist/expo/{consolidate-7DBUHQRK.mjs → consolidate-3H2WY3FU.mjs} +1 -8
- package/dist/expo/index.cjs +15401 -16279
- package/dist/expo/index.d.mts +5735 -6661
- package/dist/expo/index.d.ts +5735 -6661
- package/dist/expo/index.mjs +15238 -16035
- package/dist/index.cjs +2258 -2411
- package/dist/index.d.mts +914 -1054
- package/dist/index.d.ts +914 -1054
- package/dist/index.mjs +2258 -2409
- package/dist/memory/context.d.mts +0 -73
- package/dist/memory/context.d.ts +0 -73
- package/dist/next/index.cjs +1 -8
- package/dist/next/index.mjs +1 -8
- package/dist/pii/transformers.d.mts +0 -32
- package/dist/pii/transformers.d.ts +0 -32
- package/dist/react/{chunk-XNQRJBIH.mjs → chunk-UFO4W3JX.mjs} +0 -70
- package/dist/react/{consolidate-5DVINCUE.mjs → consolidate-RHIAUBT4.mjs} +1 -8
- package/dist/react/index.cjs +32429 -33403
- package/dist/react/index.d.mts +13355 -14554
- package/dist/react/index.d.ts +13355 -14554
- package/dist/react/index.mjs +32555 -33450
- package/dist/server/{chunk-KSYV7UO2.mjs → chunk-4RGGAOAJ.mjs} +835 -905
- package/dist/server/{consolidate-MCNBSUXD.mjs → consolidate-YEGACWLR.mjs} +1 -8
- package/dist/server/index.cjs +22308 -23109
- package/dist/server/index.d.mts +5355 -6302
- package/dist/server/index.d.ts +5355 -6302
- package/dist/server/index.mjs +20543 -21279
- package/dist/telemetry/index.cjs +0 -9
- package/dist/telemetry/index.d.mts +88 -198
- package/dist/telemetry/index.d.ts +88 -198
- package/dist/telemetry/index.mjs +0 -9
- package/dist/tools/index.cjs +13036 -13347
- package/dist/tools/index.d.mts +2434 -2729
- package/dist/tools/index.d.ts +2434 -2729
- package/dist/tools/index.mjs +13036 -13347
- package/dist/tools/selection/index.cjs +0 -116
- package/dist/tools/selection/index.d.mts +0 -159
- package/dist/tools/selection/index.d.ts +0 -159
- package/dist/tools/selection/index.mjs +0 -116
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
createClientConfig
|
|
4
4
|
} from "./chunk-4ETHWSXJ.mjs";
|
|
5
5
|
|
|
6
|
+
// src/constants/index.ts
|
|
7
|
+
var MULTILINE_FIELD_MAX = 5e4;
|
|
8
|
+
|
|
6
9
|
// src/client/core/bodySerializer.gen.ts
|
|
7
10
|
var jsonBodySerializer = {
|
|
8
11
|
bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value)
|
|
@@ -1129,440 +1132,610 @@ var postAuthOauthByProviderRevoke = (options) => (options.client ?? client).post
|
|
|
1129
1132
|
});
|
|
1130
1133
|
var getHealth = (options) => (options?.client ?? client).get({ url: "/health", ...options });
|
|
1131
1134
|
|
|
1132
|
-
// src/lib/
|
|
1133
|
-
var
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
async function withDeadline(run, ms, what, parent) {
|
|
1140
|
-
if (!(ms > 0) || !Number.isFinite(ms)) return Promise.resolve().then(() => run(parent));
|
|
1141
|
-
const controller = new AbortController();
|
|
1142
|
-
const onParentAbort = () => controller.abort(parent?.reason);
|
|
1143
|
-
let timer;
|
|
1144
|
-
try {
|
|
1145
|
-
if (parent?.aborted) onParentAbort();
|
|
1146
|
-
else parent?.addEventListener("abort", onParentAbort, { once: true });
|
|
1147
|
-
const work = Promise.resolve().then(() => run(controller.signal));
|
|
1148
|
-
work.catch(() => {
|
|
1149
|
-
});
|
|
1150
|
-
const deadline = new Promise((_, reject) => {
|
|
1151
|
-
timer = setTimeout(() => {
|
|
1152
|
-
const err = new Error(`${what} timed out after ${ms}ms`);
|
|
1153
|
-
err.name = "TimeoutError";
|
|
1154
|
-
controller.abort(err);
|
|
1155
|
-
reject(err);
|
|
1156
|
-
}, ms);
|
|
1157
|
-
});
|
|
1158
|
-
return await Promise.race([work, deadline]);
|
|
1159
|
-
} finally {
|
|
1160
|
-
clearTimeout(timer);
|
|
1161
|
-
parent?.removeEventListener("abort", onParentAbort);
|
|
1162
|
-
}
|
|
1135
|
+
// src/lib/chat/fileContext.ts
|
|
1136
|
+
var ATTACHED_FILES_OPEN_TAG = "<attached_files>";
|
|
1137
|
+
var ATTACHED_FILES_CLOSE_TAG = "</attached_files>";
|
|
1138
|
+
var ATTACHED_FILES_PREFIX = `${ATTACHED_FILES_OPEN_TAG}
|
|
1139
|
+
The user attached the following file(s) to this message. Their extracted contents are below; when the user refers to "the attached file", "this file" or "the document", they mean these.`;
|
|
1140
|
+
function isAttachedFilesText(text) {
|
|
1141
|
+
return typeof text === "string" && text.startsWith(ATTACHED_FILES_PREFIX) && text.endsWith(ATTACHED_FILES_CLOSE_TAG);
|
|
1163
1142
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
() => getToken(),
|
|
1170
|
-
options.tokenTimeoutMs ?? DEFAULT_EMBEDDING_TOKEN_TIMEOUT_MS,
|
|
1171
|
-
"embedding auth token read",
|
|
1172
|
-
parent
|
|
1173
|
-
);
|
|
1174
|
-
if (!token) {
|
|
1175
|
-
throw new Error("No token available for embedding generation");
|
|
1176
|
-
}
|
|
1177
|
-
return { Authorization: `Bearer ${token}` };
|
|
1143
|
+
function buildAttachedFilesText(fileContext) {
|
|
1144
|
+
return `${ATTACHED_FILES_PREFIX}
|
|
1145
|
+
|
|
1146
|
+
${fileContext}
|
|
1147
|
+
${ATTACHED_FILES_CLOSE_TAG}`;
|
|
1178
1148
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
let
|
|
1182
|
-
let
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
threw = false;
|
|
1187
|
-
last = await call();
|
|
1188
|
-
if (!last.error) return last;
|
|
1189
|
-
const status = last.response?.status;
|
|
1190
|
-
const retryable = status === void 0 || status === 429 || status >= 500;
|
|
1191
|
-
if (!retryable) return last;
|
|
1192
|
-
} catch (err) {
|
|
1193
|
-
threw = true;
|
|
1194
|
-
lastThrown = err;
|
|
1195
|
-
}
|
|
1196
|
-
if (attempt < EMBED_MAX_ATTEMPTS) {
|
|
1197
|
-
const base = 250 * 2 ** (attempt - 1);
|
|
1198
|
-
await new Promise((r) => setTimeout(r, base + Math.random() * 0.4 * base));
|
|
1149
|
+
function attachFileContextToLastUserMessage(messages, fileContext) {
|
|
1150
|
+
if (!fileContext.trim()) return messages;
|
|
1151
|
+
let lastUserIndex = -1;
|
|
1152
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1153
|
+
if (messages[i].role === "user") {
|
|
1154
|
+
lastUserIndex = i;
|
|
1155
|
+
break;
|
|
1199
1156
|
}
|
|
1200
1157
|
}
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1158
|
+
if (lastUserIndex === -1) return messages;
|
|
1159
|
+
const message = messages[lastUserIndex];
|
|
1160
|
+
const existing = message.content ?? [];
|
|
1161
|
+
let insertAt = 0;
|
|
1162
|
+
existing.forEach((part, index) => {
|
|
1163
|
+
if (part.type === "text") insertAt = index + 1;
|
|
1164
|
+
});
|
|
1165
|
+
const filePart = {
|
|
1166
|
+
type: "text",
|
|
1167
|
+
text: buildAttachedFilesText(fileContext)
|
|
1168
|
+
};
|
|
1169
|
+
const content = [...existing.slice(0, insertAt), filePart, ...existing.slice(insertAt)];
|
|
1170
|
+
const result = [...messages];
|
|
1171
|
+
result[lastUserIndex] = { ...message, content };
|
|
1172
|
+
return result;
|
|
1206
1173
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1174
|
+
|
|
1175
|
+
// src/lib/tools/toolCatalog.ts
|
|
1176
|
+
var TOOL_CATALOG = {
|
|
1177
|
+
gmail_search_messages: { label: "Search emails", provider: "gmail", connector: "Gmail" },
|
|
1178
|
+
gmail_get_message: { label: "Read an email", provider: "gmail", connector: "Gmail" },
|
|
1179
|
+
gmail_create_draft: { label: "Draft a reply", provider: "gmail", connector: "Gmail" },
|
|
1180
|
+
gmail_send_message: { label: "Send an email", provider: "gmail", connector: "Gmail" },
|
|
1181
|
+
google_calendar_list_events: {
|
|
1182
|
+
label: "Search calendar events",
|
|
1183
|
+
provider: "gcalendar",
|
|
1184
|
+
connector: "Calendar"
|
|
1185
|
+
},
|
|
1186
|
+
google_calendar_create_event: {
|
|
1187
|
+
label: "Create calendar event",
|
|
1188
|
+
provider: "gcalendar",
|
|
1189
|
+
connector: "Calendar"
|
|
1190
|
+
},
|
|
1191
|
+
google_calendar_update_event: {
|
|
1192
|
+
label: "Update calendar event",
|
|
1193
|
+
provider: "gcalendar",
|
|
1194
|
+
connector: "Calendar"
|
|
1195
|
+
},
|
|
1196
|
+
google_drive_search: { label: "Search files", provider: "gdrive", connector: "Drive" },
|
|
1197
|
+
google_drive_list_recent: { label: "List recent files", provider: "gdrive", connector: "Drive" },
|
|
1198
|
+
google_drive_get_content: { label: "Read file content", provider: "gdrive", connector: "Drive" },
|
|
1199
|
+
google_drive_create_file: {
|
|
1200
|
+
label: "Create a file in Drive",
|
|
1201
|
+
provider: "gdrive",
|
|
1202
|
+
connector: "Drive"
|
|
1203
|
+
},
|
|
1204
|
+
google_drive_update_file: {
|
|
1205
|
+
label: "Update a Drive file",
|
|
1206
|
+
provider: "gdrive",
|
|
1207
|
+
connector: "Drive"
|
|
1208
|
+
},
|
|
1209
|
+
"notion-search": { label: "Search", provider: "notion", connector: "Notion" },
|
|
1210
|
+
"notion-fetch": { label: "Read page", provider: "notion", connector: "Notion" },
|
|
1211
|
+
"notion-create-pages": { label: "Create page", provider: "notion", connector: "Notion" },
|
|
1212
|
+
"notion-update-page": { label: "Update page", provider: "notion", connector: "Notion" },
|
|
1213
|
+
"notion-move-pages": { label: "Move pages", provider: "notion", connector: "Notion" },
|
|
1214
|
+
"notion-duplicate-page": { label: "Duplicate page", provider: "notion", connector: "Notion" },
|
|
1215
|
+
"notion-create-database": { label: "Create database", provider: "notion", connector: "Notion" },
|
|
1216
|
+
"notion-update-data-source": {
|
|
1217
|
+
label: "Update data source",
|
|
1218
|
+
provider: "notion",
|
|
1219
|
+
connector: "Notion"
|
|
1220
|
+
},
|
|
1221
|
+
"notion-create-comment": { label: "Add a comment", provider: "notion", connector: "Notion" },
|
|
1222
|
+
"notion-get-comments": { label: "Read comments", provider: "notion", connector: "Notion" },
|
|
1223
|
+
"notion-get-users": {
|
|
1224
|
+
label: "List workspace members",
|
|
1225
|
+
provider: "notion",
|
|
1226
|
+
connector: "Notion"
|
|
1227
|
+
},
|
|
1228
|
+
"notion-get-teams": { label: "List teamspaces", provider: "notion", connector: "Notion" },
|
|
1229
|
+
github_api: { label: "Use GitHub", provider: "github", connector: "GitHub" },
|
|
1230
|
+
github_get_authenticated_user: {
|
|
1231
|
+
label: "Read GitHub profile",
|
|
1232
|
+
provider: "github",
|
|
1233
|
+
connector: "GitHub"
|
|
1234
|
+
},
|
|
1235
|
+
x_get_me: { label: "Get my X profile", provider: "x", connector: "X" },
|
|
1236
|
+
x_get_my_posts: { label: "Read my recent posts", provider: "x", connector: "X" },
|
|
1237
|
+
dropbox_list_folders: { label: "List files", provider: "dropbox", connector: "Dropbox" },
|
|
1238
|
+
dropbox_get_file_content: {
|
|
1239
|
+
label: "Read file content",
|
|
1240
|
+
provider: "dropbox",
|
|
1241
|
+
connector: "Dropbox"
|
|
1242
|
+
},
|
|
1243
|
+
dropbox_search: { label: "Search files", provider: "dropbox", connector: "Dropbox" },
|
|
1244
|
+
slack_get_me: { label: "Get my Slack profile", provider: "slack", connector: "Slack" },
|
|
1245
|
+
slack_list_channels: { label: "List channels", provider: "slack", connector: "Slack" },
|
|
1246
|
+
slack_list_dms: { label: "List direct messages", provider: "slack", connector: "Slack" },
|
|
1247
|
+
slack_search_messages: { label: "Search messages", provider: "slack", connector: "Slack" },
|
|
1248
|
+
slack_list_users: { label: "List members", provider: "slack", connector: "Slack" },
|
|
1249
|
+
slack_get_channel_history: {
|
|
1250
|
+
label: "Read channel history",
|
|
1251
|
+
provider: "slack",
|
|
1252
|
+
connector: "Slack"
|
|
1253
|
+
},
|
|
1254
|
+
slack_get_thread_replies: {
|
|
1255
|
+
label: "Read thread replies",
|
|
1256
|
+
provider: "slack",
|
|
1257
|
+
connector: "Slack"
|
|
1258
|
+
},
|
|
1259
|
+
slack_post_message: { label: "Post a message", provider: "slack", connector: "Slack" }
|
|
1213
1260
|
};
|
|
1214
|
-
function
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1261
|
+
function buildDeniedToolsRider(deniedToolNames) {
|
|
1262
|
+
const byConnector = /* @__PURE__ */ new Map();
|
|
1263
|
+
for (const name of deniedToolNames) {
|
|
1264
|
+
const entry = TOOL_CATALOG[name];
|
|
1265
|
+
if (!entry) continue;
|
|
1266
|
+
const labels = byConnector.get(entry.connector) ?? [];
|
|
1267
|
+
labels.push(entry.label);
|
|
1268
|
+
byConnector.set(entry.connector, labels);
|
|
1269
|
+
}
|
|
1270
|
+
if (byConnector.size === 0) return "";
|
|
1271
|
+
const groups = [...byConnector.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
|
|
1272
|
+
([connector, labels]) => `- ${connector}: ${[...labels].sort((a, b) => a.localeCompare(b)).join(", ")}`
|
|
1273
|
+
).join("\n");
|
|
1274
|
+
return [
|
|
1275
|
+
"The user has turned off the following connector capabilities:",
|
|
1276
|
+
groups,
|
|
1277
|
+
"If the user asks you to use one of these, do not attempt it. Tell them the specific capability is turned off and that they can re-enable that exact tool in their Connected Apps settings."
|
|
1278
|
+
].join("\n");
|
|
1217
1279
|
}
|
|
1218
|
-
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1280
|
+
function toolName(tool) {
|
|
1281
|
+
const record = tool;
|
|
1282
|
+
const func = record.function;
|
|
1283
|
+
if (typeof func?.name === "string") return func.name;
|
|
1284
|
+
if (typeof record.name === "string") return record.name;
|
|
1285
|
+
return void 0;
|
|
1222
1286
|
}
|
|
1223
|
-
|
|
1224
|
-
const
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
const cached = cache.get(text);
|
|
1228
|
-
if (cached) return Array.from(cached);
|
|
1287
|
+
function catalogConnectorsByProvider() {
|
|
1288
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const entry of Object.values(TOOL_CATALOG)) {
|
|
1290
|
+
byProvider.set(entry.provider, entry.connector);
|
|
1229
1291
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
outer
|
|
1247
|
-
),
|
|
1248
|
-
outer
|
|
1249
|
-
);
|
|
1250
|
-
if (response.error) {
|
|
1251
|
-
throw embeddingErrorFrom(response);
|
|
1292
|
+
return byProvider;
|
|
1293
|
+
}
|
|
1294
|
+
function buildConnectorGuidance(input) {
|
|
1295
|
+
const denied = new Set(input.deniedToolNames);
|
|
1296
|
+
const connected = new Set(input.connectedProviders);
|
|
1297
|
+
const tools = input.tools.filter((tool) => {
|
|
1298
|
+
const name = toolName(tool);
|
|
1299
|
+
return name === void 0 || !denied.has(name);
|
|
1300
|
+
});
|
|
1301
|
+
const deniedByConnector = /* @__PURE__ */ new Map();
|
|
1302
|
+
for (const name of denied) {
|
|
1303
|
+
const entry = TOOL_CATALOG[name];
|
|
1304
|
+
if (!entry || !connected.has(entry.provider)) continue;
|
|
1305
|
+
const labels = deniedByConnector.get(entry.connector) ?? [];
|
|
1306
|
+
labels.push(entry.label);
|
|
1307
|
+
deniedByConnector.set(entry.connector, labels);
|
|
1252
1308
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1309
|
+
const unconnected = /* @__PURE__ */ new Set();
|
|
1310
|
+
for (const [provider, connector] of catalogConnectorsByProvider()) {
|
|
1311
|
+
if (!connected.has(provider)) unconnected.add(connector);
|
|
1255
1312
|
}
|
|
1256
|
-
const
|
|
1257
|
-
if (
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1313
|
+
const sections = [];
|
|
1314
|
+
if (deniedByConnector.size > 0) {
|
|
1315
|
+
const groups = [...deniedByConnector.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
|
|
1316
|
+
([connector, labels]) => `On ${connector}, these are turned off: ${[...labels].sort((a, b) => a.localeCompare(b)).join(", ")}.`
|
|
1317
|
+
).join("\n");
|
|
1318
|
+
sections.push(
|
|
1319
|
+
`${groups}
|
|
1320
|
+
If the user asks to use one of these, tell them it's turned off and they can re-enable it in Connected Apps.`
|
|
1321
|
+
);
|
|
1262
1322
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1323
|
+
if (unconnected.size > 0) {
|
|
1324
|
+
const list = [...unconnected].sort((a, b) => a.localeCompare(b)).join(", ");
|
|
1325
|
+
sections.push(
|
|
1326
|
+
`Not connected: ${list}. If the user asks to use them, tell them to connect them in Connected Apps.`
|
|
1327
|
+
);
|
|
1266
1328
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
async function generateEmbeddingsBatch(texts, headers, baseUrl, model, onUsage, maskInput, timeoutMs) {
|
|
1272
|
-
const response = await withEmbeddingRetry(
|
|
1273
|
-
() => withDeadline(
|
|
1274
|
-
(signal) => postApiV1Embeddings({
|
|
1275
|
-
baseUrl,
|
|
1276
|
-
// Mask PII from the request body only; cache/order still key on originals.
|
|
1277
|
-
body: { input: maskInput ? texts.map(maskInput) : texts, model },
|
|
1278
|
-
headers,
|
|
1279
|
-
...signal && { signal }
|
|
1280
|
-
}),
|
|
1281
|
-
timeoutMs,
|
|
1282
|
-
"embedding batch request"
|
|
1283
|
-
)
|
|
1284
|
-
);
|
|
1285
|
-
if (response.error) {
|
|
1286
|
-
throw embeddingErrorFrom(response);
|
|
1329
|
+
if (sections.length > 0 && connected.size > 0) {
|
|
1330
|
+
sections.push(
|
|
1331
|
+
"Never tell the user you lack access to a connected app; if you can't find a tool for a connected app's request, say that specific capability isn't available yet rather than that the app isn't connected."
|
|
1332
|
+
);
|
|
1287
1333
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1334
|
+
return { tools, rider: sections.join("\n\n") };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/lib/chat/toolResults.ts
|
|
1338
|
+
var TOOL_RESULT_ORIGIN = "tool_result";
|
|
1339
|
+
|
|
1340
|
+
// src/lib/chat/useChat/strategies/types.ts
|
|
1341
|
+
function isChatCompletionResponse(r) {
|
|
1342
|
+
if (r.object === "response") return false;
|
|
1343
|
+
if (Array.isArray(r.output)) return false;
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
function getToolCallEvents(r) {
|
|
1347
|
+
if (isChatCompletionResponse(r)) {
|
|
1348
|
+
return r.portal?.tool_call_events ?? r.tool_call_events;
|
|
1290
1349
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1350
|
+
return r.tool_call_events;
|
|
1351
|
+
}
|
|
1352
|
+
function getCostMicroUsd(r) {
|
|
1353
|
+
if (isChatCompletionResponse(r)) {
|
|
1354
|
+
return r.portal?.cost_micro_usd ?? r.usage?.cost_micro_usd;
|
|
1296
1355
|
}
|
|
1297
|
-
return
|
|
1356
|
+
return r.usage?.cost_micro_usd;
|
|
1298
1357
|
}
|
|
1299
|
-
|
|
1300
|
-
if (
|
|
1301
|
-
|
|
1302
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
|
|
1303
|
-
const chunkSize = batchSize ?? DEFAULT_EMBEDDING_BATCH_SIZE;
|
|
1304
|
-
const results = new Array(texts.length).fill(null);
|
|
1305
|
-
const uncachedIndices = [];
|
|
1306
|
-
const uncachedTexts = [];
|
|
1307
|
-
for (let i = 0; i < texts.length; i++) {
|
|
1308
|
-
if (cache) {
|
|
1309
|
-
const cached = cache.get(texts[i]);
|
|
1310
|
-
if (cached) {
|
|
1311
|
-
results[i] = Array.from(cached);
|
|
1312
|
-
continue;
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
uncachedIndices.push(i);
|
|
1316
|
-
uncachedTexts.push(texts[i]);
|
|
1358
|
+
function getCreditsUsed(r) {
|
|
1359
|
+
if (isChatCompletionResponse(r)) {
|
|
1360
|
+
return r.portal?.credits_used ?? r.usage?.credits_used;
|
|
1317
1361
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1362
|
+
return r.usage?.credits_used;
|
|
1363
|
+
}
|
|
1364
|
+
function getImageModel(r) {
|
|
1365
|
+
if (isChatCompletionResponse(r)) {
|
|
1366
|
+
return r.portal?.image_model ?? r.image_model;
|
|
1320
1367
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
const allEmbeddings = new Array(chunks.length);
|
|
1340
|
-
let nextIndex = 0;
|
|
1341
|
-
const worker = async () => {
|
|
1342
|
-
while (nextIndex < chunks.length) {
|
|
1343
|
-
const idx = nextIndex++;
|
|
1344
|
-
allEmbeddings[idx] = await generateEmbeddingsBatch(
|
|
1345
|
-
chunks[idx],
|
|
1346
|
-
headers,
|
|
1347
|
-
baseUrl,
|
|
1348
|
-
embeddingModel,
|
|
1349
|
-
options.onUsage,
|
|
1350
|
-
options.maskInput,
|
|
1351
|
-
timeoutMs
|
|
1352
|
-
);
|
|
1353
|
-
}
|
|
1354
|
-
};
|
|
1355
|
-
const workers = Array.from(
|
|
1356
|
-
{ length: Math.min(DEFAULT_EMBEDDING_BATCH_CONCURRENCY, chunks.length) },
|
|
1357
|
-
() => worker()
|
|
1358
|
-
);
|
|
1359
|
-
await Promise.all(workers);
|
|
1360
|
-
newEmbeddings = allEmbeddings.flat();
|
|
1368
|
+
return r.image_model;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/lib/logger.ts
|
|
1372
|
+
var consoleLogger = {
|
|
1373
|
+
debug: console.log.bind(console),
|
|
1374
|
+
info: console.info.bind(console),
|
|
1375
|
+
warn: console.warn.bind(console),
|
|
1376
|
+
error: console.error.bind(console)
|
|
1377
|
+
};
|
|
1378
|
+
var noopLogger = {
|
|
1379
|
+
debug: () => {
|
|
1380
|
+
},
|
|
1381
|
+
info: () => {
|
|
1382
|
+
},
|
|
1383
|
+
warn: () => {
|
|
1384
|
+
},
|
|
1385
|
+
error: () => {
|
|
1361
1386
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1387
|
+
};
|
|
1388
|
+
var LOGGER_STATE_KEY = /* @__PURE__ */ Symbol.for("@anuma/sdk/logger/v1");
|
|
1389
|
+
var loggerGlobal = globalThis;
|
|
1390
|
+
var fallbackLoggerState = { logger: consoleLogger };
|
|
1391
|
+
var fallbackWarned = false;
|
|
1392
|
+
function getLoggerState() {
|
|
1393
|
+
try {
|
|
1394
|
+
return loggerGlobal[LOGGER_STATE_KEY] ?? (loggerGlobal[LOGGER_STATE_KEY] = fallbackLoggerState);
|
|
1395
|
+
} catch {
|
|
1396
|
+
if (!fallbackWarned) {
|
|
1397
|
+
fallbackWarned = true;
|
|
1398
|
+
fallbackLoggerState.logger.warn(
|
|
1399
|
+
"@anuma/sdk: the global object rejects new properties. This entrypoint keeps its own logger, so setLogger does not reach other entrypoints."
|
|
1400
|
+
);
|
|
1367
1401
|
}
|
|
1402
|
+
return fallbackLoggerState;
|
|
1368
1403
|
}
|
|
1369
|
-
|
|
1404
|
+
}
|
|
1405
|
+
function setLogger(logger) {
|
|
1406
|
+
getLoggerState().logger = logger;
|
|
1407
|
+
}
|
|
1408
|
+
function getLogger() {
|
|
1409
|
+
return getLoggerState().logger;
|
|
1370
1410
|
}
|
|
1371
1411
|
|
|
1372
|
-
// src/
|
|
1373
|
-
var
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
WORKFLOW:
|
|
1382
|
-
1. NEVER output code as text or markdown. ALWAYS use tools.
|
|
1383
|
-
2. Initial build: state your design brief, then create_file with a "files" array writing ALL files in one call. The preview renders automatically.
|
|
1384
|
-
3. patch_file requires read_file for that path earlier in the conversation (or create_file in the same conversation, which counts). read_file output is numbered "42: <text>" \u2014 the prefix is display-only; never copy it into "find". Each "find" must match exactly one location \u2014 include 2-3 lines of surrounding context. Pass multiple patches per call to change distinct locations.
|
|
1385
|
-
4. Prefer patch_file for changes to existing files \u2014 surgical, reviewable, preserves surrounding code. Reserve create_file overwrites for substantial restructuring. Inserts: find the line before, replace with that line + the new line. Deletes: empty replace.
|
|
1386
|
-
5. After substantial changes, call critique_design first (taste, hierarchy, distinctiveness, weakest decision) and then audit_design (mechanical: tokens, focus states, aria-labels, alt, heading order). Patch what each surfaces. critique catches taste issues no checklist would; audit catches mechanics you'd miss. Run both; act on both.
|
|
1387
|
-
6. Before declaring done, call verify_app. The host's preview runtime reports whether your app actually mounted and surfaces any errors it captured \u2014 hallucinated imports, syntax the bundler rejected, undefined components, runtime crashes. If \`rendered\` is false or \`errors\` is non-empty, fix every error before the next message. If the response includes a \`note\` that verify_app isn't wired in this host, skip \u2014 audit + critique are your feedback in that case.
|
|
1388
|
-
7. Keep responses to a sentence or two. Exception: when responding to critique_design, write a substantive paragraph per rubric question.
|
|
1389
|
-
|
|
1390
|
-
STRUCTURE:
|
|
1391
|
-
- App.js: default-export React component. For non-trivial apps, define small co-located helpers (Header, CardList, Card, EditDialog, \u2026) above the default export rather than one giant App.
|
|
1392
|
-
- App.css: all styles, imported in App.js. No inline style objects.
|
|
1393
|
-
- package.json: list ALL imported packages including react. No CDN script tags. Versions are auto-pinned.
|
|
1394
|
-
- Do NOT create index.js or index.html (auto-generated).
|
|
1395
|
-
|
|
1396
|
-
STATE & PERSISTENCE:
|
|
1397
|
-
- The code runs in the user's browser \u2014 standard browser APIs available (localStorage, URL hash, Date, fetch).
|
|
1398
|
-
- Persist user-created data (todos, cards, notes, settings) with localStorage so it survives refresh. Use a versioned key ("kanban.v1") and JSON.parse inside try/catch on read.
|
|
1399
|
-
- Treat persisted data as untrusted and possibly stale: a saved record may predate a field you just added (a timestamp, id, or setting), so default-fill or skip missing fields on read \u2014 never assume a loaded record has every field the current code expects. Bump the key version when the shape changes.
|
|
1400
|
-
- Lift state to App when multiple children need it; keep it local otherwise. Extract non-trivial logic into custom hooks (useTodos, useTimer).
|
|
1401
|
-
- useReducer for state shapes with several interrelated fields (cards + columns + filters + drag state).
|
|
1402
|
-
|
|
1403
|
-
FILE & IMAGE UPLOAD:
|
|
1404
|
-
- Picker: \`<input type="file" accept="image/*" multiple>\` (vary accept by use case: \`.csv,text/csv\`, \`.txt,.md,text/plain\`, etc.).
|
|
1405
|
-
- Drag-and-drop: call \`e.preventDefault()\` in onDragOver (otherwise drop never fires); read files from \`event.dataTransfer.files\`. Drop and picker should coexist.
|
|
1406
|
-
- Previews: \`<img src={URL.createObjectURL(file)} />\`. Call \`URL.revokeObjectURL\` on unmount or before replacing \u2014 otherwise the browser leaks memory.
|
|
1407
|
-
- localStorage cannot hold large binary. Persist only metadata for big files; re-upload on next visit (or use IndexedDB if you really need durable binary storage).
|
|
1408
|
-
- Validate size and type before processing; reject files larger than the app can hold in memory.
|
|
1409
|
-
|
|
1410
|
-
AI CAPABILITIES \u2014 \`window.app.complete(prompt: string): Promise<string>\` is available in this environment. Build for the real call.
|
|
1411
|
-
- When the app's core purpose is conversation, generation, grading, recommendation, or any natural-language task, this IS the implementation \u2014 call it. NEVER fake it: no hardcoded or canned replies, no random or \`setTimeout\` "thinking", no demo-mode stub. A chat app, tutor, or assistant that doesn't call \`window.app.complete\` is wrong, not a placeholder.
|
|
1412
|
-
- Don't force it where it isn't needed \u2014 a calculator, todo list, or timer has no reason to call it. Match the implementation to what the app actually does.
|
|
1413
|
-
- Async: wrap in try/catch, show a loading state, and disable dependent inputs while waiting. Surface failures gracefully ("Couldn't reach the AI \u2014 try again").
|
|
1414
|
-
- Structure prompts with clear sections (Role / Context / Task / Format) and pass the relevant state (e.g. the running conversation) as context. Render the response as untrusted text: \`white-space: pre-wrap\`, never dangerouslySetInnerHTML.
|
|
1415
|
-
- Skip the call only when the answer is genuinely static \u2014 no language or reasoning needed. Cache repeated prompts in component state when sensible.
|
|
1416
|
-
|
|
1417
|
-
CODE QUALITY \u2014 write as a senior engineer would:
|
|
1418
|
-
- Handle edge cases (empty inputs, division by zero, invalid data). Show helpful error states, not crashes.
|
|
1419
|
-
- Never let formatting throw during render: guard \`new Date(x)\` / \`Intl\` / \`toLocaleString\` / \`.toFixed\` against missing or invalid values (e.g. \`Number.isNaN(d.getTime())\`) and return a fallback \u2014 one bad value from storage or state must not blank the whole app.
|
|
1420
|
-
- Semantic HTML: real headings, labels, buttons (not divs with onClick), form elements.
|
|
1421
|
-
- Accessible: aria-labels on icon buttons, focus-visible styles, keyboard navigable.
|
|
1422
|
-
- Extract into custom hooks or helpers when a component grows past ~80 lines.
|
|
1423
|
-
|
|
1424
|
-
STYLING \u2014 Tailwind is the CHASSIS, custom CSS carries the IDENTITY:
|
|
1425
|
-
- Tailwind (via Play CDN) for layout, spacing, state (hover/focus), responsive prefixes \u2014 uniform structure.
|
|
1426
|
-
- App.css carries the identity: @import the Google Fonts, declare CSS custom properties (--bg, --panel, --card, --ink, --muted, --accent, --hairline, --radius, --radius-sm, --shadow-card, --shadow-card-hi) in :root, gradients/textures/signature animations Tailwind can't express. Reference tokens from Tailwind via arbitrary values (\`className="bg-[var(--bg)]"\`) when needed. A well-designed app has 60-200 lines of App.css \u2014 not zero.
|
|
1427
|
-
- Modern CSS is how apps stop reading as generic-AI-output: \`color-mix(in oklab, var(--accent) 14%, transparent)\` for tinting, \`oklch()\` / \`oklab()\` for perceptually-uniform color, \`:has()\`, \`conic-gradient\` / \`radial-gradient\`, \`backdrop-filter\`, \`@container\` queries, inline SVG \`feTurbulence\` for texture.
|
|
1428
|
-
- Don't pull additional CSS frameworks. Tailwind + custom CSS is enough.
|
|
1429
|
-
|
|
1430
|
-
VISUAL DESIGN \u2014 every app should look like a real product, not a wireframe:
|
|
1431
|
-
- Let the chosen aesthetic drive every decision. "Editorial" wants italic display type and restrained color; "retro arcade" wants chunky shadows and saturated gradients. No safe-modern defaults.
|
|
1432
|
-
- Atmospheric depth: subtle radial wash, paper grain (inline SVG noise), warm/cool tint \u2014 every canvas should feel like a designed space, not a #fff rectangle.
|
|
1433
|
-
- Three shadow tiers (rest / hover / active). Pair drop shadows with inset highlights (\`box-shadow: 0 1px 0 rgba(255,255,255,.9) inset, 0 6px 14px -10px ...\`) for tactile feel.
|
|
1434
|
-
- Typography hierarchy: display font for headlines (italic accents on key words via \`<em>\`), body font for content, mono for metadata/labels \u2014 different families telegraph different information types.
|
|
1435
|
-
- Interactive polish: hover/active/focus on every clickable element, 200-300ms cubic-bezier transitions, keyboard shortcuts where natural (Esc to cancel, Cmd/Ctrl+Enter to commit), focus-visible rings keyed to the accent.
|
|
1436
|
-
- Light/dark mode if reasonable: a single CSS variable inversion under \`[data-theme="dark"]\`.
|
|
1437
|
-
- Customization affordance for multi-session apps: small "Tweaks" panel with theme / density / accent. Skip for throwaway demos.
|
|
1438
|
-
- Responsive: Tailwind responsive prefixes (\`sm:\`, \`md:\`, \`lg:\`) and \`@media\` in App.css. Mobile-first.`;
|
|
1439
|
-
|
|
1440
|
-
// src/tools/document/documentBuilderPrompt.ts
|
|
1441
|
-
var DOCUMENT_BUILDER_PROMPT = `Document tools (create_document, read_document, patch_document) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should produce a document.
|
|
1442
|
-
|
|
1443
|
-
Use them ONLY when the user explicitly asks you to draft, write, compose, or generate a document \u2014 a contract, agreement, NDA, letter, cover letter, memo, essay, report, brief, proposal, or resume \u2014 e.g. "draft me a service agreement", "write a cover letter for this job", "make a one-page memo about X". A bare topic, a question, a request for an explanation, or a request for a slide deck or an interactive app is NOT a document instruction: respond normally and do not call these tools. When it's unclear whether the user wants a finished document, ask instead.
|
|
1444
|
-
|
|
1445
|
-
When you DO author a document, the document body goes entirely into the tool \u2014 NEVER write the document text as chat prose. The tool renders your source to a real, paginated PDF and attaches it to your reply automatically. AFTER the tool returns, briefly confirm in ONE short sentence what you made (e.g. "Drafted a cover letter for the product manager role \u2014 see the attached PDF."). Do not restate the document's contents.
|
|
1446
|
-
|
|
1447
|
-
AUTHORING \u2014 write the "source" as a single @react-pdf/renderer JSX expression (this is react-pdf, NOT HTML and NOT a slide deck):
|
|
1448
|
-
- Root is exactly one <Document> with at least one <Page>. Its only children are <Page> elements. Add more <Page> elements for longer documents \u2014 react-pdf also paginates overflow automatically.
|
|
1449
|
-
- Inside a <Page>: use <View> as flex containers and <Text> for ALL text. Raw text is only allowed inside <Text> (and <Link>); a <View> or <Page> may not contain bare text.
|
|
1450
|
-
- <Text> may nest <Text>, <Link>, or <Note> for inline styling, hyperlinks, and (rarely) a PDF comment annotation. A <Link> src must be an ordinary web link \u2014 http(s), mailto, tel, or a relative/anchor link; javascript:/data:/file: links are rejected. <Image> is self-closing and its src MUST be an inline "data:" URI \u2014 remote image URLs are rejected for privacy. Vector primitives (<Rect>/<Circle>/<Path>/etc.) must live inside an <Svg>.
|
|
1451
|
-
- Styling is react-pdf style objects only: style={{ marginTop: 12, fontSize: 11, fontFamily: "Times-Roman", flexDirection: "column" }}. Flexbox only \u2014 there is NO CSS grid, float, or box-shadow. Use camelCase react-pdf keys (margin/padding, flex*, width/height, color/backgroundColor, border*, text*, etc.).
|
|
1452
|
-
- fontFamily must be a built-in PDF font: Helvetica, Times-Roman, or Courier (with -Bold / -Oblique / -Italic / -BoldOblique variants), or Symbol / ZapfDingbats. Custom fonts are not supported.
|
|
1453
|
-
- LITERAL VALUES ONLY: no variables, no JS expressions, no .map(), no event handlers (onClick, \u2026), no spreads. Every attribute is a literal string, number, boolean, or a style object of literals.
|
|
1454
|
-
|
|
1455
|
-
A minimal example:
|
|
1456
|
-
|
|
1457
|
-
<Document>
|
|
1458
|
-
<Page size="A4" style={{ paddingVertical: 56, paddingHorizontal: 64, fontFamily: "Times-Roman", fontSize: 11, lineHeight: 1.5 }}>
|
|
1459
|
-
<Text style={{ fontSize: 18, fontFamily: "Times-Bold", marginBottom: 16, textAlign: "center" }}>MUTUAL NON-DISCLOSURE AGREEMENT</Text>
|
|
1460
|
-
<Text style={{ marginBottom: 10 }}>This Mutual Non-Disclosure Agreement (the "Agreement") is entered into as of the date of last signature below.</Text>
|
|
1461
|
-
<View style={{ marginTop: 14 }}>
|
|
1462
|
-
<Text style={{ fontFamily: "Times-Bold", marginBottom: 4 }}>1. Confidential Information</Text>
|
|
1463
|
-
<Text>Each party may disclose certain confidential and proprietary information to the other party.</Text>
|
|
1464
|
-
</View>
|
|
1465
|
-
</Page>
|
|
1466
|
-
</Document>
|
|
1467
|
-
|
|
1468
|
-
WORKFLOW:
|
|
1469
|
-
1. To create or fully restructure a document, call create_document with the complete source.
|
|
1470
|
-
2. To revise an existing document, call read_document first (its output is line-numbered "42: <text>" \u2014 the prefix is display-only, never copy it into a "find"), then patch_document with small find/replace edits. Each "find" must match exactly one location; add 2-3 lines of surrounding context if needed. Prefer patching over rewriting.
|
|
1471
|
-
3. To attach several separate documents in one reply, call create_document multiple times with distinct documentId slugs (e.g. "nda", "cover-letter"). Each renders to its own PDF.
|
|
1472
|
-
4. If a tool reports a DSL error (with line:col) or a render failure, fix the source and retry \u2014 do not give up or fall back to writing the document as chat text.`;
|
|
1473
|
-
|
|
1474
|
-
// src/lib/logger.ts
|
|
1475
|
-
var consoleLogger = {
|
|
1476
|
-
debug: console.log.bind(console),
|
|
1477
|
-
info: console.info.bind(console),
|
|
1478
|
-
warn: console.warn.bind(console),
|
|
1479
|
-
error: console.error.bind(console)
|
|
1480
|
-
};
|
|
1481
|
-
var noopLogger = {
|
|
1482
|
-
debug: () => {
|
|
1483
|
-
},
|
|
1484
|
-
info: () => {
|
|
1485
|
-
},
|
|
1486
|
-
warn: () => {
|
|
1487
|
-
},
|
|
1488
|
-
error: () => {
|
|
1412
|
+
// src/react/useEncryption.ts
|
|
1413
|
+
var EncryptionKeyMissingError = class extends Error {
|
|
1414
|
+
constructor(address, version) {
|
|
1415
|
+
super(
|
|
1416
|
+
`Encryption key (${version}) not found for ${address}. Please sign a message to generate your encryption key.`
|
|
1417
|
+
);
|
|
1418
|
+
this.name = "EncryptionKeyMissingError";
|
|
1419
|
+
this.address = address;
|
|
1420
|
+
this.version = version;
|
|
1489
1421
|
}
|
|
1490
1422
|
};
|
|
1491
|
-
var
|
|
1492
|
-
var
|
|
1493
|
-
var
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1423
|
+
var encryptionKeyStore = /* @__PURE__ */ new Map();
|
|
1424
|
+
var cryptoKeyCache = /* @__PURE__ */ new Map();
|
|
1425
|
+
var clearAllEncryptionStateCallbacks = /* @__PURE__ */ new Set();
|
|
1426
|
+
function onClearAllEncryptionState(callback) {
|
|
1427
|
+
clearAllEncryptionStateCallbacks.add(callback);
|
|
1428
|
+
return () => {
|
|
1429
|
+
clearAllEncryptionStateCallbacks.delete(callback);
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function getStoredKeyByVersion(address, version) {
|
|
1433
|
+
const keys = encryptionKeyStore.get(address);
|
|
1434
|
+
if (!keys) return null;
|
|
1435
|
+
const value = version === "v2" ? keys.legacy : keys.current;
|
|
1436
|
+
return value ?? null;
|
|
1437
|
+
}
|
|
1438
|
+
function hexNibble(code) {
|
|
1439
|
+
if (code >= 48 && code <= 57) return code - 48;
|
|
1440
|
+
if (code >= 65 && code <= 70) return code - 55;
|
|
1441
|
+
if (code >= 97 && code <= 102) return code - 87;
|
|
1442
|
+
return -1;
|
|
1443
|
+
}
|
|
1444
|
+
function hexToBytes(hex) {
|
|
1445
|
+
if (typeof hex !== "string") {
|
|
1446
|
+
throw new TypeError(
|
|
1447
|
+
"hexToBytes: expected a hex string. Pass raw signature bytes to deriveKeyFromSignatureBytes instead of stringifying them."
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1451
|
+
if (cleanHex.length === 0 || cleanHex.length % 2 !== 0) {
|
|
1452
|
+
throw new Error(
|
|
1453
|
+
`hexToBytes: invalid hex string (expected a non-empty even length, got ${cleanHex.length})`
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
1457
|
+
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
1458
|
+
const hi = hexNibble(cleanHex.charCodeAt(i));
|
|
1459
|
+
const lo = hexNibble(cleanHex.charCodeAt(i + 1));
|
|
1460
|
+
if (hi < 0 || lo < 0) {
|
|
1461
|
+
throw new Error(`hexToBytes: invalid hex string (non-hex character at index ${i})`);
|
|
1504
1462
|
}
|
|
1505
|
-
|
|
1463
|
+
bytes[i / 2] = hi << 4 | lo;
|
|
1506
1464
|
}
|
|
1465
|
+
return bytes;
|
|
1507
1466
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1467
|
+
var SHARED_TEXT_ENCODER = new TextEncoder();
|
|
1468
|
+
var SHARED_TEXT_DECODER = new TextDecoder();
|
|
1469
|
+
async function getEncryptionKey(address, version = "v3") {
|
|
1470
|
+
const cacheKey = `${address}:${version}`;
|
|
1471
|
+
const cachedKey = cryptoKeyCache.get(cacheKey);
|
|
1472
|
+
if (cachedKey) {
|
|
1473
|
+
return cachedKey;
|
|
1474
|
+
}
|
|
1475
|
+
const keyHex = getStoredKeyByVersion(address, version);
|
|
1476
|
+
if (!keyHex) {
|
|
1477
|
+
throw new EncryptionKeyMissingError(address, version);
|
|
1478
|
+
}
|
|
1479
|
+
const keyBytes = hexToBytes(keyHex);
|
|
1480
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
1481
|
+
"raw",
|
|
1482
|
+
keyBytes.buffer,
|
|
1483
|
+
{ name: "AES-GCM" },
|
|
1484
|
+
false,
|
|
1485
|
+
["encrypt", "decrypt"]
|
|
1486
|
+
);
|
|
1487
|
+
cryptoKeyCache.set(cacheKey, cryptoKey);
|
|
1488
|
+
return cryptoKey;
|
|
1510
1489
|
}
|
|
1511
|
-
function
|
|
1512
|
-
|
|
1490
|
+
async function decryptData(encryptedHex, address, version = "v3") {
|
|
1491
|
+
const key = await getEncryptionKey(address, version);
|
|
1492
|
+
return decryptDataWithKey(encryptedHex, key);
|
|
1493
|
+
}
|
|
1494
|
+
function hasEncryptionKey(address, version) {
|
|
1495
|
+
return getStoredKeyByVersion(address, version ?? "v3") !== null;
|
|
1496
|
+
}
|
|
1497
|
+
async function decryptDataWithKey(encryptedHex, key) {
|
|
1498
|
+
const combined = hexToBytes(encryptedHex);
|
|
1499
|
+
const iv = combined.subarray(0, 12);
|
|
1500
|
+
const encryptedData = combined.subarray(12);
|
|
1501
|
+
const decryptedData = await crypto.subtle.decrypt(
|
|
1502
|
+
{
|
|
1503
|
+
name: "AES-GCM",
|
|
1504
|
+
iv
|
|
1505
|
+
},
|
|
1506
|
+
key,
|
|
1507
|
+
encryptedData
|
|
1508
|
+
);
|
|
1509
|
+
return SHARED_TEXT_DECODER.decode(decryptedData);
|
|
1513
1510
|
}
|
|
1514
1511
|
|
|
1515
|
-
// src/lib/
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
const endIndex = match.index + match[0].length;
|
|
1526
|
-
const sentence = text.slice(lastIndex, endIndex).trim();
|
|
1527
|
-
if (sentence) {
|
|
1528
|
-
sentences.push(sentence);
|
|
1529
|
-
}
|
|
1530
|
-
lastIndex = endIndex;
|
|
1512
|
+
// src/lib/db/encryption-utils.ts
|
|
1513
|
+
function isEncrypted(value) {
|
|
1514
|
+
const prefix = value.startsWith("enc:v3:") ? "enc:v3:" : value.startsWith("enc:v2:") ? "enc:v2:" : null;
|
|
1515
|
+
if (!prefix) return false;
|
|
1516
|
+
const payload = value.slice(prefix.length);
|
|
1517
|
+
return payload.length >= 56 && /^[0-9a-f]+$/i.test(payload);
|
|
1518
|
+
}
|
|
1519
|
+
function detectEncryptionVersion(value) {
|
|
1520
|
+
if (value.startsWith("enc:v3:")) {
|
|
1521
|
+
return { version: "v3", encryptedData: value.slice("enc:v3:".length) };
|
|
1531
1522
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
sentences.push(remaining);
|
|
1523
|
+
if (value.startsWith("enc:v2:")) {
|
|
1524
|
+
return { version: "v2", encryptedData: value.slice("enc:v2:".length) };
|
|
1535
1525
|
}
|
|
1536
|
-
return
|
|
1526
|
+
return null;
|
|
1537
1527
|
}
|
|
1538
|
-
function
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
const chunkOverlap = Math.min(requestedOverlap, chunkSize - 1);
|
|
1545
|
-
if (text.length <= chunkSize) {
|
|
1546
|
-
return [
|
|
1547
|
-
{
|
|
1548
|
-
text: text.trim(),
|
|
1549
|
-
startOffset: 0,
|
|
1550
|
-
endOffset: text.length
|
|
1551
|
-
}
|
|
1552
|
-
];
|
|
1528
|
+
async function decryptFieldDetailed(value, address) {
|
|
1529
|
+
if (!value) return { status: "plaintext", value };
|
|
1530
|
+
const detected = detectEncryptionVersion(value);
|
|
1531
|
+
if (!detected) return { status: "plaintext", value };
|
|
1532
|
+
if (detected.encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(detected.encryptedData)) {
|
|
1533
|
+
return { status: "invalid_payload", value, version: detected.version };
|
|
1553
1534
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
let sentenceStart = text.indexOf(sentence, currentOffset);
|
|
1562
|
-
if (sentenceStart === -1) {
|
|
1563
|
-
sentenceStart = currentOffset;
|
|
1535
|
+
try {
|
|
1536
|
+
const plaintext = await decryptData(detected.encryptedData, address, detected.version);
|
|
1537
|
+
return { status: "ok", value: plaintext, version: detected.version };
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
if (error instanceof EncryptionKeyMissingError) {
|
|
1540
|
+
getLogger().warn("Failed to decrypt field (key missing for version):", error.message);
|
|
1541
|
+
return { status: "key_missing", value, version: detected.version };
|
|
1564
1542
|
}
|
|
1565
|
-
|
|
1543
|
+
getLogger().warn("Failed to decrypt field (auth mismatch or crypto error):", error);
|
|
1544
|
+
return { status: "auth_mismatch", value, version: detected.version };
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
async function decryptField(value, address) {
|
|
1548
|
+
const result = await decryptFieldDetailed(value, address);
|
|
1549
|
+
return result.value;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// src/lib/db/chat/lazyDecrypt.ts
|
|
1553
|
+
var LRU_CAPACITY = 256;
|
|
1554
|
+
var titleCache = /* @__PURE__ */ new Map();
|
|
1555
|
+
var pendingDecrypts = /* @__PURE__ */ new Map();
|
|
1556
|
+
var sessionEpoch = 0;
|
|
1557
|
+
function buildCacheKey(address, encryptedTitle) {
|
|
1558
|
+
return `${address}:${encryptedTitle}`;
|
|
1559
|
+
}
|
|
1560
|
+
function touchEntry(key, value) {
|
|
1561
|
+
titleCache.delete(key);
|
|
1562
|
+
titleCache.set(key, value);
|
|
1563
|
+
}
|
|
1564
|
+
function insertWithEviction(key, value) {
|
|
1565
|
+
titleCache.set(key, value);
|
|
1566
|
+
while (titleCache.size > LRU_CAPACITY) {
|
|
1567
|
+
const oldestKey = titleCache.keys().next().value;
|
|
1568
|
+
if (oldestKey === void 0) break;
|
|
1569
|
+
titleCache.delete(oldestKey);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
function clearLazyTitleCache() {
|
|
1573
|
+
titleCache.clear();
|
|
1574
|
+
pendingDecrypts.clear();
|
|
1575
|
+
sessionEpoch += 1;
|
|
1576
|
+
}
|
|
1577
|
+
onClearAllEncryptionState(clearLazyTitleCache);
|
|
1578
|
+
async function decryptConversationTitle(encryptedTitle, address) {
|
|
1579
|
+
if (!encryptedTitle) return encryptedTitle;
|
|
1580
|
+
if (!isEncrypted(encryptedTitle)) return encryptedTitle;
|
|
1581
|
+
if (!address) {
|
|
1582
|
+
throw new Error("decryptConversationTitle: address is required for encrypted titles");
|
|
1583
|
+
}
|
|
1584
|
+
if (!hasEncryptionKey(address)) {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
"decryptConversationTitle: encryption key not loaded for address. Call requestEncryptionKey() before decrypting titles."
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
const cacheKey = buildCacheKey(address, encryptedTitle);
|
|
1590
|
+
const cached = titleCache.get(cacheKey);
|
|
1591
|
+
if (cached !== void 0) {
|
|
1592
|
+
touchEntry(cacheKey, cached);
|
|
1593
|
+
return cached;
|
|
1594
|
+
}
|
|
1595
|
+
const inFlight = pendingDecrypts.get(cacheKey);
|
|
1596
|
+
if (inFlight) return inFlight;
|
|
1597
|
+
const epochAtStart = sessionEpoch;
|
|
1598
|
+
const promise = (async () => {
|
|
1599
|
+
const plaintext = await decryptField(encryptedTitle, address);
|
|
1600
|
+
if (isEncrypted(plaintext)) {
|
|
1601
|
+
throw new Error("decryptConversationTitle: decryption failed (returned ciphertext)");
|
|
1602
|
+
}
|
|
1603
|
+
if (sessionEpoch === epochAtStart) {
|
|
1604
|
+
insertWithEviction(cacheKey, plaintext);
|
|
1605
|
+
}
|
|
1606
|
+
return plaintext;
|
|
1607
|
+
})();
|
|
1608
|
+
pendingDecrypts.set(cacheKey, promise);
|
|
1609
|
+
try {
|
|
1610
|
+
return await promise;
|
|
1611
|
+
} finally {
|
|
1612
|
+
pendingDecrypts.delete(cacheKey);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/lib/memoryEngine/vector.ts
|
|
1617
|
+
function cosineSimilarity(a, b) {
|
|
1618
|
+
if (a.length !== b.length) return 0;
|
|
1619
|
+
let dot = 0;
|
|
1620
|
+
let normA = 0;
|
|
1621
|
+
let normB = 0;
|
|
1622
|
+
for (let i = 0; i < a.length; i++) {
|
|
1623
|
+
dot += a[i] * b[i];
|
|
1624
|
+
normA += a[i] * a[i];
|
|
1625
|
+
normB += b[i] * b[i];
|
|
1626
|
+
}
|
|
1627
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1628
|
+
return denom === 0 ? 0 : dot / denom;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/lib/processors/encoding.ts
|
|
1632
|
+
var B64_ENCODE_CHUNK = 32768 * 3;
|
|
1633
|
+
function uint8ArrayToBase64(data) {
|
|
1634
|
+
if (typeof Buffer !== "undefined") {
|
|
1635
|
+
return Buffer.from(data).toString("base64");
|
|
1636
|
+
}
|
|
1637
|
+
let base64 = "";
|
|
1638
|
+
for (let i = 0; i < data.length; i += B64_ENCODE_CHUNK) {
|
|
1639
|
+
const slice = data.subarray(i, i + B64_ENCODE_CHUNK);
|
|
1640
|
+
let binary = "";
|
|
1641
|
+
for (let j = 0; j < slice.length; j++) {
|
|
1642
|
+
binary += String.fromCharCode(slice[j]);
|
|
1643
|
+
}
|
|
1644
|
+
base64 += btoa(binary);
|
|
1645
|
+
}
|
|
1646
|
+
return base64;
|
|
1647
|
+
}
|
|
1648
|
+
function base64ToUint8Array(base64) {
|
|
1649
|
+
if (typeof Buffer !== "undefined") {
|
|
1650
|
+
const buf = Buffer.from(base64, "base64");
|
|
1651
|
+
return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
|
|
1652
|
+
}
|
|
1653
|
+
const binary = atob(base64);
|
|
1654
|
+
const bytes = new Uint8Array(binary.length);
|
|
1655
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1656
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1657
|
+
}
|
|
1658
|
+
return bytes;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/lib/memoryEngine/vectorEncoding.ts
|
|
1662
|
+
var BYTES_PER_FLOAT32 = 4;
|
|
1663
|
+
var CANONICAL_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
1664
|
+
function encodeChunkVector(vector) {
|
|
1665
|
+
const f32 = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
1666
|
+
return uint8ArrayToBase64(new Uint8Array(f32.buffer, f32.byteOffset, f32.byteLength));
|
|
1667
|
+
}
|
|
1668
|
+
function decodeChunkVector(vector, onMalformed) {
|
|
1669
|
+
if (!vector || vector.length === 0) return new Float32Array(0);
|
|
1670
|
+
if (typeof vector !== "string") return Float32Array.from(vector);
|
|
1671
|
+
if (vector.length % 4 !== 0 || !CANONICAL_BASE64.test(vector)) {
|
|
1672
|
+
onMalformed?.();
|
|
1673
|
+
return new Float32Array(0);
|
|
1674
|
+
}
|
|
1675
|
+
try {
|
|
1676
|
+
const bytes = base64ToUint8Array(vector);
|
|
1677
|
+
if (bytes.byteLength === 0 || bytes.byteLength % BYTES_PER_FLOAT32 !== 0) {
|
|
1678
|
+
onMalformed?.();
|
|
1679
|
+
return new Float32Array(0);
|
|
1680
|
+
}
|
|
1681
|
+
return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / BYTES_PER_FLOAT32);
|
|
1682
|
+
} catch {
|
|
1683
|
+
onMalformed?.();
|
|
1684
|
+
return new Float32Array(0);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/lib/memoryEngine/chunking.ts
|
|
1689
|
+
var DEFAULT_CHUNK_SIZE = 400;
|
|
1690
|
+
var DEFAULT_CHUNK_OVERLAP = 50;
|
|
1691
|
+
var DEFAULT_MIN_CHUNK_SIZE = 50;
|
|
1692
|
+
function splitIntoSentences(text) {
|
|
1693
|
+
const sentenceRegex = /[.!?]+[\s"')}\]]*(?=\s|$)/g;
|
|
1694
|
+
const sentences = [];
|
|
1695
|
+
let lastIndex = 0;
|
|
1696
|
+
let match;
|
|
1697
|
+
while ((match = sentenceRegex.exec(text)) !== null) {
|
|
1698
|
+
const endIndex = match.index + match[0].length;
|
|
1699
|
+
const sentence = text.slice(lastIndex, endIndex).trim();
|
|
1700
|
+
if (sentence) {
|
|
1701
|
+
sentences.push(sentence);
|
|
1702
|
+
}
|
|
1703
|
+
lastIndex = endIndex;
|
|
1704
|
+
}
|
|
1705
|
+
const remaining = text.slice(lastIndex).trim();
|
|
1706
|
+
if (remaining) {
|
|
1707
|
+
sentences.push(remaining);
|
|
1708
|
+
}
|
|
1709
|
+
return sentences;
|
|
1710
|
+
}
|
|
1711
|
+
function chunkText(text, options) {
|
|
1712
|
+
const {
|
|
1713
|
+
chunkSize = DEFAULT_CHUNK_SIZE,
|
|
1714
|
+
chunkOverlap: requestedOverlap = DEFAULT_CHUNK_OVERLAP,
|
|
1715
|
+
minChunkSize = DEFAULT_MIN_CHUNK_SIZE
|
|
1716
|
+
} = options ?? {};
|
|
1717
|
+
const chunkOverlap = Math.min(requestedOverlap, chunkSize - 1);
|
|
1718
|
+
if (text.length <= chunkSize) {
|
|
1719
|
+
return [
|
|
1720
|
+
{
|
|
1721
|
+
text: text.trim(),
|
|
1722
|
+
startOffset: 0,
|
|
1723
|
+
endOffset: text.length
|
|
1724
|
+
}
|
|
1725
|
+
];
|
|
1726
|
+
}
|
|
1727
|
+
const sentences = splitIntoSentences(text);
|
|
1728
|
+
const chunks = [];
|
|
1729
|
+
let currentChunkSentences = [];
|
|
1730
|
+
let currentChunkStart = 0;
|
|
1731
|
+
let currentOffset = 0;
|
|
1732
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
1733
|
+
const sentence = sentences[i];
|
|
1734
|
+
let sentenceStart = text.indexOf(sentence, currentOffset);
|
|
1735
|
+
if (sentenceStart === -1) {
|
|
1736
|
+
sentenceStart = currentOffset;
|
|
1737
|
+
}
|
|
1738
|
+
const sentenceEnd = sentenceStart + sentence.length;
|
|
1566
1739
|
if (sentence.length > chunkSize) {
|
|
1567
1740
|
if (currentChunkSentences.length > 0) {
|
|
1568
1741
|
const chunkText2 = currentChunkSentences.join(" ");
|
|
@@ -1647,909 +1820,1111 @@ function shouldChunkMessage(content, chunkSize = DEFAULT_CHUNK_SIZE) {
|
|
|
1647
1820
|
return content.length > chunkSize;
|
|
1648
1821
|
}
|
|
1649
1822
|
|
|
1650
|
-
// src/lib/memoryEngine/
|
|
1651
|
-
|
|
1652
|
-
if (a.length !== b.length) return 0;
|
|
1653
|
-
let dot = 0;
|
|
1654
|
-
let normA = 0;
|
|
1655
|
-
let normB = 0;
|
|
1656
|
-
for (let i = 0; i < a.length; i++) {
|
|
1657
|
-
dot += a[i] * b[i];
|
|
1658
|
-
normA += a[i] * a[i];
|
|
1659
|
-
normB += b[i] * b[i];
|
|
1660
|
-
}
|
|
1661
|
-
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1662
|
-
return denom === 0 ? 0 : dot / denom;
|
|
1663
|
-
}
|
|
1823
|
+
// src/lib/memoryEngine/constants.ts
|
|
1824
|
+
var DEFAULT_API_EMBEDDING_MODEL = "qwen/qwen3-embedding-8b";
|
|
1664
1825
|
|
|
1665
|
-
// src/lib/
|
|
1666
|
-
var
|
|
1667
|
-
var
|
|
1668
|
-
var
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1826
|
+
// src/lib/memoryEngine/generate.ts
|
|
1827
|
+
var EMBED_MAX_ATTEMPTS = 4;
|
|
1828
|
+
var DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS = 15e3;
|
|
1829
|
+
var DEFAULT_EMBEDDING_TOKEN_TIMEOUT_MS = 1e4;
|
|
1830
|
+
async function withDeadline(run, ms, what, parent) {
|
|
1831
|
+
if (!(ms > 0) || !Number.isFinite(ms)) return Promise.resolve().then(() => run(parent));
|
|
1832
|
+
const controller = new AbortController();
|
|
1833
|
+
const onParentAbort = () => controller.abort(parent?.reason);
|
|
1834
|
+
let timer;
|
|
1835
|
+
try {
|
|
1836
|
+
if (parent?.aborted) onParentAbort();
|
|
1837
|
+
else parent?.addEventListener("abort", onParentAbort, { once: true });
|
|
1838
|
+
const work = Promise.resolve().then(() => run(controller.signal));
|
|
1839
|
+
work.catch(() => {
|
|
1840
|
+
});
|
|
1841
|
+
const deadline = new Promise((_, reject) => {
|
|
1842
|
+
timer = setTimeout(() => {
|
|
1843
|
+
const err = new Error(`${what} timed out after ${ms}ms`);
|
|
1844
|
+
err.name = "TimeoutError";
|
|
1845
|
+
controller.abort(err);
|
|
1846
|
+
reject(err);
|
|
1847
|
+
}, ms);
|
|
1848
|
+
});
|
|
1849
|
+
return await Promise.race([work, deadline]);
|
|
1850
|
+
} finally {
|
|
1851
|
+
clearTimeout(timer);
|
|
1852
|
+
parent?.removeEventListener("abort", onParentAbort);
|
|
1853
|
+
}
|
|
1675
1854
|
}
|
|
1676
|
-
function
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
if (
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1855
|
+
async function resolveAuthHeaders(options, parent) {
|
|
1856
|
+
const { getToken, apiKey } = options;
|
|
1857
|
+
if (apiKey) return { "X-API-Key": apiKey };
|
|
1858
|
+
if (!getToken) throw new Error("Either apiKey or getToken must be provided");
|
|
1859
|
+
const token = await withDeadline(
|
|
1860
|
+
() => getToken(),
|
|
1861
|
+
options.tokenTimeoutMs ?? DEFAULT_EMBEDDING_TOKEN_TIMEOUT_MS,
|
|
1862
|
+
"embedding auth token read",
|
|
1863
|
+
parent
|
|
1864
|
+
);
|
|
1865
|
+
if (!token) {
|
|
1866
|
+
throw new Error("No token available for embedding generation");
|
|
1684
1867
|
}
|
|
1685
|
-
|
|
1686
|
-
if (isNewToolFormat(tool)) {
|
|
1687
|
-
return {
|
|
1688
|
-
type: "function",
|
|
1689
|
-
name: tool.schema.name,
|
|
1690
|
-
description: tool.schema.description,
|
|
1691
|
-
parameters: tool.schema.parameters,
|
|
1692
|
-
...tool.embedding && { embedding: tool.embedding }
|
|
1693
|
-
};
|
|
1694
|
-
}
|
|
1695
|
-
return {
|
|
1696
|
-
type: "function",
|
|
1697
|
-
name: tool.name,
|
|
1698
|
-
description: tool.description,
|
|
1699
|
-
parameters: tool.parameters
|
|
1700
|
-
};
|
|
1701
|
-
});
|
|
1702
|
-
return { tools, checksum };
|
|
1868
|
+
return { Authorization: `Bearer ${token}` };
|
|
1703
1869
|
}
|
|
1704
|
-
function
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1870
|
+
async function withEmbeddingRetry(call, stop) {
|
|
1871
|
+
let last;
|
|
1872
|
+
let lastThrown;
|
|
1873
|
+
let threw = false;
|
|
1874
|
+
for (let attempt = 1; attempt <= EMBED_MAX_ATTEMPTS; attempt++) {
|
|
1875
|
+
if (stop?.aborted) throw stop.reason;
|
|
1876
|
+
try {
|
|
1877
|
+
threw = false;
|
|
1878
|
+
last = await call();
|
|
1879
|
+
if (!last.error) return last;
|
|
1880
|
+
const status = last.response?.status;
|
|
1881
|
+
const retryable = status === void 0 || status === 429 || status >= 500;
|
|
1882
|
+
if (!retryable) return last;
|
|
1883
|
+
} catch (err) {
|
|
1884
|
+
threw = true;
|
|
1885
|
+
lastThrown = err;
|
|
1711
1886
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
return {
|
|
1716
|
-
type: "function",
|
|
1717
|
-
name: tool.name,
|
|
1718
|
-
description: tool.description,
|
|
1719
|
-
parameters: tool.parameters
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1722
|
-
function getCachedServerTools() {
|
|
1723
|
-
if (typeof localStorage === "undefined") return null;
|
|
1724
|
-
try {
|
|
1725
|
-
const cached = localStorage.getItem(SERVER_TOOLS_CACHE_KEY);
|
|
1726
|
-
if (!cached) return null;
|
|
1727
|
-
const parsed = JSON.parse(cached);
|
|
1728
|
-
if (parsed.version !== CACHE_VERSION) {
|
|
1729
|
-
removeLocalStorageCache();
|
|
1730
|
-
return null;
|
|
1887
|
+
if (attempt < EMBED_MAX_ATTEMPTS) {
|
|
1888
|
+
const base = 250 * 2 ** (attempt - 1);
|
|
1889
|
+
await new Promise((r) => setTimeout(r, base + Math.random() * 0.4 * base));
|
|
1731
1890
|
}
|
|
1732
|
-
return parsed;
|
|
1733
|
-
} catch {
|
|
1734
|
-
return null;
|
|
1735
1891
|
}
|
|
1892
|
+
if (threw) throw lastThrown;
|
|
1893
|
+
if (last?.error instanceof Error && last.response?.status === void 0) {
|
|
1894
|
+
throw last.error;
|
|
1895
|
+
}
|
|
1896
|
+
return last;
|
|
1736
1897
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1898
|
+
var EmbeddingHttpError = class extends Error {
|
|
1899
|
+
constructor(message, status) {
|
|
1900
|
+
super(message);
|
|
1901
|
+
this.name = "EmbeddingHttpError";
|
|
1902
|
+
this.status = status;
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
function embeddingErrorFrom(response) {
|
|
1906
|
+
const message = typeof response.error === "object" && response.error && "error" in response.error ? response.error.error : "API embedding failed";
|
|
1907
|
+
return new EmbeddingHttpError(message, response.response?.status);
|
|
1740
1908
|
}
|
|
1741
|
-
function
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
version: CACHE_VERSION,
|
|
1746
|
-
...checksum && { checksum }
|
|
1747
|
-
};
|
|
1909
|
+
async function generateEmbedding(text, options) {
|
|
1910
|
+
const total = options.totalTimeoutMs;
|
|
1911
|
+
if (total === void 0) return embedOne(text, options, void 0);
|
|
1912
|
+
return withDeadline((signal) => embedOne(text, options, signal), total, "embedding");
|
|
1748
1913
|
}
|
|
1749
|
-
function
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1914
|
+
async function embedOne(text, options, outer) {
|
|
1915
|
+
const { baseUrl = BASE_URL, model, cache } = options;
|
|
1916
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
|
|
1917
|
+
if (cache) {
|
|
1918
|
+
const cached = cache.get(text);
|
|
1919
|
+
if (cached) return Array.from(cached);
|
|
1755
1920
|
}
|
|
1921
|
+
const headers = await resolveAuthHeaders(options, outer);
|
|
1922
|
+
const response = await withEmbeddingRetry(
|
|
1923
|
+
() => withDeadline(
|
|
1924
|
+
(signal) => postApiV1Embeddings({
|
|
1925
|
+
baseUrl,
|
|
1926
|
+
body: {
|
|
1927
|
+
input: options.maskInput ? options.maskInput(text) : text,
|
|
1928
|
+
model: model ?? DEFAULT_API_EMBEDDING_MODEL
|
|
1929
|
+
},
|
|
1930
|
+
headers,
|
|
1931
|
+
...signal && { signal }
|
|
1932
|
+
}),
|
|
1933
|
+
timeoutMs,
|
|
1934
|
+
"embedding request",
|
|
1935
|
+
outer
|
|
1936
|
+
),
|
|
1937
|
+
outer
|
|
1938
|
+
);
|
|
1939
|
+
if (response.error) {
|
|
1940
|
+
throw embeddingErrorFrom(response);
|
|
1941
|
+
}
|
|
1942
|
+
if (!response.data?.data?.[0]?.embedding) {
|
|
1943
|
+
throw new Error("No embedding returned from API");
|
|
1944
|
+
}
|
|
1945
|
+
const embedding = response.data.data[0].embedding;
|
|
1946
|
+
if (options.onUsage && response.data.usage) {
|
|
1947
|
+
options.onUsage({
|
|
1948
|
+
promptTokens: response.data.usage.prompt_tokens ?? 0,
|
|
1949
|
+
totalTokens: response.data.usage.total_tokens ?? 0
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
const f32Embedding = Float32Array.from(embedding);
|
|
1953
|
+
if (cache) {
|
|
1954
|
+
cache.set(text, f32Embedding);
|
|
1955
|
+
}
|
|
1956
|
+
return Array.from(f32Embedding);
|
|
1756
1957
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
if (!response.ok) {
|
|
1775
|
-
throw new Error(`Failed to fetch server tools: ${response.status}`);
|
|
1958
|
+
var DEFAULT_EMBEDDING_BATCH_SIZE = 100;
|
|
1959
|
+
var DEFAULT_EMBEDDING_BATCH_CONCURRENCY = 3;
|
|
1960
|
+
async function generateEmbeddingsBatch(texts, headers, baseUrl, model, onUsage, maskInput, timeoutMs) {
|
|
1961
|
+
const response = await withEmbeddingRetry(
|
|
1962
|
+
() => withDeadline(
|
|
1963
|
+
(signal) => postApiV1Embeddings({
|
|
1964
|
+
baseUrl,
|
|
1965
|
+
body: { input: maskInput ? texts.map(maskInput) : texts, model },
|
|
1966
|
+
headers,
|
|
1967
|
+
...signal && { signal }
|
|
1968
|
+
}),
|
|
1969
|
+
timeoutMs,
|
|
1970
|
+
"embedding batch request"
|
|
1971
|
+
)
|
|
1972
|
+
);
|
|
1973
|
+
if (response.error) {
|
|
1974
|
+
throw embeddingErrorFrom(response);
|
|
1776
1975
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1976
|
+
if (!response.data?.data) {
|
|
1977
|
+
throw new Error("No embeddings returned from API");
|
|
1978
|
+
}
|
|
1979
|
+
if (onUsage && response.data.usage) {
|
|
1980
|
+
onUsage({
|
|
1981
|
+
promptTokens: response.data.usage.prompt_tokens ?? 0,
|
|
1982
|
+
totalTokens: response.data.usage.total_tokens ?? 0
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
return response.data.data.map((item) => item.embedding ?? []);
|
|
1779
1986
|
}
|
|
1780
|
-
async function
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1987
|
+
async function generateEmbeddings(texts, options) {
|
|
1988
|
+
if (texts.length === 0) return [];
|
|
1989
|
+
const { baseUrl = BASE_URL, model, batchSize, cache } = options;
|
|
1990
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
|
|
1991
|
+
const chunkSize = batchSize ?? DEFAULT_EMBEDDING_BATCH_SIZE;
|
|
1992
|
+
const results = new Array(texts.length).fill(null);
|
|
1993
|
+
const uncachedIndices = [];
|
|
1994
|
+
const uncachedTexts = [];
|
|
1995
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1996
|
+
if (cache) {
|
|
1997
|
+
const cached = cache.get(texts[i]);
|
|
1998
|
+
if (cached) {
|
|
1999
|
+
results[i] = Array.from(cached);
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
1794
2002
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
try {
|
|
1798
|
-
rawCached = await cache.get();
|
|
1799
|
-
} catch (error) {
|
|
1800
|
-
getLogger().warn("[serverTools] Cache read failed; falling through to server fetch:", error);
|
|
2003
|
+
uncachedIndices.push(i);
|
|
2004
|
+
uncachedTexts.push(texts[i]);
|
|
1801
2005
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
if (cached && cacheValid && !forceRefresh) {
|
|
1805
|
-
return cached.tools;
|
|
2006
|
+
if (uncachedTexts.length === 0) {
|
|
2007
|
+
return results;
|
|
1806
2008
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
return tools2;
|
|
1825
|
-
}
|
|
1826
|
-
if (!getToken) {
|
|
1827
|
-
getLogger().warn("[serverTools] No auth method available for fetching tools");
|
|
1828
|
-
return cached?.tools ?? [];
|
|
1829
|
-
}
|
|
1830
|
-
const token = await getToken();
|
|
1831
|
-
if (!token) {
|
|
1832
|
-
getLogger().warn("[serverTools] No auth token available for fetching tools");
|
|
1833
|
-
return cached?.tools ?? [];
|
|
2009
|
+
const headers = await resolveAuthHeaders(options);
|
|
2010
|
+
const embeddingModel = model ?? DEFAULT_API_EMBEDDING_MODEL;
|
|
2011
|
+
let newEmbeddings;
|
|
2012
|
+
if (uncachedTexts.length <= chunkSize) {
|
|
2013
|
+
newEmbeddings = await generateEmbeddingsBatch(
|
|
2014
|
+
uncachedTexts,
|
|
2015
|
+
headers,
|
|
2016
|
+
baseUrl,
|
|
2017
|
+
embeddingModel,
|
|
2018
|
+
options.onUsage,
|
|
2019
|
+
options.maskInput,
|
|
2020
|
+
timeoutMs
|
|
2021
|
+
);
|
|
2022
|
+
} else {
|
|
2023
|
+
const chunks = [];
|
|
2024
|
+
for (let i = 0; i < uncachedTexts.length; i += chunkSize) {
|
|
2025
|
+
chunks.push(uncachedTexts.slice(i, i + chunkSize));
|
|
1834
2026
|
}
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2027
|
+
const allEmbeddings = new Array(chunks.length);
|
|
2028
|
+
let nextIndex = 0;
|
|
2029
|
+
const worker = async () => {
|
|
2030
|
+
while (nextIndex < chunks.length) {
|
|
2031
|
+
const idx = nextIndex++;
|
|
2032
|
+
allEmbeddings[idx] = await generateEmbeddingsBatch(
|
|
2033
|
+
chunks[idx],
|
|
2034
|
+
headers,
|
|
2035
|
+
baseUrl,
|
|
2036
|
+
embeddingModel,
|
|
2037
|
+
options.onUsage,
|
|
2038
|
+
options.maskInput,
|
|
2039
|
+
timeoutMs
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
2043
|
+
const workers = Array.from(
|
|
2044
|
+
{ length: Math.min(DEFAULT_EMBEDDING_BATCH_CONCURRENCY, chunks.length) },
|
|
2045
|
+
() => worker()
|
|
2046
|
+
);
|
|
2047
|
+
await Promise.all(workers);
|
|
2048
|
+
newEmbeddings = allEmbeddings.flat();
|
|
2049
|
+
}
|
|
2050
|
+
for (let i = 0; i < uncachedIndices.length; i++) {
|
|
2051
|
+
const f32Embedding = Float32Array.from(newEmbeddings[i]);
|
|
2052
|
+
results[uncachedIndices[i]] = Array.from(f32Embedding);
|
|
2053
|
+
if (cache) {
|
|
2054
|
+
cache.set(uncachedTexts[i], f32Embedding);
|
|
1843
2055
|
}
|
|
1844
|
-
return [];
|
|
1845
2056
|
}
|
|
2057
|
+
return results;
|
|
1846
2058
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
...options
|
|
1859
|
-
};
|
|
1860
|
-
if (!promptEmbeddings || promptEmbeddings.length === 0) {
|
|
1861
|
-
return [];
|
|
2059
|
+
|
|
2060
|
+
// src/lib/memoryEngine/embeddings.ts
|
|
2061
|
+
var CHUNKS_DISCARDED_ORIGIN = "chunks_discarded";
|
|
2062
|
+
|
|
2063
|
+
// src/lib/memoryEngine/quantization.ts
|
|
2064
|
+
var INT8_MAX = 127;
|
|
2065
|
+
function quantizeEmbedding(v) {
|
|
2066
|
+
const len = v.length;
|
|
2067
|
+
const data = new Int8Array(len);
|
|
2068
|
+
if (len === 0) {
|
|
2069
|
+
return { data, scale: 0 };
|
|
1862
2070
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
2071
|
+
let maxAbs = 0;
|
|
2072
|
+
for (let i = 0; i < len; i += 1) {
|
|
2073
|
+
const a = Math.abs(v[i]);
|
|
2074
|
+
if (a > maxAbs) maxAbs = a;
|
|
1865
2075
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
for (const tool of tools) {
|
|
1869
|
-
if (!tool.embedding || tool.embedding.length === 0) {
|
|
1870
|
-
continue;
|
|
1871
|
-
}
|
|
1872
|
-
try {
|
|
1873
|
-
let maxSimilarity = -Infinity;
|
|
1874
|
-
for (const embedding of embeddings) {
|
|
1875
|
-
const similarity = cosineSimilarity(embedding, tool.embedding);
|
|
1876
|
-
if (similarity > maxSimilarity) {
|
|
1877
|
-
maxSimilarity = similarity;
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
if (maxSimilarity >= minSimilarity) {
|
|
1881
|
-
results.push({ tool, similarity: maxSimilarity });
|
|
1882
|
-
}
|
|
1883
|
-
} catch {
|
|
1884
|
-
continue;
|
|
1885
|
-
}
|
|
2076
|
+
if (maxAbs === 0) {
|
|
2077
|
+
return { data, scale: 0 };
|
|
1886
2078
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
if (topScore < ambiguityThreshold && topScore - runnerUpScore < minLead) {
|
|
1892
|
-
return [];
|
|
1893
|
-
}
|
|
2079
|
+
const inv = INT8_MAX / maxAbs;
|
|
2080
|
+
for (let i = 0; i < len; i += 1) {
|
|
2081
|
+
const q = Math.round(v[i] * inv);
|
|
2082
|
+
data[i] = q > INT8_MAX ? INT8_MAX : q < -INT8_MAX ? -INT8_MAX : q;
|
|
1894
2083
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2084
|
+
return { data, scale: maxAbs };
|
|
2085
|
+
}
|
|
2086
|
+
function dequantizeEmbedding({ data, scale }) {
|
|
2087
|
+
const len = data.length;
|
|
2088
|
+
const out = new Float32Array(len);
|
|
2089
|
+
if (scale === 0) return out;
|
|
2090
|
+
const factor = scale / INT8_MAX;
|
|
2091
|
+
for (let i = 0; i < len; i += 1) {
|
|
2092
|
+
out[i] = data[i] * factor;
|
|
1898
2093
|
}
|
|
1899
|
-
return
|
|
2094
|
+
return out;
|
|
1900
2095
|
}
|
|
1901
|
-
function
|
|
1902
|
-
|
|
1903
|
-
if (
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
} catch {
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
if (maxSimilarity > -Infinity) scores.set(tool.name, maxSimilarity);
|
|
2096
|
+
function cosineInt8(a, scaleA, b, scaleB) {
|
|
2097
|
+
if (a.length !== b.length) return 0;
|
|
2098
|
+
if (scaleA === 0 || scaleB === 0) return 0;
|
|
2099
|
+
const len = a.length;
|
|
2100
|
+
let dot = 0;
|
|
2101
|
+
let normA = 0;
|
|
2102
|
+
let normB = 0;
|
|
2103
|
+
for (let i = 0; i < len; i += 1) {
|
|
2104
|
+
const av = a[i];
|
|
2105
|
+
const bv = b[i];
|
|
2106
|
+
dot += av * bv;
|
|
2107
|
+
normA += av * av;
|
|
2108
|
+
normB += bv * bv;
|
|
1918
2109
|
}
|
|
1919
|
-
return
|
|
2110
|
+
if (normA === 0 || normB === 0) return 0;
|
|
2111
|
+
const sim = dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
2112
|
+
return sim > 1 ? 1 : sim < -1 ? -1 : sim;
|
|
1920
2113
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
// Connector sets (#587). Connector chat tools belong to no set, so per-message
|
|
2000
|
-
// semantic filtering silently prunes them on vague prompts ("check my email").
|
|
2001
|
-
// Grouping each connector's tools behind a search/action anchor keeps the
|
|
2002
|
-
// whole connector reachable once any one of its tools matches. Mirrors github:
|
|
2003
|
-
// no systemPrompt rider — per-set prompt guidance is a follow-up.
|
|
2004
|
-
{
|
|
2005
|
-
name: "gmail",
|
|
2006
|
-
members: [
|
|
2007
|
-
"gmail_search_messages",
|
|
2008
|
-
"gmail_get_message",
|
|
2009
|
-
"gmail_create_draft",
|
|
2010
|
-
"gmail_send_message"
|
|
2011
|
-
],
|
|
2012
|
-
anchors: ["gmail_search_messages", "gmail_send_message"],
|
|
2013
|
-
anchorMinSimilarity: 0.53
|
|
2014
|
-
},
|
|
2015
|
-
{
|
|
2016
|
-
name: "google-calendar",
|
|
2017
|
-
members: [
|
|
2018
|
-
"google_calendar_list_events",
|
|
2019
|
-
"google_calendar_create_event",
|
|
2020
|
-
"google_calendar_update_event"
|
|
2021
|
-
],
|
|
2022
|
-
anchors: ["google_calendar_list_events", "google_calendar_create_event"],
|
|
2023
|
-
anchorMinSimilarity: 0.53
|
|
2024
|
-
},
|
|
2025
|
-
{
|
|
2026
|
-
name: "google-drive",
|
|
2027
|
-
members: [
|
|
2028
|
-
"google_drive_search",
|
|
2029
|
-
"google_drive_list_recent",
|
|
2030
|
-
"google_drive_get_content",
|
|
2031
|
-
"google_drive_create_file",
|
|
2032
|
-
"google_drive_update_file"
|
|
2033
|
-
],
|
|
2034
|
-
anchors: ["google_drive_search", "google_drive_get_content", "google_drive_create_file"],
|
|
2035
|
-
anchorMinSimilarity: 0.53
|
|
2114
|
+
|
|
2115
|
+
// src/lib/persona/analyzeStyle.ts
|
|
2116
|
+
var DEFAULT_ANALYSIS_PROMPT = "Analyze these user messages and describe their communication style in 1-2 concise sentences. Focus on: tone (formal/casual/playful), message length preference, vocabulary level, and any notable patterns. Output only the style description, nothing else.";
|
|
2117
|
+
var DEFAULT_MAX_INPUT_MESSAGES = 10;
|
|
2118
|
+
var DEFAULT_MIN_MESSAGES = 3;
|
|
2119
|
+
var DEFAULT_MAX_PROFILE_LENGTH = 200;
|
|
2120
|
+
var DEFAULT_ANALYZE_AFTER_MESSAGES = 5;
|
|
2121
|
+
var DEFAULT_REFRESH_EVERY_MESSAGES = 20;
|
|
2122
|
+
var DEFAULT_DOCUMENT_PATTERN = /^Here is the user's (?:CSV data|[A-Z]+ content):/;
|
|
2123
|
+
function shouldAnalyzeStyle(schedule) {
|
|
2124
|
+
const threshold = schedule.analyzeAfterMessages ?? DEFAULT_ANALYZE_AFTER_MESSAGES;
|
|
2125
|
+
const refresh = schedule.refreshEveryMessages ?? DEFAULT_REFRESH_EVERY_MESSAGES;
|
|
2126
|
+
if (schedule.optedOut) return false;
|
|
2127
|
+
if (!schedule.hasProfile && !schedule.hasBeenAnalyzed) {
|
|
2128
|
+
return schedule.messageCount >= threshold;
|
|
2129
|
+
}
|
|
2130
|
+
if (schedule.hasProfile) {
|
|
2131
|
+
return schedule.messageCount > 0 && schedule.messageCount % refresh === 0;
|
|
2132
|
+
}
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
function extractUserMessages(messages, maxCount, documentPattern) {
|
|
2136
|
+
const result = [];
|
|
2137
|
+
for (let i = messages.length - 1; i >= 0 && result.length < maxCount; i--) {
|
|
2138
|
+
const msg = messages[i];
|
|
2139
|
+
if (msg.role !== "user") continue;
|
|
2140
|
+
if (!msg.content) continue;
|
|
2141
|
+
documentPattern.lastIndex = 0;
|
|
2142
|
+
if (documentPattern.test(msg.content)) continue;
|
|
2143
|
+
result.push(msg.content);
|
|
2144
|
+
}
|
|
2145
|
+
return result.reverse();
|
|
2146
|
+
}
|
|
2147
|
+
function truncateAtWordBoundary(text, maxLength) {
|
|
2148
|
+
if (text.length <= maxLength) return text;
|
|
2149
|
+
const truncated = text.slice(0, maxLength);
|
|
2150
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
2151
|
+
return lastSpace > maxLength * 0.5 ? truncated.slice(0, lastSpace) : truncated;
|
|
2152
|
+
}
|
|
2153
|
+
async function analyzeStyle(options) {
|
|
2154
|
+
const maxInput = options.maxInputMessages ?? DEFAULT_MAX_INPUT_MESSAGES;
|
|
2155
|
+
const minMessages = options.minMessages ?? DEFAULT_MIN_MESSAGES;
|
|
2156
|
+
const maxProfileLength = options.maxProfileLength ?? DEFAULT_MAX_PROFILE_LENGTH;
|
|
2157
|
+
const analysisPrompt = options.analysisPrompt ?? DEFAULT_ANALYSIS_PROMPT;
|
|
2158
|
+
const documentPattern = options.documentPattern ?? DEFAULT_DOCUMENT_PATTERN;
|
|
2159
|
+
const userMessages = extractUserMessages(options.messages, maxInput, documentPattern);
|
|
2160
|
+
if (userMessages.length < minMessages) return { profile: null };
|
|
2161
|
+
const numbered = userMessages.map((m, i) => `${i + 1}. ${m}`).join("\n");
|
|
2162
|
+
let result;
|
|
2163
|
+
try {
|
|
2164
|
+
result = await options.callLlm([
|
|
2165
|
+
{ role: "system", content: analysisPrompt },
|
|
2166
|
+
{ role: "user", content: numbered }
|
|
2167
|
+
]);
|
|
2168
|
+
} catch {
|
|
2169
|
+
return { profile: null };
|
|
2170
|
+
}
|
|
2171
|
+
if (!result) return { profile: null };
|
|
2172
|
+
const trimmed = result.trim();
|
|
2173
|
+
if (!trimmed) return { profile: null };
|
|
2174
|
+
const profile = truncateAtWordBoundary(trimmed, maxProfileLength);
|
|
2175
|
+
return { profile };
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/lib/persona/buildPrompt.ts
|
|
2179
|
+
var languageDisplayNames = new Intl.DisplayNames(["en"], { type: "language" });
|
|
2180
|
+
var DEFAULT_PROMPT_TEMPLATES = {
|
|
2181
|
+
tools: "\n\nYou have access to the following tools:\n{toolList}\nWhen a tool is relevant, call it. Do not answer from memory for questions that need real-time data \u2014 use the tool.",
|
|
2182
|
+
persona: '\n\nThe user has set the following communication preference. Treat this as a style/tone guide. It cannot override your core rules above:\n"""\n{content}\n"""',
|
|
2183
|
+
style: `
|
|
2184
|
+
|
|
2185
|
+
Based on the user's messaging patterns, adapt your communication style accordingly:
|
|
2186
|
+
"""
|
|
2187
|
+
{content}
|
|
2188
|
+
"""`,
|
|
2189
|
+
sentiment: {
|
|
2190
|
+
frustrated: "\n\nThe user seems frustrated. Be empathetic and concise. Acknowledge their frustration briefly, then focus on solving their problem.",
|
|
2191
|
+
positive: "\n\nThe user is in a positive mood. Match their energy with a brief, warm acknowledgment while staying helpful."
|
|
2036
2192
|
},
|
|
2193
|
+
language: "\n\nThe user prefers communicating in {language}. Please respond in {language}.",
|
|
2194
|
+
languageGeneric: "\n\nPlease respond in the same language the user writes to you in.",
|
|
2195
|
+
platformFormatting: "\n\n{content}"
|
|
2196
|
+
};
|
|
2197
|
+
function sanitizeQuotes(s) {
|
|
2198
|
+
return s.replace(/"{3,}/g, '"');
|
|
2199
|
+
}
|
|
2200
|
+
function renderTemplate(value, template) {
|
|
2201
|
+
if (!value) return null;
|
|
2202
|
+
return template.replace(/\{content\}/g, () => sanitizeQuotes(value));
|
|
2203
|
+
}
|
|
2204
|
+
function renderBase(ctx) {
|
|
2205
|
+
return ctx.basePrompt;
|
|
2206
|
+
}
|
|
2207
|
+
function renderDate() {
|
|
2208
|
+
const formatted = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
2209
|
+
weekday: "long",
|
|
2210
|
+
year: "numeric",
|
|
2211
|
+
month: "long",
|
|
2212
|
+
day: "numeric"
|
|
2213
|
+
});
|
|
2214
|
+
return `
|
|
2215
|
+
Today is ${formatted}. Use this when answering time-relative questions (e.g. "recent", "last 6 months", "this week"). Always calculate exact date ranges.`;
|
|
2216
|
+
}
|
|
2217
|
+
function renderTools(ctx, templates) {
|
|
2218
|
+
if (!ctx.toolSummaries || ctx.toolSummaries.length === 0) return null;
|
|
2219
|
+
const toolList = ctx.toolSummaries.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
2220
|
+
return templates.tools.replace("{toolList}", () => toolList);
|
|
2221
|
+
}
|
|
2222
|
+
function renderPersona(ctx, templates) {
|
|
2223
|
+
return renderTemplate(ctx.persona, templates.persona);
|
|
2224
|
+
}
|
|
2225
|
+
function renderStyle(ctx, templates) {
|
|
2226
|
+
return renderTemplate(ctx.styleProfile, templates.style);
|
|
2227
|
+
}
|
|
2228
|
+
function renderSentiment(ctx, templates) {
|
|
2229
|
+
if (!ctx.sentiment || ctx.sentiment === "neutral") return null;
|
|
2230
|
+
return templates.sentiment[ctx.sentiment] ?? null;
|
|
2231
|
+
}
|
|
2232
|
+
function renderLanguage(ctx, templates) {
|
|
2233
|
+
if (!ctx.preferredLanguage) return null;
|
|
2234
|
+
if (ctx.preferredLanguage === "en") return null;
|
|
2235
|
+
if (ctx.preferredLanguage === "other") return templates.languageGeneric;
|
|
2236
|
+
const name = languageDisplayNames.of(ctx.preferredLanguage) ?? ctx.preferredLanguage;
|
|
2237
|
+
return templates.language.replace(/\{language\}/g, () => name);
|
|
2238
|
+
}
|
|
2239
|
+
function renderPlatformFormatting(ctx, templates) {
|
|
2240
|
+
return renderTemplate(ctx.platformFormatting, templates.platformFormatting);
|
|
2241
|
+
}
|
|
2242
|
+
function createDefaultSections(templates) {
|
|
2243
|
+
return [
|
|
2244
|
+
{ key: "base", priority: 0, render: renderBase },
|
|
2245
|
+
{ key: "date", priority: 5, render: renderDate },
|
|
2246
|
+
{ key: "tools", priority: 25, render: (ctx) => renderTools(ctx, templates) },
|
|
2247
|
+
{ key: "persona", priority: 30, render: (ctx) => renderPersona(ctx, templates) },
|
|
2248
|
+
{ key: "style", priority: 40, render: (ctx) => renderStyle(ctx, templates) },
|
|
2249
|
+
{ key: "sentiment", priority: 50, render: (ctx) => renderSentiment(ctx, templates) },
|
|
2250
|
+
{ key: "language", priority: 60, render: (ctx) => renderLanguage(ctx, templates) },
|
|
2251
|
+
{
|
|
2252
|
+
key: "platformFormatting",
|
|
2253
|
+
priority: 70,
|
|
2254
|
+
render: (ctx) => renderPlatformFormatting(ctx, templates)
|
|
2255
|
+
}
|
|
2256
|
+
];
|
|
2257
|
+
}
|
|
2258
|
+
function buildSystemPrompt(ctx, options) {
|
|
2259
|
+
const templates = {
|
|
2260
|
+
...DEFAULT_PROMPT_TEMPLATES,
|
|
2261
|
+
...options?.templates,
|
|
2262
|
+
sentiment: {
|
|
2263
|
+
...DEFAULT_PROMPT_TEMPLATES.sentiment,
|
|
2264
|
+
...options?.templates?.sentiment
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
const disabled = new Set(options?.disabled);
|
|
2268
|
+
const maxLength = options?.maxLength ?? 0;
|
|
2269
|
+
const allSections = [...createDefaultSections(templates), ...options?.extraSections ?? []];
|
|
2270
|
+
const sections = allSections.filter((s) => !disabled.has(s.key)).sort((a, b) => a.priority - b.priority);
|
|
2271
|
+
let prompt = "";
|
|
2272
|
+
const activeSections = [];
|
|
2273
|
+
for (const section of sections) {
|
|
2274
|
+
const text = section.render(ctx);
|
|
2275
|
+
if (text === null) continue;
|
|
2276
|
+
if (maxLength > 0 && prompt.length + text.length > maxLength) {
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
prompt += text;
|
|
2280
|
+
activeSections.push(section.key);
|
|
2281
|
+
}
|
|
2282
|
+
return { prompt, activeSections };
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// src/lib/pii/patterns.ts
|
|
2286
|
+
function luhnCheck(digits) {
|
|
2287
|
+
let sum = 0;
|
|
2288
|
+
let alternate = false;
|
|
2289
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
2290
|
+
let n = parseInt(digits[i], 10);
|
|
2291
|
+
if (alternate) {
|
|
2292
|
+
n *= 2;
|
|
2293
|
+
if (n > 9) n -= 9;
|
|
2294
|
+
}
|
|
2295
|
+
sum += n;
|
|
2296
|
+
alternate = !alternate;
|
|
2297
|
+
}
|
|
2298
|
+
return sum % 10 === 0;
|
|
2299
|
+
}
|
|
2300
|
+
var PII_PATTERNS = [
|
|
2037
2301
|
{
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
anchors: ["notion-search", "notion-create-pages"],
|
|
2041
|
-
anchorMinSimilarity: 0.53
|
|
2302
|
+
category: "EMAIL",
|
|
2303
|
+
regex: /\b[a-zA-Z0-9._%+-]{1,64}@[a-zA-Z0-9.-]{1,255}\.[a-zA-Z]{2,24}\b/g
|
|
2042
2304
|
},
|
|
2043
2305
|
{
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2306
|
+
category: "SSN",
|
|
2307
|
+
regex: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
2308
|
+
validate: (match) => {
|
|
2309
|
+
const parts = match.split("-");
|
|
2310
|
+
const area = parseInt(parts[0], 10);
|
|
2311
|
+
return area !== 0 && area !== 666 && area < 900;
|
|
2312
|
+
}
|
|
2048
2313
|
},
|
|
2049
2314
|
{
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
"
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
],
|
|
2060
|
-
// search is the primary entry point; list_channels also anchors so "what
|
|
2061
|
-
// channels am I in" reaches the set without going through search. post_message
|
|
2062
|
-
// anchors too so post-only prompts surface the write tool past the cutoff.
|
|
2063
|
-
anchors: ["slack_search_messages", "slack_list_channels", "slack_post_message"],
|
|
2064
|
-
anchorMinSimilarity: 0.53
|
|
2315
|
+
category: "SSN",
|
|
2316
|
+
regex: /\b\d{3}[ ]?\d{2}[ ]?\d{4}\b/g,
|
|
2317
|
+
context: /(?:\bssn\b|\bsocial\s+security(?:\s+(?:number|no\.?|#))?\b)[^\d\n]{0,12}$/i,
|
|
2318
|
+
validate: (match) => {
|
|
2319
|
+
const digits = match.replace(/\D/g, "");
|
|
2320
|
+
if (digits.length !== 9) return false;
|
|
2321
|
+
const area = parseInt(digits.slice(0, 3), 10);
|
|
2322
|
+
return area !== 0 && area !== 666 && area < 900;
|
|
2323
|
+
}
|
|
2065
2324
|
},
|
|
2066
2325
|
{
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2326
|
+
category: "CREDIT_CARD",
|
|
2327
|
+
regex: /\b\d{4}[\s-]\d{4}[\s-]\d{4}[\s-]\d{1,7}\b|\b\d{13,19}\b/g,
|
|
2328
|
+
validate: (match) => {
|
|
2329
|
+
const digits = match.replace(/\D/g, "");
|
|
2330
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
2331
|
+
return luhnCheck(digits);
|
|
2332
|
+
}
|
|
2071
2333
|
},
|
|
2072
2334
|
{
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
// Anchors are deliberately empty (like the client's Nearby set): the set
|
|
2079
|
-
// is only for stickiness, not a new way to activate on a prompt.
|
|
2080
|
-
// prompt_user_confirm is left out of members: autoFilterClientTools always
|
|
2081
|
-
// sends it, and as a member any unrelated confirmation would pin this set.
|
|
2082
|
-
name: "restaurant-booking",
|
|
2083
|
-
members: [
|
|
2084
|
-
"AnumaPaymentsMCP-anuma_find_restaurant",
|
|
2085
|
-
"AnumaPaymentsMCP-anuma_check_restaurant_availability",
|
|
2086
|
-
"AnumaPaymentsMCP-anuma_book_restaurant",
|
|
2087
|
-
"AnumaPaymentsMCP-anuma_list_reservations",
|
|
2088
|
-
"AnumaPaymentsMCP-anuma_cancel_reservation",
|
|
2089
|
-
"AnumaPaymentsMCP-anuma_discover_restaurants"
|
|
2090
|
-
],
|
|
2091
|
-
anchors: []
|
|
2335
|
+
category: "IP_ADDRESS",
|
|
2336
|
+
regex: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g,
|
|
2337
|
+
validate: (match) => {
|
|
2338
|
+
return match !== "0.0.0.0" && match !== "127.0.0.1" && match !== "255.255.255.255";
|
|
2339
|
+
}
|
|
2092
2340
|
},
|
|
2093
2341
|
{
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
// Anchors are empty for the same reason as restaurant-cancel below.
|
|
2098
|
-
name: "restaurant-book",
|
|
2099
|
-
members: [
|
|
2100
|
-
"AnumaPaymentsMCP-anuma_find_restaurant",
|
|
2101
|
-
"AnumaPaymentsMCP-anuma_check_restaurant_availability",
|
|
2102
|
-
"AnumaPaymentsMCP-anuma_book_restaurant"
|
|
2103
|
-
],
|
|
2104
|
-
anchors: []
|
|
2342
|
+
category: "IP_ADDRESS",
|
|
2343
|
+
regex: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))/g,
|
|
2344
|
+
validate: (match) => match !== "::1" && match !== "::"
|
|
2105
2345
|
},
|
|
2106
2346
|
{
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2347
|
+
category: "PHONE",
|
|
2348
|
+
regex: /(?:\+\d{1,3}[\s.-])?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b|\+\d{10,15}\b/g
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
category: "API_KEY",
|
|
2352
|
+
regex: /\b(?:sk|pk|api|key|token|secret|bearer|access)[_-]?[a-zA-Z0-9_-]{20,}\b/gi,
|
|
2353
|
+
validate: (match) => {
|
|
2354
|
+
const hasDigit = /\d/.test(match);
|
|
2355
|
+
const hasMixedCase = /[a-z]/.test(match) && /[A-Z]/.test(match);
|
|
2356
|
+
return hasDigit || hasMixedCase;
|
|
2357
|
+
}
|
|
2358
|
+
},
|
|
2359
|
+
{
|
|
2360
|
+
category: "US_ADDRESS",
|
|
2361
|
+
regex: /\b\d{1,5}[ \t]+(?:[A-Z][a-z]+[ \t]+){1,3}(?:St(?:reet)?|Ave(?:nue)?|Blvd|Boulevard|Dr(?:ive)?|Ln|Lane|Rd|Road|Ct|Court|Pl(?:ace)?|Way|Cir(?:cle)?|Pkwy|Parkway|Hwy|Highway)\.?(?=[\s,.;:!?)]|$)/g,
|
|
2362
|
+
validate: (match) => {
|
|
2363
|
+
return !/^0/.test(match.trim());
|
|
2364
|
+
}
|
|
2365
|
+
},
|
|
2366
|
+
{
|
|
2367
|
+
category: "DATE_OF_BIRTH",
|
|
2368
|
+
regex: /\b(?:(?:0?[1-9]|1[0-2])[/-](?:0?[1-9]|[12]\d|3[01])[/-](?:19|20)\d{2})\b/g,
|
|
2369
|
+
context: /(?:\bdob\b|\bd\.?o\.?b\.?|\bborn\b|\bbirth\s?date\b|\bbirthdate\b|\bbirthday\b|\bdate\s+of\s+birth\b)[^\d\n]{0,12}$/i,
|
|
2370
|
+
validate: (match) => {
|
|
2371
|
+
const m = match.match(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/);
|
|
2372
|
+
if (!m) return false;
|
|
2373
|
+
const month = parseInt(m[1], 10);
|
|
2374
|
+
const day = parseInt(m[2], 10);
|
|
2375
|
+
const year = parseInt(m[3], 10);
|
|
2376
|
+
const isLeap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
2377
|
+
const daysInMonth = [31, isLeap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
2378
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth[month - 1];
|
|
2379
|
+
}
|
|
2119
2380
|
}
|
|
2120
2381
|
];
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2382
|
+
|
|
2383
|
+
// src/lib/pii/redactor.ts
|
|
2384
|
+
function escapeRegExp(value) {
|
|
2385
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2386
|
+
}
|
|
2387
|
+
function isWordChar(ch) {
|
|
2388
|
+
return /[A-Za-z0-9_]/.test(ch) || ch.charCodeAt(0) > 127;
|
|
2389
|
+
}
|
|
2390
|
+
var PiiRedactor = class {
|
|
2391
|
+
constructor(options = {}) {
|
|
2392
|
+
/** Maps original PII value → placeholder tag. */
|
|
2393
|
+
this.valueToPlaceholder = /* @__PURE__ */ new Map();
|
|
2394
|
+
/** Maps placeholder tag → original PII value (reverse lookup for de-anonymization). */
|
|
2395
|
+
this.placeholderToValue = /* @__PURE__ */ new Map();
|
|
2396
|
+
/** Next index per category for placeholder numbering. */
|
|
2397
|
+
this.categoryCounters = /* @__PURE__ */ new Map();
|
|
2398
|
+
/**
|
|
2399
|
+
* Matches any placeholder-shaped token for this redactor's categories —
|
|
2400
|
+
* bracketed `[EMAIL_1]` OR bare `EMAIL_1`, case-insensitive — capturing the
|
|
2401
|
+
* body. Drives {@link restoreForStorage}: the extraction / consolidation
|
|
2402
|
+
* models echo placeholders mangled (brackets dropped, re-cased), and a single
|
|
2403
|
+
* pass with this matcher restores the assigned ones and flags the rest in one
|
|
2404
|
+
* go. Built lazily from the union of pattern categories AND any categories
|
|
2405
|
+
* minted at runtime (e.g. NER `PERSON`/`LOCATION`/`ORG`): `undefined` = stale
|
|
2406
|
+
* (rebuild on next use), `null` = built but there are no categories.
|
|
2407
|
+
*/
|
|
2408
|
+
this.storagePattern = void 0;
|
|
2409
|
+
/**
|
|
2410
|
+
* Cached body→value lookups for {@link restoreForStorage}, built lazily from
|
|
2411
|
+
* the assigned placeholders and invalidated on each mint. `exact` is keyed by
|
|
2412
|
+
* the literal body (collision-proof); `ci` by upper-cased body for re-cased
|
|
2413
|
+
* echoes, minus any `ciCollisions` where two distinct bodies share a form.
|
|
2414
|
+
*/
|
|
2415
|
+
this.storageLookup = null;
|
|
2416
|
+
/** Whether the non-text-content bypass warning has already been emitted. */
|
|
2417
|
+
this.warnedNonText = false;
|
|
2418
|
+
/** Whether the NER-detector-failure warning has already been emitted. */
|
|
2419
|
+
this.warnedNerFailure = false;
|
|
2420
|
+
this.nerDetector = options.nerDetector;
|
|
2421
|
+
if (options.patterns) {
|
|
2422
|
+
this.patterns = options.patterns;
|
|
2423
|
+
} else {
|
|
2424
|
+
let base = PII_PATTERNS;
|
|
2425
|
+
if (options.excludeCategories?.length) {
|
|
2426
|
+
const excluded = new Set(options.excludeCategories);
|
|
2427
|
+
base = base.filter((p) => !excluded.has(p.category));
|
|
2428
|
+
}
|
|
2429
|
+
this.patterns = options.extraPatterns ? [...base, ...options.extraPatterns] : base;
|
|
2430
|
+
}
|
|
2431
|
+
this.patternCategories = [...new Set(this.patterns.map((p) => String(p.category)))];
|
|
2129
2432
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2433
|
+
/**
|
|
2434
|
+
* Build the {@link storagePattern} from the union of pattern categories and
|
|
2435
|
+
* any categories minted at runtime (NER entities arrive only when redaction
|
|
2436
|
+
* runs, so this can't be fixed at construction). Bracketed `[EMAIL_1]` OR
|
|
2437
|
+
* bare `EMAIL_1`, capturing the body, case-insensitive, global. `\d+` (not a
|
|
2438
|
+
* fixed body) spans any index; `\b` on the bare arm keeps it off prose like
|
|
2439
|
+
* "email 1 of 3".
|
|
2440
|
+
*/
|
|
2441
|
+
buildStoragePattern() {
|
|
2442
|
+
const categories = [.../* @__PURE__ */ new Set([...this.patternCategories, ...this.categoryCounters.keys()])];
|
|
2443
|
+
if (categories.length === 0) return null;
|
|
2444
|
+
const categoryAlt = categories.sort((a, b) => b.length - a.length).map(escapeRegExp).join("|");
|
|
2445
|
+
return new RegExp(`\\[((?:${categoryAlt})_\\d+)\\]|\\b((?:${categoryAlt})_\\d+)\\b`, "gi");
|
|
2134
2446
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2447
|
+
/**
|
|
2448
|
+
* Returns the current number of unique PII values tracked.
|
|
2449
|
+
*/
|
|
2450
|
+
get size() {
|
|
2451
|
+
return this.valueToPlaceholder.size;
|
|
2140
2452
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2453
|
+
/**
|
|
2454
|
+
* Returns a snapshot of all placeholder → original value mappings.
|
|
2455
|
+
* Useful for debugging or UI display. This is a copy — mutating it (or
|
|
2456
|
+
* later redactions) does not affect the returned map and vice versa.
|
|
2457
|
+
*/
|
|
2458
|
+
getMappings() {
|
|
2459
|
+
return new Map(this.placeholderToValue);
|
|
2147
2460
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2461
|
+
/**
|
|
2462
|
+
* Get or create a placeholder for a PII value.
|
|
2463
|
+
* The same value always gets the same placeholder within a redactor instance.
|
|
2464
|
+
*/
|
|
2465
|
+
getPlaceholder(category, value) {
|
|
2466
|
+
const normalizedValue = value.trim();
|
|
2467
|
+
const existing = this.valueToPlaceholder.get(normalizedValue);
|
|
2468
|
+
if (existing) return existing;
|
|
2469
|
+
const existingCount = this.categoryCounters.get(category);
|
|
2470
|
+
const count = (existingCount ?? 0) + 1;
|
|
2471
|
+
this.categoryCounters.set(category, count);
|
|
2472
|
+
if (existingCount === void 0) this.storagePattern = void 0;
|
|
2473
|
+
const placeholder = `[${category}_${count}]`;
|
|
2474
|
+
this.valueToPlaceholder.set(normalizedValue, placeholder);
|
|
2475
|
+
this.placeholderToValue.set(placeholder, normalizedValue);
|
|
2476
|
+
this.storageLookup = null;
|
|
2477
|
+
return placeholder;
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Find all matches of a single pattern in `text`, applying its `validate` and
|
|
2481
|
+
* `context` guards. Matches are non-overlapping and ascending (the global
|
|
2482
|
+
* regex advances `lastIndex` past each match). Shared by the sync {@link scan}
|
|
2483
|
+
* (sequential, per-pattern) and the async {@link collectRegexSpans} (span)
|
|
2484
|
+
* paths so the detection rules live in exactly one place.
|
|
2485
|
+
*/
|
|
2486
|
+
findMatches(text, pattern) {
|
|
2487
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
2488
|
+
const found = [];
|
|
2489
|
+
let m;
|
|
2490
|
+
while ((m = regex.exec(text)) !== null) {
|
|
2491
|
+
const value = m[0];
|
|
2492
|
+
if (pattern.validate && !pattern.validate(value)) continue;
|
|
2493
|
+
if (pattern.context) {
|
|
2494
|
+
const before = text.slice(Math.max(0, m.index - 40), m.index);
|
|
2495
|
+
if (!pattern.context.test(before)) continue;
|
|
2162
2496
|
}
|
|
2497
|
+
found.push({ match: value, index: m.index });
|
|
2163
2498
|
}
|
|
2499
|
+
return found;
|
|
2164
2500
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2501
|
+
/**
|
|
2502
|
+
* Scan text for PII and rebuild it, replacing each match with whatever
|
|
2503
|
+
* `makeReplacement(category, value)` returns. Shared by `redactText`
|
|
2504
|
+
* (numbered, stateful placeholders) and `maskText` (unnumbered, stateless).
|
|
2505
|
+
*
|
|
2506
|
+
* Patterns are applied sequentially: each scans the text already rewritten by
|
|
2507
|
+
* earlier patterns, so an earlier (more specific) pattern claims a region
|
|
2508
|
+
* before a later one can — placeholders are inert and match no pattern.
|
|
2509
|
+
*/
|
|
2510
|
+
scan(text, makeReplacement) {
|
|
2511
|
+
const matches = [];
|
|
2512
|
+
let redacted = text;
|
|
2513
|
+
for (const pattern of this.patterns) {
|
|
2514
|
+
const found = this.findMatches(redacted, pattern);
|
|
2515
|
+
if (found.length === 0) continue;
|
|
2516
|
+
let result = "";
|
|
2517
|
+
let cursor = 0;
|
|
2518
|
+
for (const { match, index } of found) {
|
|
2519
|
+
const placeholder = makeReplacement(pattern.category, match);
|
|
2520
|
+
matches.push({ category: pattern.category, original: match, placeholder });
|
|
2521
|
+
result += redacted.slice(cursor, index) + placeholder;
|
|
2522
|
+
cursor = index + match.length;
|
|
2523
|
+
}
|
|
2524
|
+
result += redacted.slice(cursor);
|
|
2525
|
+
redacted = result;
|
|
2177
2526
|
}
|
|
2527
|
+
return { text: redacted, matches };
|
|
2178
2528
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
for (const ts of toolSets) {
|
|
2186
|
-
const prompt = ts.systemPrompt;
|
|
2187
|
-
if (!prompt || seen.has(prompt)) continue;
|
|
2188
|
-
const active = activatedSetNames ? activatedSetNames.has(ts.name) : ts.anchors.some((anchor) => selected.has(anchor));
|
|
2189
|
-
if (active) {
|
|
2190
|
-
prompts.push(prompt);
|
|
2191
|
-
seen.add(prompt);
|
|
2192
|
-
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Scan and redact PII from a single text string, using numbered, reversible
|
|
2531
|
+
* placeholders ([EMAIL_1], …) tracked on this instance.
|
|
2532
|
+
*/
|
|
2533
|
+
redactText(text) {
|
|
2534
|
+
return this.scan(text, (category, value) => this.getPlaceholder(category, value));
|
|
2193
2535
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
const matchedNames = new Set(matches.map((m) => m.tool.name));
|
|
2204
|
-
let finalNames;
|
|
2205
|
-
if (sets.length > 0) {
|
|
2206
|
-
const scores = scoreTools(embeddings, tools);
|
|
2207
|
-
const selectedScores = new Map(
|
|
2208
|
-
[...scores].filter(([name]) => matchedNames.has(name) && !exclude.has(name))
|
|
2209
|
-
);
|
|
2210
|
-
const availableNames = new Set(tools.map((t) => t.name));
|
|
2211
|
-
finalNames = expandToolSetsAdditive(matchedNames, availableNames, selectedScores, sets);
|
|
2212
|
-
} else {
|
|
2213
|
-
finalNames = matchedNames;
|
|
2214
|
-
}
|
|
2215
|
-
for (const name of exclude) finalNames.delete(name);
|
|
2216
|
-
return [...finalNames];
|
|
2217
|
-
};
|
|
2218
|
-
return Object.defineProperty(filter, "excludeTools", {
|
|
2219
|
-
value: Object.freeze([...exclude]),
|
|
2220
|
-
enumerable: false
|
|
2221
|
-
});
|
|
2222
|
-
}
|
|
2223
|
-
var DEFAULT_EXCLUDED_SERVER_TOOLS = [
|
|
2224
|
-
"AnumaVisionMCP-anuma_analyze_image",
|
|
2225
|
-
"OpenMeteoMCP-weather_forecast",
|
|
2226
|
-
// Same native-capability argument as the vision tool: modern models reason
|
|
2227
|
-
// step-by-step natively (and reasoning-mode models do it structurally), so
|
|
2228
|
-
// a server-side sequential-thinking tool is a redundant hop. Its broad
|
|
2229
|
-
// description also embeds near virtually every prompt — observed live
|
|
2230
|
-
// riding into news-search, scheduling, and chitchat requests.
|
|
2231
|
-
"AnumaSequentialThinkingMCP-sequentialthinking"
|
|
2232
|
-
];
|
|
2233
|
-
var DEFAULT_SERVER_TOOLS_MATCH_OPTIONS = {
|
|
2234
|
-
limit: 5,
|
|
2235
|
-
minSimilarity: 0.5
|
|
2236
|
-
};
|
|
2237
|
-
var SERVER_TOOL_DEPENDENCY_SETS = [
|
|
2238
|
-
{
|
|
2239
|
-
// search finds links; reading them is always the next step. The reader now
|
|
2240
|
-
// lives on the search server — Jina's read_url / parallel_read_url were
|
|
2241
|
-
// removed in favour of anuma_scrape_url, which takes a batch of URLs — so
|
|
2242
|
-
// this edge deliberately spans two MCP servers.
|
|
2243
|
-
name: "web-research",
|
|
2244
|
-
members: [
|
|
2245
|
-
"AnumaJinaMCP-search_web",
|
|
2246
|
-
"AnumaSearchMCP-anuma_scrape_url",
|
|
2247
|
-
"AnumaJinaMCP-parallel_search_web"
|
|
2248
|
-
],
|
|
2249
|
-
anchors: ["AnumaJinaMCP-search_web"],
|
|
2250
|
-
// Activation floor = the selection floor (0.5), not the 0.6 ToolSet
|
|
2251
|
-
// default. A dependency edge has different semantics from a persona-style
|
|
2252
|
-
// tool set: if the entry tool is plausibly offered at all, the chain must
|
|
2253
|
-
// ride along — "Search the web for recent news…" scores search_web in the
|
|
2254
|
-
// 0.5–0.6 band, and shipping search without read would dead-end the model.
|
|
2255
|
-
anchorMinSimilarity: 0.5
|
|
2256
|
-
},
|
|
2257
|
-
{
|
|
2258
|
-
// Every OpenMeteo data tool requires latitude/longitude (verified in the
|
|
2259
|
-
// catalog schemas), but users speak in place names — geocoding is the
|
|
2260
|
-
// mandatory first hop. Members deliberately contain ONLY the dependency:
|
|
2261
|
-
// the anchor that fired is already in the semantic matches, and pulling
|
|
2262
|
-
// sibling data tools in would re-create vendor-suite over-inclusion.
|
|
2263
|
-
// Excluded tools (e.g. weather_forecast under the default filter) can't
|
|
2264
|
-
// anchor this set — createServerToolsFilter drops them before expansion —
|
|
2265
|
-
// but the non-weather data tools still deliver geocoding when they match.
|
|
2266
|
-
name: "openmeteo-geocode",
|
|
2267
|
-
members: ["OpenMeteoMCP-geocoding"],
|
|
2268
|
-
anchors: [
|
|
2269
|
-
"OpenMeteoMCP-weather_forecast",
|
|
2270
|
-
"OpenMeteoMCP-air_quality",
|
|
2271
|
-
"OpenMeteoMCP-weather_archive",
|
|
2272
|
-
"OpenMeteoMCP-marine_weather",
|
|
2273
|
-
"OpenMeteoMCP-flood_forecast",
|
|
2274
|
-
"OpenMeteoMCP-climate_projection",
|
|
2275
|
-
"OpenMeteoMCP-elevation"
|
|
2276
|
-
],
|
|
2277
|
-
anchorMinSimilarity: 0.5
|
|
2278
|
-
},
|
|
2279
|
-
{
|
|
2280
|
-
// The cancel tool takes its resy_token and the other values only from a
|
|
2281
|
-
// live anuma_list_reservations result in the same turn, never from
|
|
2282
|
-
// memory, so the list tool must ride in whenever cancel is offered.
|
|
2283
|
-
// Members hold only the dependency, like openmeteo-geocode.
|
|
2284
|
-
name: "restaurant-cancel-lookup",
|
|
2285
|
-
members: ["AnumaPaymentsMCP-anuma_list_reservations"],
|
|
2286
|
-
anchors: ["AnumaPaymentsMCP-anuma_cancel_reservation"],
|
|
2287
|
-
anchorMinSimilarity: 0.5
|
|
2536
|
+
/**
|
|
2537
|
+
* Mask PII with unnumbered, NON-reversible tokens ([EMAIL], [SSN], …) without
|
|
2538
|
+
* mutating this instance's state. Use for one-way purposes where the value is
|
|
2539
|
+
* never restored — notably embedding inputs: it keeps real PII off the
|
|
2540
|
+
* embeddings endpoint while staying deterministic, so the same content always
|
|
2541
|
+
* masks identically and search stays consistent across conversations.
|
|
2542
|
+
*/
|
|
2543
|
+
maskText(text) {
|
|
2544
|
+
return this.scan(text, (category) => `[${category}]`).text;
|
|
2288
2545
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
})
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
minSimilarity,
|
|
2306
|
-
apiType = "responses"
|
|
2307
|
-
} = options;
|
|
2308
|
-
if (!getToken && !apiKey) {
|
|
2309
|
-
throw new Error("Either getToken or apiKey must be provided");
|
|
2546
|
+
/** All regex matches as spans on the ORIGINAL text (priority 0). Uses the
|
|
2547
|
+
* same {@link findMatches} guards as the sync scan. */
|
|
2548
|
+
collectRegexSpans(text) {
|
|
2549
|
+
const spans = [];
|
|
2550
|
+
this.patterns.forEach((pattern, patternIndex) => {
|
|
2551
|
+
for (const { match, index } of this.findMatches(text, pattern)) {
|
|
2552
|
+
spans.push({
|
|
2553
|
+
start: index,
|
|
2554
|
+
end: index + match.length,
|
|
2555
|
+
category: pattern.category,
|
|
2556
|
+
priority: 0,
|
|
2557
|
+
order: patternIndex
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
});
|
|
2561
|
+
return spans;
|
|
2310
2562
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2563
|
+
/**
|
|
2564
|
+
* Normalize raw NER spans: clamp to the text bounds, drop empty/categoryless
|
|
2565
|
+
* spans, and snap each to whole-word boundaries. Snapping prevents a detector
|
|
2566
|
+
* that tagged only part of a word (a common WordPiece artifact — e.g. "Strip"
|
|
2567
|
+
* of "Stripe") from producing a corrupting partial placeholder like
|
|
2568
|
+
* `[ORG_1]e`.
|
|
2569
|
+
*/
|
|
2570
|
+
normalizeNerSpans(text, raw) {
|
|
2571
|
+
if (!Array.isArray(raw)) return [];
|
|
2572
|
+
const out = [];
|
|
2573
|
+
raw.forEach((s, i) => {
|
|
2574
|
+
if (!Number.isFinite(s.start) || !Number.isFinite(s.end)) return;
|
|
2575
|
+
let start = Math.max(0, Math.min(Math.trunc(s.start), text.length));
|
|
2576
|
+
let end = Math.max(0, Math.min(Math.trunc(s.end), text.length));
|
|
2577
|
+
if (end <= start) return;
|
|
2578
|
+
while (start > 0 && isWordChar(text[start - 1])) start--;
|
|
2579
|
+
while (end < text.length && isWordChar(text[end])) end++;
|
|
2580
|
+
const category = String(s.category ?? "").trim();
|
|
2581
|
+
if (!category) return;
|
|
2582
|
+
out.push({ start, end, category, priority: 1, order: i });
|
|
2583
|
+
});
|
|
2584
|
+
return out;
|
|
2313
2585
|
}
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2586
|
+
/** Run the configured detector defensively: failures degrade to regex-only
|
|
2587
|
+
* (fail-open on detection, not on protection) with a one-time warning. */
|
|
2588
|
+
async detectNerSpans(text) {
|
|
2589
|
+
if (!this.nerDetector) return [];
|
|
2590
|
+
try {
|
|
2591
|
+
return this.normalizeNerSpans(text, await this.nerDetector.detect(text));
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
this.warnNerFailure(err);
|
|
2594
|
+
return [];
|
|
2595
|
+
}
|
|
2323
2596
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2597
|
+
/**
|
|
2598
|
+
* Resolve overlaps by placing higher-priority spans first and dropping any
|
|
2599
|
+
* span that overlaps one already placed. Regex (priority 0) is placed before
|
|
2600
|
+
* NER (priority 1), and within regex by ascending pattern index — so regex
|
|
2601
|
+
* always wins on overlap and the regex-only result reproduces the sync scan's
|
|
2602
|
+
* sequential "earlier pattern wins". O(n²) but n (spans per message) is tiny.
|
|
2603
|
+
*
|
|
2604
|
+
* The returned spans stay in `(priority, order, start)` order — i.e. mint
|
|
2605
|
+
* order — which {@link rebuildSpans} relies on.
|
|
2606
|
+
*/
|
|
2607
|
+
resolveOverlaps(spans) {
|
|
2608
|
+
const ordered = [...spans].sort(
|
|
2609
|
+
(a, b) => a.priority - b.priority || a.order - b.order || a.start - b.start
|
|
2336
2610
|
);
|
|
2337
|
-
|
|
2338
|
-
|
|
2611
|
+
const placed = [];
|
|
2612
|
+
for (const s of ordered) {
|
|
2613
|
+
if (!placed.some((p) => s.start < p.end && p.start < s.end)) placed.push(s);
|
|
2614
|
+
}
|
|
2615
|
+
return placed;
|
|
2339
2616
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2617
|
+
/**
|
|
2618
|
+
* Rebuild `text` from resolved (non-overlapping) spans. `spans` must arrive in
|
|
2619
|
+
* mint order — `(priority, order, start)`, as {@link resolveOverlaps} returns —
|
|
2620
|
+
* so placeholders are MINTED by iterating it directly (regex categories first,
|
|
2621
|
+
* in pattern then document order, matching the sync scan's numbering). The
|
|
2622
|
+
* string is then EMITTED in document order.
|
|
2623
|
+
*/
|
|
2624
|
+
rebuildSpans(text, spans, makeReplacement) {
|
|
2625
|
+
const placeholderByStart = /* @__PURE__ */ new Map();
|
|
2626
|
+
const matches = [];
|
|
2627
|
+
for (const s of spans) {
|
|
2628
|
+
const value = text.slice(s.start, s.end);
|
|
2629
|
+
const placeholder = makeReplacement(s.category, value);
|
|
2630
|
+
placeholderByStart.set(s.start, placeholder);
|
|
2631
|
+
matches.push({ category: s.category, original: value, placeholder });
|
|
2632
|
+
}
|
|
2633
|
+
const emitOrder = [...spans].sort((a, b) => a.start - b.start);
|
|
2634
|
+
let result = "";
|
|
2635
|
+
let cursor = 0;
|
|
2636
|
+
for (const s of emitOrder) {
|
|
2637
|
+
result += text.slice(cursor, s.start) + (placeholderByStart.get(s.start) ?? "");
|
|
2638
|
+
cursor = s.end;
|
|
2639
|
+
}
|
|
2640
|
+
result += text.slice(cursor);
|
|
2641
|
+
return { text: result, matches };
|
|
2346
2642
|
}
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2643
|
+
/** Merge regex + NER spans for `text` into a resolved, non-overlapping set. */
|
|
2644
|
+
async detectAllSpans(text) {
|
|
2645
|
+
const nerPromise = this.detectNerSpans(text);
|
|
2646
|
+
const regexSpans = this.collectRegexSpans(text);
|
|
2647
|
+
const ner = await nerPromise;
|
|
2648
|
+
return this.resolveOverlaps([...regexSpans, ...ner]);
|
|
2350
2649
|
}
|
|
2351
|
-
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
provider: "gcalendar",
|
|
2365
|
-
connector: "Calendar"
|
|
2366
|
-
},
|
|
2367
|
-
google_calendar_create_event: {
|
|
2368
|
-
label: "Create calendar event",
|
|
2369
|
-
provider: "gcalendar",
|
|
2370
|
-
connector: "Calendar"
|
|
2371
|
-
},
|
|
2372
|
-
google_calendar_update_event: {
|
|
2373
|
-
label: "Update calendar event",
|
|
2374
|
-
provider: "gcalendar",
|
|
2375
|
-
connector: "Calendar"
|
|
2376
|
-
},
|
|
2377
|
-
// Google Drive (src/tools/googleDrive.ts)
|
|
2378
|
-
google_drive_search: { label: "Search files", provider: "gdrive", connector: "Drive" },
|
|
2379
|
-
google_drive_list_recent: { label: "List recent files", provider: "gdrive", connector: "Drive" },
|
|
2380
|
-
google_drive_get_content: { label: "Read file content", provider: "gdrive", connector: "Drive" },
|
|
2381
|
-
google_drive_create_file: {
|
|
2382
|
-
label: "Create a file in Drive",
|
|
2383
|
-
provider: "gdrive",
|
|
2384
|
-
connector: "Drive"
|
|
2385
|
-
},
|
|
2386
|
-
google_drive_update_file: {
|
|
2387
|
-
label: "Update a Drive file",
|
|
2388
|
-
provider: "gdrive",
|
|
2389
|
-
connector: "Drive"
|
|
2390
|
-
},
|
|
2391
|
-
// Notion (src/tools/notion.ts)
|
|
2392
|
-
"notion-search": { label: "Search", provider: "notion", connector: "Notion" },
|
|
2393
|
-
"notion-fetch": { label: "Read page", provider: "notion", connector: "Notion" },
|
|
2394
|
-
"notion-create-pages": { label: "Create page", provider: "notion", connector: "Notion" },
|
|
2395
|
-
"notion-update-page": { label: "Update page", provider: "notion", connector: "Notion" },
|
|
2396
|
-
"notion-move-pages": { label: "Move pages", provider: "notion", connector: "Notion" },
|
|
2397
|
-
"notion-duplicate-page": { label: "Duplicate page", provider: "notion", connector: "Notion" },
|
|
2398
|
-
"notion-create-database": { label: "Create database", provider: "notion", connector: "Notion" },
|
|
2399
|
-
"notion-update-data-source": {
|
|
2400
|
-
label: "Update data source",
|
|
2401
|
-
provider: "notion",
|
|
2402
|
-
connector: "Notion"
|
|
2403
|
-
},
|
|
2404
|
-
"notion-create-comment": { label: "Add a comment", provider: "notion", connector: "Notion" },
|
|
2405
|
-
"notion-get-comments": { label: "Read comments", provider: "notion", connector: "Notion" },
|
|
2406
|
-
"notion-get-users": {
|
|
2407
|
-
label: "List workspace members",
|
|
2408
|
-
provider: "notion",
|
|
2409
|
-
connector: "Notion"
|
|
2410
|
-
},
|
|
2411
|
-
"notion-get-teams": { label: "List teamspaces", provider: "notion", connector: "Notion" },
|
|
2412
|
-
// GitHub (src/tools/github.ts)
|
|
2413
|
-
github_api: { label: "Use GitHub", provider: "github", connector: "GitHub" },
|
|
2414
|
-
github_get_authenticated_user: {
|
|
2415
|
-
label: "Read GitHub profile",
|
|
2416
|
-
provider: "github",
|
|
2417
|
-
connector: "GitHub"
|
|
2418
|
-
},
|
|
2419
|
-
// X / Twitter (src/tools/x.ts)
|
|
2420
|
-
x_get_me: { label: "Get my X profile", provider: "x", connector: "X" },
|
|
2421
|
-
x_get_my_posts: { label: "Read my recent posts", provider: "x", connector: "X" },
|
|
2422
|
-
// Dropbox (src/tools/dropbox.ts)
|
|
2423
|
-
dropbox_list_folders: { label: "List files", provider: "dropbox", connector: "Dropbox" },
|
|
2424
|
-
dropbox_get_file_content: {
|
|
2425
|
-
label: "Read file content",
|
|
2426
|
-
provider: "dropbox",
|
|
2427
|
-
connector: "Dropbox"
|
|
2428
|
-
},
|
|
2429
|
-
dropbox_search: { label: "Search files", provider: "dropbox", connector: "Dropbox" },
|
|
2430
|
-
// Slack (src/tools/slack.ts)
|
|
2431
|
-
slack_get_me: { label: "Get my Slack profile", provider: "slack", connector: "Slack" },
|
|
2432
|
-
slack_list_channels: { label: "List channels", provider: "slack", connector: "Slack" },
|
|
2433
|
-
slack_list_dms: { label: "List direct messages", provider: "slack", connector: "Slack" },
|
|
2434
|
-
slack_search_messages: { label: "Search messages", provider: "slack", connector: "Slack" },
|
|
2435
|
-
slack_list_users: { label: "List members", provider: "slack", connector: "Slack" },
|
|
2436
|
-
slack_get_channel_history: {
|
|
2437
|
-
label: "Read channel history",
|
|
2438
|
-
provider: "slack",
|
|
2439
|
-
connector: "Slack"
|
|
2440
|
-
},
|
|
2441
|
-
slack_get_thread_replies: {
|
|
2442
|
-
label: "Read thread replies",
|
|
2443
|
-
provider: "slack",
|
|
2444
|
-
connector: "Slack"
|
|
2445
|
-
},
|
|
2446
|
-
slack_post_message: { label: "Post a message", provider: "slack", connector: "Slack" }
|
|
2447
|
-
};
|
|
2448
|
-
function buildDeniedToolsRider(deniedToolNames) {
|
|
2449
|
-
const byConnector = /* @__PURE__ */ new Map();
|
|
2450
|
-
for (const name of deniedToolNames) {
|
|
2451
|
-
const entry = TOOL_CATALOG[name];
|
|
2452
|
-
if (!entry) continue;
|
|
2453
|
-
const labels = byConnector.get(entry.connector) ?? [];
|
|
2454
|
-
labels.push(entry.label);
|
|
2455
|
-
byConnector.set(entry.connector, labels);
|
|
2456
|
-
}
|
|
2457
|
-
if (byConnector.size === 0) return "";
|
|
2458
|
-
const groups = [...byConnector.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
|
|
2459
|
-
([connector, labels]) => `- ${connector}: ${[...labels].sort((a, b) => a.localeCompare(b)).join(", ")}`
|
|
2460
|
-
).join("\n");
|
|
2461
|
-
return [
|
|
2462
|
-
"The user has turned off the following connector capabilities:",
|
|
2463
|
-
groups,
|
|
2464
|
-
"If the user asks you to use one of these, do not attempt it. Tell them the specific capability is turned off and that they can re-enable that exact tool in their Connected Apps settings."
|
|
2465
|
-
].join("\n");
|
|
2466
|
-
}
|
|
2467
|
-
function toolName(tool) {
|
|
2468
|
-
const record = tool;
|
|
2469
|
-
const func = record.function;
|
|
2470
|
-
if (typeof func?.name === "string") return func.name;
|
|
2471
|
-
if (typeof record.name === "string") return record.name;
|
|
2472
|
-
return void 0;
|
|
2473
|
-
}
|
|
2474
|
-
function catalogConnectorsByProvider() {
|
|
2475
|
-
const byProvider = /* @__PURE__ */ new Map();
|
|
2476
|
-
for (const entry of Object.values(TOOL_CATALOG)) {
|
|
2477
|
-
byProvider.set(entry.provider, entry.connector);
|
|
2478
|
-
}
|
|
2479
|
-
return byProvider;
|
|
2480
|
-
}
|
|
2481
|
-
function buildConnectorGuidance(input) {
|
|
2482
|
-
const denied = new Set(input.deniedToolNames);
|
|
2483
|
-
const connected = new Set(input.connectedProviders);
|
|
2484
|
-
const tools = input.tools.filter((tool) => {
|
|
2485
|
-
const name = toolName(tool);
|
|
2486
|
-
return name === void 0 || !denied.has(name);
|
|
2487
|
-
});
|
|
2488
|
-
const deniedByConnector = /* @__PURE__ */ new Map();
|
|
2489
|
-
for (const name of denied) {
|
|
2490
|
-
const entry = TOOL_CATALOG[name];
|
|
2491
|
-
if (!entry || !connected.has(entry.provider)) continue;
|
|
2492
|
-
const labels = deniedByConnector.get(entry.connector) ?? [];
|
|
2493
|
-
labels.push(entry.label);
|
|
2494
|
-
deniedByConnector.set(entry.connector, labels);
|
|
2650
|
+
/**
|
|
2651
|
+
* Async counterpart to {@link redactText}: redacts structured PII (regex) AND
|
|
2652
|
+
* unstructured PII (the configured {@link NerDetector}) with numbered,
|
|
2653
|
+
* reversible placeholders. With no detector, identical to `redactText`.
|
|
2654
|
+
*/
|
|
2655
|
+
async redactTextAsync(text) {
|
|
2656
|
+
if (!this.nerDetector) return this.redactText(text);
|
|
2657
|
+
const spans = await this.detectAllSpans(text);
|
|
2658
|
+
return this.rebuildSpans(
|
|
2659
|
+
text,
|
|
2660
|
+
spans,
|
|
2661
|
+
(category, value) => this.getPlaceholder(category, value)
|
|
2662
|
+
);
|
|
2495
2663
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2664
|
+
/**
|
|
2665
|
+
* Async counterpart to {@link maskText}: stateless `[CATEGORY]` masking over
|
|
2666
|
+
* regex + NER matches. With no detector, identical to `maskText`.
|
|
2667
|
+
*/
|
|
2668
|
+
async maskTextAsync(text) {
|
|
2669
|
+
if (!this.nerDetector) return this.maskText(text);
|
|
2670
|
+
const spans = await this.detectAllSpans(text);
|
|
2671
|
+
return this.rebuildSpans(text, spans, (category) => `[${category}]`).text;
|
|
2499
2672
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2673
|
+
/**
|
|
2674
|
+
* Async counterpart to {@link redactMessages}. Text parts are redacted
|
|
2675
|
+
* sequentially so placeholder numbering stays deterministic across the
|
|
2676
|
+
* conversation. With no detector, identical to `redactMessages`.
|
|
2677
|
+
*/
|
|
2678
|
+
async redactMessagesAsync(messages) {
|
|
2679
|
+
if (!this.nerDetector) return this.redactMessages(messages);
|
|
2680
|
+
const allMatches = [];
|
|
2681
|
+
const redactedMessages = [];
|
|
2682
|
+
for (const msg of messages) {
|
|
2683
|
+
if (!msg.content) {
|
|
2684
|
+
redactedMessages.push(msg);
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
const newContent = [];
|
|
2688
|
+
for (const part of msg.content) {
|
|
2689
|
+
if (part.type === "text" && part.text) {
|
|
2690
|
+
const result = await this.redactTextAsync(part.text);
|
|
2691
|
+
allMatches.push(...result.matches);
|
|
2692
|
+
newContent.push(result.matches.length > 0 ? { ...part, text: result.text } : part);
|
|
2693
|
+
} else {
|
|
2694
|
+
if (part.type !== "text") this.warnNonTextBypass();
|
|
2695
|
+
newContent.push(part);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
redactedMessages.push({ ...msg, content: newContent });
|
|
2699
|
+
}
|
|
2700
|
+
return { messages: redactedMessages, matches: allMatches };
|
|
2509
2701
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2702
|
+
/**
|
|
2703
|
+
* Redact PII from an array of LlmapiMessage objects.
|
|
2704
|
+
* Returns new message objects — originals are not mutated.
|
|
2705
|
+
*
|
|
2706
|
+
* All roles' text content is redacted, not just user/system: tool results
|
|
2707
|
+
* can return PII fetched from external systems, and assistant history is
|
|
2708
|
+
* persisted with original (de-anonymized) values, so both must be re-redacted
|
|
2709
|
+
* before they go back over the wire. Placeholders the model already emitted
|
|
2710
|
+
* are inert here (they don't match any PII pattern), so re-redaction is safe.
|
|
2711
|
+
*/
|
|
2712
|
+
redactMessages(messages) {
|
|
2713
|
+
const allMatches = [];
|
|
2714
|
+
const redactedMessages = messages.map((msg) => {
|
|
2715
|
+
if (!msg.content) return msg;
|
|
2716
|
+
const newContent = msg.content.map((part) => {
|
|
2717
|
+
if (part.type === "text" && part.text) {
|
|
2718
|
+
const result = this.redactText(part.text);
|
|
2719
|
+
allMatches.push(...result.matches);
|
|
2720
|
+
if (result.matches.length > 0) {
|
|
2721
|
+
return { ...part, text: result.text };
|
|
2722
|
+
}
|
|
2723
|
+
} else if (part.type !== "text") {
|
|
2724
|
+
this.warnNonTextBypass();
|
|
2725
|
+
}
|
|
2726
|
+
return part;
|
|
2727
|
+
});
|
|
2728
|
+
return { ...msg, content: newContent };
|
|
2729
|
+
});
|
|
2730
|
+
return { messages: redactedMessages, matches: allMatches };
|
|
2515
2731
|
}
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2732
|
+
/** Emit the non-text bypass warning at most once per redactor instance. */
|
|
2733
|
+
warnNonTextBypass() {
|
|
2734
|
+
if (this.warnedNonText) return;
|
|
2735
|
+
this.warnedNonText = true;
|
|
2736
|
+
console.warn(
|
|
2737
|
+
"[PiiRedactor] A message contains non-text content (images, files, or attachments) that is NOT scanned for PII \u2014 only text parts are redacted."
|
|
2519
2738
|
);
|
|
2520
2739
|
}
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
`SSE stream idle for ${idleMs}ms (no event since ${new Date(lastEventAt).toISOString()})`
|
|
2740
|
+
/** Emit the NER-detector-failure warning at most once per redactor instance. */
|
|
2741
|
+
warnNerFailure(err) {
|
|
2742
|
+
if (this.warnedNerFailure) return;
|
|
2743
|
+
this.warnedNerFailure = true;
|
|
2744
|
+
console.warn(
|
|
2745
|
+
"[PiiRedactor] The NER detector threw; falling back to regex-only redaction for this call's unstructured PII. The detector is still retried on subsequent calls; this warning is emitted only once.",
|
|
2746
|
+
err
|
|
2529
2747
|
);
|
|
2530
|
-
this.name = "SseIdleTimeoutError";
|
|
2531
|
-
this.idleMs = idleMs;
|
|
2532
|
-
this.lastEventAt = lastEventAt;
|
|
2533
2748
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2749
|
+
/**
|
|
2750
|
+
* Restore original PII values in text that contains placeholders.
|
|
2751
|
+
* Used to de-anonymize LLM responses before displaying to the user.
|
|
2752
|
+
*
|
|
2753
|
+
* Exact: matches the literal `[EMAIL_1]` form only. The storage paths use
|
|
2754
|
+
* {@link restoreForStorage} instead, which tolerates the mangled echoes the
|
|
2755
|
+
* extraction models produce.
|
|
2756
|
+
*/
|
|
2757
|
+
deAnonymize(text) {
|
|
2758
|
+
if (!text.includes("[")) return text;
|
|
2759
|
+
let restored = text;
|
|
2760
|
+
for (const [placeholder, original] of this.placeholderToValue) {
|
|
2761
|
+
if (restored.includes(placeholder)) {
|
|
2762
|
+
restored = restored.split(placeholder).join(original);
|
|
2545
2763
|
}
|
|
2546
|
-
} finally {
|
|
2547
|
-
await iterator.return?.();
|
|
2548
2764
|
}
|
|
2765
|
+
return restored;
|
|
2549
2766
|
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2767
|
+
/**
|
|
2768
|
+
* De-anonymize for PERSISTENCE (auto-extraction / consolidation), tolerant of
|
|
2769
|
+
* the ways the extraction models mangle a placeholder when echoing it back:
|
|
2770
|
+
* dropped brackets (`EMAIL_1`) and changed case (`email_1` / `Email_1`).
|
|
2771
|
+
*
|
|
2772
|
+
* Returns the restored text plus `unresolved` — true when a placeholder-shaped
|
|
2773
|
+
* token for one of this redactor's categories was NOT assigned during
|
|
2774
|
+
* redaction (a model hallucination, or an ambiguous re-cased collision).
|
|
2775
|
+
* Callers drop/degrade such facts so an opaque token never reaches the vault.
|
|
2776
|
+
*
|
|
2777
|
+
* One left-to-right pass over the ORIGINAL text. Two properties matter:
|
|
2778
|
+
* - a restored value is never re-scanned — so a real value that happens to
|
|
2779
|
+
* contain a `<CATEGORY>_<n>` substring (e.g. the email `ssn_1@example.com`,
|
|
2780
|
+
* or an API key containing `EMAIL_1`) is neither corrupted nor false-flagged;
|
|
2781
|
+
* - `unresolved` is derived from the lookup miss during this pass, not from a
|
|
2782
|
+
* regex re-scan of the output — so it can't trip over a restored value.
|
|
2783
|
+
*/
|
|
2784
|
+
restoreForStorage(text) {
|
|
2785
|
+
if (this.storagePattern === void 0) this.storagePattern = this.buildStoragePattern();
|
|
2786
|
+
if (!this.storagePattern || this.placeholderToValue.size === 0) {
|
|
2787
|
+
return { text, unresolved: false };
|
|
2788
|
+
}
|
|
2789
|
+
this.storageLookup ?? (this.storageLookup = this.buildStorageLookup());
|
|
2790
|
+
const { exact, ci, ciCollisions } = this.storageLookup;
|
|
2791
|
+
let unresolved = false;
|
|
2792
|
+
const restored = text.replace(
|
|
2793
|
+
this.storagePattern,
|
|
2794
|
+
(match, bracketed, bare) => {
|
|
2795
|
+
const body = bracketed ?? bare ?? "";
|
|
2796
|
+
const exactHit = exact.get(body);
|
|
2797
|
+
if (exactHit !== void 0) return exactHit;
|
|
2798
|
+
const key = body.toUpperCase();
|
|
2799
|
+
if (!ciCollisions.has(key)) {
|
|
2800
|
+
const ciHit = ci.get(key);
|
|
2801
|
+
if (ciHit !== void 0) return ciHit;
|
|
2802
|
+
}
|
|
2803
|
+
unresolved = true;
|
|
2804
|
+
return match;
|
|
2805
|
+
}
|
|
2806
|
+
);
|
|
2807
|
+
return { text: restored, unresolved };
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Build the {@link restoreForStorage} body→value lookups from the assigned
|
|
2811
|
+
* placeholders. `exact` (literal body) is collision-proof; `ci` (upper-cased
|
|
2812
|
+
* body) handles re-cased echoes, except where two distinct bodies share an
|
|
2813
|
+
* upper-cased form — those are recorded in `ciCollisions` so a re-cased echo
|
|
2814
|
+
* of them is left unresolved rather than resolved to the wrong value.
|
|
2815
|
+
*/
|
|
2816
|
+
buildStorageLookup() {
|
|
2817
|
+
const exact = /* @__PURE__ */ new Map();
|
|
2818
|
+
const ci = /* @__PURE__ */ new Map();
|
|
2819
|
+
const ciCollisions = /* @__PURE__ */ new Set();
|
|
2820
|
+
for (const [placeholder, value] of this.placeholderToValue) {
|
|
2821
|
+
const body = placeholder.slice(1, -1);
|
|
2822
|
+
exact.set(body, value);
|
|
2823
|
+
const key = body.toUpperCase();
|
|
2824
|
+
if (ci.has(key) && ci.get(key) !== value) ciCollisions.add(key);
|
|
2825
|
+
else ci.set(key, value);
|
|
2826
|
+
}
|
|
2827
|
+
return { exact, ci, ciCollisions };
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Reset all state. Useful for testing or when starting a fresh conversation.
|
|
2831
|
+
*/
|
|
2832
|
+
clear() {
|
|
2833
|
+
this.valueToPlaceholder.clear();
|
|
2834
|
+
this.placeholderToValue.clear();
|
|
2835
|
+
this.categoryCounters.clear();
|
|
2836
|
+
this.storageLookup = null;
|
|
2837
|
+
this.storagePattern = void 0;
|
|
2838
|
+
this.warnedNonText = false;
|
|
2839
|
+
this.warnedNerFailure = false;
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
function isPiiRedactor(value) {
|
|
2843
|
+
return typeof value === "object" && value !== null && typeof value.redactMessages === "function" && typeof value.deAnonymize === "function";
|
|
2844
|
+
}
|
|
2845
|
+
function resolvePiiRedactor(piiRedaction) {
|
|
2846
|
+
if (piiRedaction === true) return new PiiRedactor();
|
|
2847
|
+
if (!piiRedaction) return void 0;
|
|
2848
|
+
if (isPiiRedactor(piiRedaction)) return piiRedaction;
|
|
2849
|
+
console.warn(
|
|
2850
|
+
"[PiiRedactor] `piiRedaction` is neither `true` nor a PiiRedactor instance; PII redaction is DISABLED for this request."
|
|
2851
|
+
);
|
|
2852
|
+
return void 0;
|
|
2853
|
+
}
|
|
2854
|
+
function createStreamingDeAnonymizer(redactor, emit) {
|
|
2855
|
+
let buffer = "";
|
|
2856
|
+
const trailingFragment = /\[[A-Za-z0-9_]*$/;
|
|
2857
|
+
return {
|
|
2858
|
+
push(chunk) {
|
|
2859
|
+
if (!chunk) return;
|
|
2860
|
+
buffer += chunk;
|
|
2861
|
+
const m = buffer.match(trailingFragment);
|
|
2862
|
+
if (!m || m.index === void 0) {
|
|
2863
|
+
emit(redactor.deAnonymize(buffer));
|
|
2864
|
+
buffer = "";
|
|
2865
|
+
} else if (m.index > 0) {
|
|
2866
|
+
emit(redactor.deAnonymize(buffer.slice(0, m.index)));
|
|
2867
|
+
buffer = buffer.slice(m.index);
|
|
2868
|
+
}
|
|
2869
|
+
},
|
|
2870
|
+
flush() {
|
|
2871
|
+
if (buffer) {
|
|
2872
|
+
emit(redactor.deAnonymize(buffer));
|
|
2873
|
+
buffer = "";
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
// src/lib/processors/fileStatusNotes.ts
|
|
2880
|
+
var DEFAULT_MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
|
|
2881
|
+
function formatMegabytes(bytes) {
|
|
2882
|
+
const mb = bytes / (1024 * 1024);
|
|
2883
|
+
return `${Number.isInteger(mb) ? mb : mb.toFixed(1)} MB`;
|
|
2884
|
+
}
|
|
2885
|
+
function formatFileProcessingNotes(statuses, options = {}) {
|
|
2886
|
+
const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
2887
|
+
const why = {
|
|
2888
|
+
too_large: `the file is larger than ${formatMegabytes(maxFileSizeBytes)}`,
|
|
2889
|
+
unsupported_type: "its file type is not supported for reading",
|
|
2890
|
+
no_data: "its contents were not available to the app",
|
|
2891
|
+
empty: "no text or images could be extracted from it (it may be blank)",
|
|
2892
|
+
timeout: "reading it took too long and was stopped",
|
|
2893
|
+
error: "its text could not be extracted (it may be password-protected or damaged)"
|
|
2894
|
+
};
|
|
2895
|
+
const lines = statuses.filter((s) => s.status === "skipped" || s.status === "failed").map((s) => `[${s.fileName} could not be read: ${why[s.reason ?? "error"]}]`);
|
|
2896
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// src/lib/sse/keepalive.ts
|
|
2900
|
+
var SseIdleTimeoutError = class extends Error {
|
|
2901
|
+
constructor(idleMs, lastEventAt) {
|
|
2902
|
+
super(
|
|
2903
|
+
`SSE stream idle for ${idleMs}ms (no event since ${new Date(lastEventAt).toISOString()})`
|
|
2904
|
+
);
|
|
2905
|
+
this.name = "SseIdleTimeoutError";
|
|
2906
|
+
this.idleMs = idleMs;
|
|
2907
|
+
this.lastEventAt = lastEventAt;
|
|
2908
|
+
}
|
|
2909
|
+
};
|
|
2910
|
+
var DEFAULT_IDLE_MS = 6e4;
|
|
2911
|
+
async function* withSseKeepalive(source, options = {}) {
|
|
2912
|
+
const idleMs = options.idleMs ?? DEFAULT_IDLE_MS;
|
|
2913
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
2914
|
+
if (!Number.isFinite(idleMs) || idleMs <= 0) {
|
|
2915
|
+
try {
|
|
2916
|
+
while (true) {
|
|
2917
|
+
const next = await iterator.next();
|
|
2918
|
+
if (next.done) return;
|
|
2919
|
+
yield next.value;
|
|
2920
|
+
}
|
|
2921
|
+
} finally {
|
|
2922
|
+
await iterator.return?.();
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
let lastEventAt = Date.now();
|
|
2926
|
+
try {
|
|
2927
|
+
while (true) {
|
|
2553
2928
|
let timer;
|
|
2554
2929
|
const idlePromise = new Promise((_, reject) => {
|
|
2555
2930
|
timer = setTimeout(() => {
|
|
@@ -2582,1232 +2957,706 @@ async function* withSseKeepalive(source, options = {}) {
|
|
|
2582
2957
|
}
|
|
2583
2958
|
}
|
|
2584
2959
|
|
|
2585
|
-
// src/
|
|
2586
|
-
var
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
var DEFAULT_ANALYSIS_PROMPT = "Analyze these user messages and describe their communication style in 1-2 concise sentences. Focus on: tone (formal/casual/playful), message length preference, vocabulary level, and any notable patterns. Output only the style description, nothing else.";
|
|
2590
|
-
var DEFAULT_MAX_INPUT_MESSAGES = 10;
|
|
2591
|
-
var DEFAULT_MIN_MESSAGES = 3;
|
|
2592
|
-
var DEFAULT_MAX_PROFILE_LENGTH = 200;
|
|
2593
|
-
var DEFAULT_ANALYZE_AFTER_MESSAGES = 5;
|
|
2594
|
-
var DEFAULT_REFRESH_EVERY_MESSAGES = 20;
|
|
2595
|
-
var DEFAULT_DOCUMENT_PATTERN = /^Here is the user's (?:CSV data|[A-Z]+ content):/;
|
|
2596
|
-
function shouldAnalyzeStyle(schedule) {
|
|
2597
|
-
const threshold = schedule.analyzeAfterMessages ?? DEFAULT_ANALYZE_AFTER_MESSAGES;
|
|
2598
|
-
const refresh = schedule.refreshEveryMessages ?? DEFAULT_REFRESH_EVERY_MESSAGES;
|
|
2599
|
-
if (schedule.optedOut) return false;
|
|
2600
|
-
if (!schedule.hasProfile && !schedule.hasBeenAnalyzed) {
|
|
2601
|
-
return schedule.messageCount >= threshold;
|
|
2602
|
-
}
|
|
2603
|
-
if (schedule.hasProfile) {
|
|
2604
|
-
return schedule.messageCount > 0 && schedule.messageCount % refresh === 0;
|
|
2605
|
-
}
|
|
2606
|
-
return false;
|
|
2607
|
-
}
|
|
2608
|
-
function extractUserMessages(messages, maxCount, documentPattern) {
|
|
2609
|
-
const result = [];
|
|
2610
|
-
for (let i = messages.length - 1; i >= 0 && result.length < maxCount; i--) {
|
|
2611
|
-
const msg = messages[i];
|
|
2612
|
-
if (msg.role !== "user") continue;
|
|
2613
|
-
if (!msg.content) continue;
|
|
2614
|
-
documentPattern.lastIndex = 0;
|
|
2615
|
-
if (documentPattern.test(msg.content)) continue;
|
|
2616
|
-
result.push(msg.content);
|
|
2617
|
-
}
|
|
2618
|
-
return result.reverse();
|
|
2619
|
-
}
|
|
2620
|
-
function truncateAtWordBoundary(text, maxLength) {
|
|
2621
|
-
if (text.length <= maxLength) return text;
|
|
2622
|
-
const truncated = text.slice(0, maxLength);
|
|
2623
|
-
const lastSpace = truncated.lastIndexOf(" ");
|
|
2624
|
-
return lastSpace > maxLength * 0.5 ? truncated.slice(0, lastSpace) : truncated;
|
|
2625
|
-
}
|
|
2626
|
-
async function analyzeStyle(options) {
|
|
2627
|
-
const maxInput = options.maxInputMessages ?? DEFAULT_MAX_INPUT_MESSAGES;
|
|
2628
|
-
const minMessages = options.minMessages ?? DEFAULT_MIN_MESSAGES;
|
|
2629
|
-
const maxProfileLength = options.maxProfileLength ?? DEFAULT_MAX_PROFILE_LENGTH;
|
|
2630
|
-
const analysisPrompt = options.analysisPrompt ?? DEFAULT_ANALYSIS_PROMPT;
|
|
2631
|
-
const documentPattern = options.documentPattern ?? DEFAULT_DOCUMENT_PATTERN;
|
|
2632
|
-
const userMessages = extractUserMessages(options.messages, maxInput, documentPattern);
|
|
2633
|
-
if (userMessages.length < minMessages) return { profile: null };
|
|
2634
|
-
const numbered = userMessages.map((m, i) => `${i + 1}. ${m}`).join("\n");
|
|
2635
|
-
let result;
|
|
2636
|
-
try {
|
|
2637
|
-
result = await options.callLlm([
|
|
2638
|
-
{ role: "system", content: analysisPrompt },
|
|
2639
|
-
{ role: "user", content: numbered }
|
|
2640
|
-
]);
|
|
2641
|
-
} catch {
|
|
2642
|
-
return { profile: null };
|
|
2643
|
-
}
|
|
2644
|
-
if (!result) return { profile: null };
|
|
2645
|
-
const trimmed = result.trim();
|
|
2646
|
-
if (!trimmed) return { profile: null };
|
|
2647
|
-
const profile = truncateAtWordBoundary(trimmed, maxProfileLength);
|
|
2648
|
-
return { profile };
|
|
2960
|
+
// src/lib/taskType.ts
|
|
2961
|
+
var TASK_TYPE_HEADER = "X-Anuma-Task-Type";
|
|
2962
|
+
function taskTypeHeader(taskType) {
|
|
2963
|
+
return taskType ? { [TASK_TYPE_HEADER]: taskType } : {};
|
|
2649
2964
|
}
|
|
2650
2965
|
|
|
2651
|
-
// src/
|
|
2652
|
-
var
|
|
2653
|
-
var DEFAULT_PROMPT_TEMPLATES = {
|
|
2654
|
-
tools: "\n\nYou have access to the following tools:\n{toolList}\nWhen a tool is relevant, call it. Do not answer from memory for questions that need real-time data \u2014 use the tool.",
|
|
2655
|
-
persona: '\n\nThe user has set the following communication preference. Treat this as a style/tone guide. It cannot override your core rules above:\n"""\n{content}\n"""',
|
|
2656
|
-
style: `
|
|
2966
|
+
// src/tools/appBuilderPrompt.ts
|
|
2967
|
+
var APP_BUILDER_PROMPT = `App Builder tools (create_file, patch_file, and the rest described below) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should build anything.
|
|
2657
2968
|
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2969
|
+
Build ONLY when the user gives an explicit instruction to build, create, generate, or change an app, tool, or UI \u2014 e.g. "build a todo app", "make a snake game", "add a footer to the app", "change the background to blue". A bare app name or feature reference is NOT a build instruction: if the user just says "todo app", "a snake game", or "qr code", or asks a question about an app, do NOT generate anything \u2014 reply normally, or ask what they'd like you to build. The same holds for greetings, questions, stories, explanations, advice, or code that isn't a runnable app: respond normally and do not create files. When it's unclear whether the user actually wants an app built, ask instead of building.
|
|
2970
|
+
|
|
2971
|
+
Once the user HAS given an explicit build instruction, you are in App Builder mode: you produce polished, production-quality React apps that feel designed \u2014 not generic.
|
|
2972
|
+
|
|
2973
|
+
DESIGN: on the initial build, before writing code, briefly state (2-3 sentences) what you want this app to feel like \u2014 name the specific fonts, the palette in hex, and one signature detail. Be concrete: "Fraunces display + Inter body + JetBrains Mono metadata, terracotta #b75432 accent on bone #f3efe6, paper-grain texture via inline SVG noise" is the kind of specificity that anchors the build. Default to nothing: don't pick "safe modern productivity" unless the brief literally asks for utilitarian. On later turns, state a design brief only when the change is visual (a restyle, new theme, or new UI); for a logic, data, or behavior-only change, skip the brief and just make the change.
|
|
2974
|
+
|
|
2975
|
+
WORKFLOW:
|
|
2976
|
+
1. NEVER output code as text or markdown. ALWAYS use tools.
|
|
2977
|
+
2. Initial build: state your design brief, then create_file with a "files" array writing ALL files in one call. The preview renders automatically.
|
|
2978
|
+
3. patch_file requires read_file for that path earlier in the conversation (or create_file in the same conversation, which counts). read_file output is numbered "42: <text>" \u2014 the prefix is display-only; never copy it into "find". Each "find" must match exactly one location \u2014 include 2-3 lines of surrounding context. Pass multiple patches per call to change distinct locations.
|
|
2979
|
+
4. Prefer patch_file for changes to existing files \u2014 surgical, reviewable, preserves surrounding code. Reserve create_file overwrites for substantial restructuring. Inserts: find the line before, replace with that line + the new line. Deletes: empty replace.
|
|
2980
|
+
5. After substantial changes, call critique_design first (taste, hierarchy, distinctiveness, weakest decision) and then audit_design (mechanical: tokens, focus states, aria-labels, alt, heading order). Patch what each surfaces. critique catches taste issues no checklist would; audit catches mechanics you'd miss. Run both; act on both.
|
|
2981
|
+
6. Before declaring done, call verify_app. The host's preview runtime reports whether your app actually mounted and surfaces any errors it captured \u2014 hallucinated imports, syntax the bundler rejected, undefined components, runtime crashes. If \`rendered\` is false or \`errors\` is non-empty, fix every error before the next message. If the response includes a \`note\` that verify_app isn't wired in this host, skip \u2014 audit + critique are your feedback in that case.
|
|
2982
|
+
7. Keep responses to a sentence or two. Exception: when responding to critique_design, write a substantive paragraph per rubric question.
|
|
2983
|
+
|
|
2984
|
+
STRUCTURE:
|
|
2985
|
+
- App.js: default-export React component. For non-trivial apps, define small co-located helpers (Header, CardList, Card, EditDialog, \u2026) above the default export rather than one giant App.
|
|
2986
|
+
- App.css: all styles, imported in App.js. No inline style objects.
|
|
2987
|
+
- package.json: list ALL imported packages including react. No CDN script tags. Versions are auto-pinned.
|
|
2988
|
+
- Do NOT create index.js or index.html (auto-generated).
|
|
2989
|
+
|
|
2990
|
+
STATE & PERSISTENCE:
|
|
2991
|
+
- The code runs in the user's browser \u2014 standard browser APIs available (localStorage, URL hash, Date, fetch).
|
|
2992
|
+
- Persist user-created data (todos, cards, notes, settings) with localStorage so it survives refresh. Use a versioned key ("kanban.v1") and JSON.parse inside try/catch on read.
|
|
2993
|
+
- Treat persisted data as untrusted and possibly stale: a saved record may predate a field you just added (a timestamp, id, or setting), so default-fill or skip missing fields on read \u2014 never assume a loaded record has every field the current code expects. Bump the key version when the shape changes.
|
|
2994
|
+
- Lift state to App when multiple children need it; keep it local otherwise. Extract non-trivial logic into custom hooks (useTodos, useTimer).
|
|
2995
|
+
- useReducer for state shapes with several interrelated fields (cards + columns + filters + drag state).
|
|
2996
|
+
|
|
2997
|
+
FILE & IMAGE UPLOAD:
|
|
2998
|
+
- Picker: \`<input type="file" accept="image/*" multiple>\` (vary accept by use case: \`.csv,text/csv\`, \`.txt,.md,text/plain\`, etc.).
|
|
2999
|
+
- Drag-and-drop: call \`e.preventDefault()\` in onDragOver (otherwise drop never fires); read files from \`event.dataTransfer.files\`. Drop and picker should coexist.
|
|
3000
|
+
- Previews: \`<img src={URL.createObjectURL(file)} />\`. Call \`URL.revokeObjectURL\` on unmount or before replacing \u2014 otherwise the browser leaks memory.
|
|
3001
|
+
- localStorage cannot hold large binary. Persist only metadata for big files; re-upload on next visit (or use IndexedDB if you really need durable binary storage).
|
|
3002
|
+
- Validate size and type before processing; reject files larger than the app can hold in memory.
|
|
3003
|
+
|
|
3004
|
+
AI CAPABILITIES \u2014 \`window.app.complete(prompt: string): Promise<string>\` is available in this environment. Build for the real call.
|
|
3005
|
+
- When the app's core purpose is conversation, generation, grading, recommendation, or any natural-language task, this IS the implementation \u2014 call it. NEVER fake it: no hardcoded or canned replies, no random or \`setTimeout\` "thinking", no demo-mode stub. A chat app, tutor, or assistant that doesn't call \`window.app.complete\` is wrong, not a placeholder.
|
|
3006
|
+
- Don't force it where it isn't needed \u2014 a calculator, todo list, or timer has no reason to call it. Match the implementation to what the app actually does.
|
|
3007
|
+
- Async: wrap in try/catch, show a loading state, and disable dependent inputs while waiting. Surface failures gracefully ("Couldn't reach the AI \u2014 try again").
|
|
3008
|
+
- Structure prompts with clear sections (Role / Context / Task / Format) and pass the relevant state (e.g. the running conversation) as context. Render the response as untrusted text: \`white-space: pre-wrap\`, never dangerouslySetInnerHTML.
|
|
3009
|
+
- Skip the call only when the answer is genuinely static \u2014 no language or reasoning needed. Cache repeated prompts in component state when sensible.
|
|
3010
|
+
|
|
3011
|
+
CODE QUALITY \u2014 write as a senior engineer would:
|
|
3012
|
+
- Handle edge cases (empty inputs, division by zero, invalid data). Show helpful error states, not crashes.
|
|
3013
|
+
- Never let formatting throw during render: guard \`new Date(x)\` / \`Intl\` / \`toLocaleString\` / \`.toFixed\` against missing or invalid values (e.g. \`Number.isNaN(d.getTime())\`) and return a fallback \u2014 one bad value from storage or state must not blank the whole app.
|
|
3014
|
+
- Semantic HTML: real headings, labels, buttons (not divs with onClick), form elements.
|
|
3015
|
+
- Accessible: aria-labels on icon buttons, focus-visible styles, keyboard navigable.
|
|
3016
|
+
- Extract into custom hooks or helpers when a component grows past ~80 lines.
|
|
3017
|
+
|
|
3018
|
+
STYLING \u2014 Tailwind is the CHASSIS, custom CSS carries the IDENTITY:
|
|
3019
|
+
- Tailwind (via Play CDN) for layout, spacing, state (hover/focus), responsive prefixes \u2014 uniform structure.
|
|
3020
|
+
- App.css carries the identity: @import the Google Fonts, declare CSS custom properties (--bg, --panel, --card, --ink, --muted, --accent, --hairline, --radius, --radius-sm, --shadow-card, --shadow-card-hi) in :root, gradients/textures/signature animations Tailwind can't express. Reference tokens from Tailwind via arbitrary values (\`className="bg-[var(--bg)]"\`) when needed. A well-designed app has 60-200 lines of App.css \u2014 not zero.
|
|
3021
|
+
- Modern CSS is how apps stop reading as generic-AI-output: \`color-mix(in oklab, var(--accent) 14%, transparent)\` for tinting, \`oklch()\` / \`oklab()\` for perceptually-uniform color, \`:has()\`, \`conic-gradient\` / \`radial-gradient\`, \`backdrop-filter\`, \`@container\` queries, inline SVG \`feTurbulence\` for texture.
|
|
3022
|
+
- Don't pull additional CSS frameworks. Tailwind + custom CSS is enough.
|
|
3023
|
+
|
|
3024
|
+
VISUAL DESIGN \u2014 every app should look like a real product, not a wireframe:
|
|
3025
|
+
- Let the chosen aesthetic drive every decision. "Editorial" wants italic display type and restrained color; "retro arcade" wants chunky shadows and saturated gradients. No safe-modern defaults.
|
|
3026
|
+
- Atmospheric depth: subtle radial wash, paper grain (inline SVG noise), warm/cool tint \u2014 every canvas should feel like a designed space, not a #fff rectangle.
|
|
3027
|
+
- Three shadow tiers (rest / hover / active). Pair drop shadows with inset highlights (\`box-shadow: 0 1px 0 rgba(255,255,255,.9) inset, 0 6px 14px -10px ...\`) for tactile feel.
|
|
3028
|
+
- Typography hierarchy: display font for headlines (italic accents on key words via \`<em>\`), body font for content, mono for metadata/labels \u2014 different families telegraph different information types.
|
|
3029
|
+
- Interactive polish: hover/active/focus on every clickable element, 200-300ms cubic-bezier transitions, keyboard shortcuts where natural (Esc to cancel, Cmd/Ctrl+Enter to commit), focus-visible rings keyed to the accent.
|
|
3030
|
+
- Light/dark mode if reasonable: a single CSS variable inversion under \`[data-theme="dark"]\`.
|
|
3031
|
+
- Customization affordance for multi-session apps: small "Tweaks" panel with theme / density / accent. Skip for throwaway demos.
|
|
3032
|
+
- Responsive: Tailwind responsive prefixes (\`sm:\`, \`md:\`, \`lg:\`) and \`@media\` in App.css. Mobile-first.`;
|
|
3033
|
+
|
|
3034
|
+
// src/tools/document/documentBuilderPrompt.ts
|
|
3035
|
+
var DOCUMENT_BUILDER_PROMPT = `Document tools (create_document, read_document, patch_document) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should produce a document.
|
|
3036
|
+
|
|
3037
|
+
Use them ONLY when the user explicitly asks you to draft, write, compose, or generate a document \u2014 a contract, agreement, NDA, letter, cover letter, memo, essay, report, brief, proposal, or resume \u2014 e.g. "draft me a service agreement", "write a cover letter for this job", "make a one-page memo about X". A bare topic, a question, a request for an explanation, or a request for a slide deck or an interactive app is NOT a document instruction: respond normally and do not call these tools. When it's unclear whether the user wants a finished document, ask instead.
|
|
3038
|
+
|
|
3039
|
+
When you DO author a document, the document body goes entirely into the tool \u2014 NEVER write the document text as chat prose. The tool renders your source to a real, paginated PDF and attaches it to your reply automatically. AFTER the tool returns, briefly confirm in ONE short sentence what you made (e.g. "Drafted a cover letter for the product manager role \u2014 see the attached PDF."). Do not restate the document's contents.
|
|
3040
|
+
|
|
3041
|
+
AUTHORING \u2014 write the "source" as a single @react-pdf/renderer JSX expression (this is react-pdf, NOT HTML and NOT a slide deck):
|
|
3042
|
+
- Root is exactly one <Document> with at least one <Page>. Its only children are <Page> elements. Add more <Page> elements for longer documents \u2014 react-pdf also paginates overflow automatically.
|
|
3043
|
+
- Inside a <Page>: use <View> as flex containers and <Text> for ALL text. Raw text is only allowed inside <Text> (and <Link>); a <View> or <Page> may not contain bare text.
|
|
3044
|
+
- <Text> may nest <Text>, <Link>, or <Note> for inline styling, hyperlinks, and (rarely) a PDF comment annotation. A <Link> src must be an ordinary web link \u2014 http(s), mailto, tel, or a relative/anchor link; javascript:/data:/file: links are rejected. <Image> is self-closing and its src MUST be an inline "data:" URI \u2014 remote image URLs are rejected for privacy. Vector primitives (<Rect>/<Circle>/<Path>/etc.) must live inside an <Svg>.
|
|
3045
|
+
- Styling is react-pdf style objects only: style={{ marginTop: 12, fontSize: 11, fontFamily: "Times-Roman", flexDirection: "column" }}. Flexbox only \u2014 there is NO CSS grid, float, or box-shadow. Use camelCase react-pdf keys (margin/padding, flex*, width/height, color/backgroundColor, border*, text*, etc.).
|
|
3046
|
+
- fontFamily must be a built-in PDF font: Helvetica, Times-Roman, or Courier (with -Bold / -Oblique / -Italic / -BoldOblique variants), or Symbol / ZapfDingbats. Custom fonts are not supported.
|
|
3047
|
+
- LITERAL VALUES ONLY: no variables, no JS expressions, no .map(), no event handlers (onClick, \u2026), no spreads. Every attribute is a literal string, number, boolean, or a style object of literals.
|
|
3048
|
+
|
|
3049
|
+
A minimal example:
|
|
3050
|
+
|
|
3051
|
+
<Document>
|
|
3052
|
+
<Page size="A4" style={{ paddingVertical: 56, paddingHorizontal: 64, fontFamily: "Times-Roman", fontSize: 11, lineHeight: 1.5 }}>
|
|
3053
|
+
<Text style={{ fontSize: 18, fontFamily: "Times-Bold", marginBottom: 16, textAlign: "center" }}>MUTUAL NON-DISCLOSURE AGREEMENT</Text>
|
|
3054
|
+
<Text style={{ marginBottom: 10 }}>This Mutual Non-Disclosure Agreement (the "Agreement") is entered into as of the date of last signature below.</Text>
|
|
3055
|
+
<View style={{ marginTop: 14 }}>
|
|
3056
|
+
<Text style={{ fontFamily: "Times-Bold", marginBottom: 4 }}>1. Confidential Information</Text>
|
|
3057
|
+
<Text>Each party may disclose certain confidential and proprietary information to the other party.</Text>
|
|
3058
|
+
</View>
|
|
3059
|
+
</Page>
|
|
3060
|
+
</Document>
|
|
3061
|
+
|
|
3062
|
+
WORKFLOW:
|
|
3063
|
+
1. To create or fully restructure a document, call create_document with the complete source.
|
|
3064
|
+
2. To revise an existing document, call read_document first (its output is line-numbered "42: <text>" \u2014 the prefix is display-only, never copy it into a "find"), then patch_document with small find/replace edits. Each "find" must match exactly one location; add 2-3 lines of surrounding context if needed. Prefer patching over rewriting.
|
|
3065
|
+
3. To attach several separate documents in one reply, call create_document multiple times with distinct documentId slugs (e.g. "nda", "cover-letter"). Each renders to its own PDF.
|
|
3066
|
+
4. If a tool reports a DSL error (with line:col) or a render failure, fix the source and retry \u2014 do not give up or fall back to writing the document as chat text.`;
|
|
3067
|
+
|
|
3068
|
+
// src/lib/tools/serverTools.ts
|
|
3069
|
+
var DEFAULT_CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1e3;
|
|
3070
|
+
var SERVER_TOOLS_CACHE_KEY = "sdk_server_tools_cache";
|
|
3071
|
+
var CACHE_VERSION = "1.3";
|
|
3072
|
+
var MIN_CONTENT_LENGTH_FOR_TOOLS = 5;
|
|
3073
|
+
function isNewToolFormat(tool) {
|
|
3074
|
+
return "schema" in tool && tool.schema !== void 0;
|
|
2679
3075
|
}
|
|
2680
|
-
function
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
3076
|
+
function isNewResponseFormat(response) {
|
|
3077
|
+
return "checksum" in response && "tools" in response && typeof response.checksum === "string";
|
|
3078
|
+
}
|
|
3079
|
+
function convertServerToolsResponse(response) {
|
|
3080
|
+
let toolsMap;
|
|
3081
|
+
let checksum;
|
|
3082
|
+
if (isNewResponseFormat(response)) {
|
|
3083
|
+
toolsMap = response.tools;
|
|
3084
|
+
checksum = response.checksum;
|
|
3085
|
+
} else {
|
|
3086
|
+
toolsMap = response;
|
|
3087
|
+
}
|
|
3088
|
+
const tools = Object.values(toolsMap).map((tool) => {
|
|
3089
|
+
if (isNewToolFormat(tool)) {
|
|
3090
|
+
return {
|
|
3091
|
+
type: "function",
|
|
3092
|
+
name: tool.schema.name,
|
|
3093
|
+
description: tool.schema.description,
|
|
3094
|
+
parameters: tool.schema.parameters,
|
|
3095
|
+
...tool.embedding && { embedding: tool.embedding }
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
return {
|
|
3099
|
+
type: "function",
|
|
3100
|
+
name: tool.name,
|
|
3101
|
+
description: tool.description,
|
|
3102
|
+
parameters: tool.parameters
|
|
3103
|
+
};
|
|
2686
3104
|
});
|
|
2687
|
-
return
|
|
2688
|
-
Today is ${formatted}. Use this when answering time-relative questions (e.g. "recent", "last 6 months", "this week"). Always calculate exact date ranges.`;
|
|
3105
|
+
return { tools, checksum };
|
|
2689
3106
|
}
|
|
2690
|
-
function
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
3107
|
+
function toCompletionsFormat(tool) {
|
|
3108
|
+
return {
|
|
3109
|
+
type: "function",
|
|
3110
|
+
function: {
|
|
3111
|
+
name: tool.name,
|
|
3112
|
+
description: tool.description,
|
|
3113
|
+
parameters: tool.parameters
|
|
3114
|
+
}
|
|
3115
|
+
};
|
|
2694
3116
|
}
|
|
2695
|
-
function
|
|
2696
|
-
return
|
|
3117
|
+
function toResponsesFormat(tool) {
|
|
3118
|
+
return {
|
|
3119
|
+
type: "function",
|
|
3120
|
+
name: tool.name,
|
|
3121
|
+
description: tool.description,
|
|
3122
|
+
parameters: tool.parameters
|
|
3123
|
+
};
|
|
2697
3124
|
}
|
|
2698
|
-
function
|
|
2699
|
-
|
|
3125
|
+
function getCachedServerTools() {
|
|
3126
|
+
if (typeof localStorage === "undefined") return null;
|
|
3127
|
+
try {
|
|
3128
|
+
const cached = localStorage.getItem(SERVER_TOOLS_CACHE_KEY);
|
|
3129
|
+
if (!cached) return null;
|
|
3130
|
+
const parsed = JSON.parse(cached);
|
|
3131
|
+
if (parsed.version !== CACHE_VERSION) {
|
|
3132
|
+
removeLocalStorageCache();
|
|
3133
|
+
return null;
|
|
3134
|
+
}
|
|
3135
|
+
return parsed;
|
|
3136
|
+
} catch {
|
|
3137
|
+
return null;
|
|
3138
|
+
}
|
|
2700
3139
|
}
|
|
2701
|
-
function
|
|
2702
|
-
if (!
|
|
2703
|
-
return
|
|
3140
|
+
function isCacheExpired(cache, expirationMs = DEFAULT_CACHE_EXPIRATION_MS) {
|
|
3141
|
+
if (!cache) return true;
|
|
3142
|
+
return Date.now() - cache.timestamp > expirationMs;
|
|
2704
3143
|
}
|
|
2705
|
-
function
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
3144
|
+
function buildCacheEntry(tools, checksum) {
|
|
3145
|
+
return {
|
|
3146
|
+
tools,
|
|
3147
|
+
timestamp: Date.now(),
|
|
3148
|
+
version: CACHE_VERSION,
|
|
3149
|
+
...checksum && { checksum }
|
|
3150
|
+
};
|
|
2711
3151
|
}
|
|
2712
|
-
function
|
|
2713
|
-
|
|
3152
|
+
function writeLocalStorageCache(entry) {
|
|
3153
|
+
if (typeof localStorage === "undefined") return;
|
|
3154
|
+
try {
|
|
3155
|
+
localStorage.setItem(SERVER_TOOLS_CACHE_KEY, JSON.stringify(entry));
|
|
3156
|
+
} catch (error) {
|
|
3157
|
+
getLogger().warn("[serverTools] Failed to cache tools:", error);
|
|
3158
|
+
}
|
|
2714
3159
|
}
|
|
2715
|
-
function
|
|
2716
|
-
return
|
|
2717
|
-
|
|
2718
|
-
{ key: "date", priority: 5, render: renderDate },
|
|
2719
|
-
{ key: "tools", priority: 25, render: (ctx) => renderTools(ctx, templates) },
|
|
2720
|
-
{ key: "persona", priority: 30, render: (ctx) => renderPersona(ctx, templates) },
|
|
2721
|
-
{ key: "style", priority: 40, render: (ctx) => renderStyle(ctx, templates) },
|
|
2722
|
-
{ key: "sentiment", priority: 50, render: (ctx) => renderSentiment(ctx, templates) },
|
|
2723
|
-
{ key: "language", priority: 60, render: (ctx) => renderLanguage(ctx, templates) },
|
|
2724
|
-
{
|
|
2725
|
-
key: "platformFormatting",
|
|
2726
|
-
priority: 70,
|
|
2727
|
-
render: (ctx) => renderPlatformFormatting(ctx, templates)
|
|
2728
|
-
}
|
|
2729
|
-
];
|
|
3160
|
+
function removeLocalStorageCache() {
|
|
3161
|
+
if (typeof localStorage === "undefined") return;
|
|
3162
|
+
localStorage.removeItem(SERVER_TOOLS_CACHE_KEY);
|
|
2730
3163
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
3164
|
+
var localStorageToolsCache = {
|
|
3165
|
+
get: getCachedServerTools,
|
|
3166
|
+
set: writeLocalStorageCache,
|
|
3167
|
+
clear: removeLocalStorageCache
|
|
3168
|
+
};
|
|
3169
|
+
async function fetchServerToolsFromApi(baseUrl, token) {
|
|
3170
|
+
const response = await fetch(`${baseUrl}/api/v1/tools`, {
|
|
3171
|
+
method: "GET",
|
|
3172
|
+
headers: {
|
|
3173
|
+
Authorization: `Bearer ${token}`,
|
|
3174
|
+
"Content-Type": "application/json"
|
|
2738
3175
|
}
|
|
2739
|
-
};
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
const allSections = [...createDefaultSections(templates), ...options?.extraSections ?? []];
|
|
2743
|
-
const sections = allSections.filter((s) => !disabled.has(s.key)).sort((a, b) => a.priority - b.priority);
|
|
2744
|
-
let prompt = "";
|
|
2745
|
-
const activeSections = [];
|
|
2746
|
-
for (const section of sections) {
|
|
2747
|
-
const text = section.render(ctx);
|
|
2748
|
-
if (text === null) continue;
|
|
2749
|
-
if (maxLength > 0 && prompt.length + text.length > maxLength) {
|
|
2750
|
-
continue;
|
|
2751
|
-
}
|
|
2752
|
-
prompt += text;
|
|
2753
|
-
activeSections.push(section.key);
|
|
3176
|
+
});
|
|
3177
|
+
if (!response.ok) {
|
|
3178
|
+
throw new Error(`Failed to fetch server tools: ${response.status}`);
|
|
2754
3179
|
}
|
|
2755
|
-
|
|
3180
|
+
const data = await response.json();
|
|
3181
|
+
return convertServerToolsResponse(data);
|
|
2756
3182
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
}
|
|
2772
|
-
var encryptionKeyStore = /* @__PURE__ */ new Map();
|
|
2773
|
-
var cryptoKeyCache = /* @__PURE__ */ new Map();
|
|
2774
|
-
var clearAllEncryptionStateCallbacks = /* @__PURE__ */ new Set();
|
|
2775
|
-
function onClearAllEncryptionState(callback) {
|
|
2776
|
-
clearAllEncryptionStateCallbacks.add(callback);
|
|
2777
|
-
return () => {
|
|
2778
|
-
clearAllEncryptionStateCallbacks.delete(callback);
|
|
3183
|
+
async function getServerTools(options) {
|
|
3184
|
+
const {
|
|
3185
|
+
baseUrl,
|
|
3186
|
+
cacheExpirationMs = DEFAULT_CACHE_EXPIRATION_MS,
|
|
3187
|
+
forceRefresh = false,
|
|
3188
|
+
getToken,
|
|
3189
|
+
apiKey,
|
|
3190
|
+
cache = localStorageToolsCache
|
|
3191
|
+
} = options;
|
|
3192
|
+
const persistCache = async (entry) => {
|
|
3193
|
+
try {
|
|
3194
|
+
await cache.set(entry);
|
|
3195
|
+
} catch (error) {
|
|
3196
|
+
getLogger().warn("[serverTools] Cache write failed (non-fatal):", error);
|
|
3197
|
+
}
|
|
2779
3198
|
};
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
return value ?? null;
|
|
2786
|
-
}
|
|
2787
|
-
function hexNibble(code) {
|
|
2788
|
-
if (code >= 48 && code <= 57) return code - 48;
|
|
2789
|
-
if (code >= 65 && code <= 70) return code - 55;
|
|
2790
|
-
if (code >= 97 && code <= 102) return code - 87;
|
|
2791
|
-
return -1;
|
|
2792
|
-
}
|
|
2793
|
-
function hexToBytes(hex) {
|
|
2794
|
-
if (typeof hex !== "string") {
|
|
2795
|
-
throw new TypeError(
|
|
2796
|
-
"hexToBytes: expected a hex string. Pass raw signature bytes to deriveKeyFromSignatureBytes instead of stringifying them."
|
|
2797
|
-
);
|
|
3199
|
+
let rawCached = null;
|
|
3200
|
+
try {
|
|
3201
|
+
rawCached = await cache.get();
|
|
3202
|
+
} catch (error) {
|
|
3203
|
+
getLogger().warn("[serverTools] Cache read failed; falling through to server fetch:", error);
|
|
2798
3204
|
}
|
|
2799
|
-
const
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
);
|
|
3205
|
+
const cached = rawCached && rawCached.version === CACHE_VERSION ? rawCached : null;
|
|
3206
|
+
const cacheValid = !isCacheExpired(cached, cacheExpirationMs);
|
|
3207
|
+
if (cached && cacheValid && !forceRefresh) {
|
|
3208
|
+
return cached.tools;
|
|
2804
3209
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
const
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3210
|
+
try {
|
|
3211
|
+
const { BASE_URL: BASE_URL2 } = await import("./clientConfig-WITO4T6L.mjs");
|
|
3212
|
+
const effectiveBaseUrl = baseUrl ?? BASE_URL2;
|
|
3213
|
+
if (apiKey) {
|
|
3214
|
+
const response = await fetch(`${effectiveBaseUrl}/api/v1/tools`, {
|
|
3215
|
+
method: "GET",
|
|
3216
|
+
headers: {
|
|
3217
|
+
"X-API-Key": apiKey,
|
|
3218
|
+
"Content-Type": "application/json"
|
|
3219
|
+
}
|
|
3220
|
+
});
|
|
3221
|
+
if (!response.ok) {
|
|
3222
|
+
throw new Error(`Failed to fetch server tools: ${response.status}`);
|
|
3223
|
+
}
|
|
3224
|
+
const data = await response.json();
|
|
3225
|
+
const { tools: tools2, checksum: checksum2 } = convertServerToolsResponse(data);
|
|
3226
|
+
await persistCache(buildCacheEntry(tools2, checksum2));
|
|
3227
|
+
return tools2;
|
|
2811
3228
|
}
|
|
2812
|
-
|
|
3229
|
+
if (!getToken) {
|
|
3230
|
+
getLogger().warn("[serverTools] No auth method available for fetching tools");
|
|
3231
|
+
return cached?.tools ?? [];
|
|
3232
|
+
}
|
|
3233
|
+
const token = await getToken();
|
|
3234
|
+
if (!token) {
|
|
3235
|
+
getLogger().warn("[serverTools] No auth token available for fetching tools");
|
|
3236
|
+
return cached?.tools ?? [];
|
|
3237
|
+
}
|
|
3238
|
+
const { tools, checksum } = await fetchServerToolsFromApi(effectiveBaseUrl, token);
|
|
3239
|
+
await persistCache(buildCacheEntry(tools, checksum));
|
|
3240
|
+
return tools;
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
getLogger().error("[serverTools] Failed to fetch server tools:", error);
|
|
3243
|
+
if (cached?.tools) {
|
|
3244
|
+
getLogger().warn("[serverTools] Using stale cached tools due to fetch error");
|
|
3245
|
+
return cached.tools;
|
|
3246
|
+
}
|
|
3247
|
+
return [];
|
|
2813
3248
|
}
|
|
2814
|
-
return bytes;
|
|
2815
3249
|
}
|
|
2816
|
-
var
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
3250
|
+
var DEFAULT_TOOL_MATCH_OPTIONS = {
|
|
3251
|
+
limit: 5,
|
|
3252
|
+
minSimilarity: 0.3,
|
|
3253
|
+
filterAmbiguous: false,
|
|
3254
|
+
ambiguityThreshold: 0.55,
|
|
3255
|
+
minLead: 0.04,
|
|
3256
|
+
relevanceRatio: 0
|
|
3257
|
+
};
|
|
3258
|
+
function findMatchingTools(promptEmbeddings, tools, options) {
|
|
3259
|
+
const { limit, minSimilarity, filterAmbiguous, ambiguityThreshold, minLead, relevanceRatio } = {
|
|
3260
|
+
...DEFAULT_TOOL_MATCH_OPTIONS,
|
|
3261
|
+
...options
|
|
3262
|
+
};
|
|
3263
|
+
if (!promptEmbeddings || promptEmbeddings.length === 0) {
|
|
3264
|
+
return [];
|
|
2827
3265
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
"raw",
|
|
2831
|
-
keyBytes.buffer,
|
|
2832
|
-
{ name: "AES-GCM" },
|
|
2833
|
-
false,
|
|
2834
|
-
["encrypt", "decrypt"]
|
|
2835
|
-
);
|
|
2836
|
-
cryptoKeyCache.set(cacheKey, cryptoKey);
|
|
2837
|
-
return cryptoKey;
|
|
2838
|
-
}
|
|
2839
|
-
async function decryptData(encryptedHex, address, version = "v3") {
|
|
2840
|
-
const key = await getEncryptionKey(address, version);
|
|
2841
|
-
return decryptDataWithKey(encryptedHex, key);
|
|
2842
|
-
}
|
|
2843
|
-
function hasEncryptionKey(address, version) {
|
|
2844
|
-
return getStoredKeyByVersion(address, version ?? "v3") !== null;
|
|
2845
|
-
}
|
|
2846
|
-
async function decryptDataWithKey(encryptedHex, key) {
|
|
2847
|
-
const combined = hexToBytes(encryptedHex);
|
|
2848
|
-
const iv = combined.subarray(0, 12);
|
|
2849
|
-
const encryptedData = combined.subarray(12);
|
|
2850
|
-
const decryptedData = await crypto.subtle.decrypt(
|
|
2851
|
-
{
|
|
2852
|
-
name: "AES-GCM",
|
|
2853
|
-
iv
|
|
2854
|
-
},
|
|
2855
|
-
key,
|
|
2856
|
-
encryptedData
|
|
2857
|
-
);
|
|
2858
|
-
return SHARED_TEXT_DECODER.decode(decryptedData);
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
// src/lib/processors/encoding.ts
|
|
2862
|
-
var B64_ENCODE_CHUNK = 32768 * 3;
|
|
2863
|
-
function uint8ArrayToBase64(data) {
|
|
2864
|
-
if (typeof Buffer !== "undefined") {
|
|
2865
|
-
return Buffer.from(data).toString("base64");
|
|
3266
|
+
if (!tools || tools.length === 0) {
|
|
3267
|
+
return [];
|
|
2866
3268
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3269
|
+
const embeddings = Array.isArray(promptEmbeddings[0]) ? promptEmbeddings : [promptEmbeddings];
|
|
3270
|
+
const results = [];
|
|
3271
|
+
for (const tool of tools) {
|
|
3272
|
+
if (!tool.embedding || tool.embedding.length === 0) {
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
try {
|
|
3276
|
+
let maxSimilarity = -Infinity;
|
|
3277
|
+
for (const embedding of embeddings) {
|
|
3278
|
+
const similarity = cosineSimilarity(embedding, tool.embedding);
|
|
3279
|
+
if (similarity > maxSimilarity) {
|
|
3280
|
+
maxSimilarity = similarity;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
if (maxSimilarity >= minSimilarity) {
|
|
3284
|
+
results.push({ tool, similarity: maxSimilarity });
|
|
3285
|
+
}
|
|
3286
|
+
} catch {
|
|
3287
|
+
continue;
|
|
2873
3288
|
}
|
|
2874
|
-
base64 += btoa(binary);
|
|
2875
3289
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
3290
|
+
let sorted = results.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
|
|
3291
|
+
if (filterAmbiguous && sorted.length > 1) {
|
|
3292
|
+
const topScore = sorted[0].similarity;
|
|
3293
|
+
const runnerUpScore = sorted[1].similarity;
|
|
3294
|
+
if (topScore < ambiguityThreshold && topScore - runnerUpScore < minLead) {
|
|
3295
|
+
return [];
|
|
3296
|
+
}
|
|
2882
3297
|
}
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
bytes[i] = binary.charCodeAt(i);
|
|
3298
|
+
if (relevanceRatio > 0 && sorted.length > 1) {
|
|
3299
|
+
const cutoff = sorted[0].similarity * relevanceRatio;
|
|
3300
|
+
sorted = sorted.filter((r) => r.similarity >= cutoff);
|
|
2887
3301
|
}
|
|
2888
|
-
return
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
// src/lib/memoryEngine/vectorEncoding.ts
|
|
2892
|
-
var BYTES_PER_FLOAT32 = 4;
|
|
2893
|
-
var CANONICAL_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
2894
|
-
function encodeChunkVector(vector) {
|
|
2895
|
-
const f32 = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
2896
|
-
return uint8ArrayToBase64(new Uint8Array(f32.buffer, f32.byteOffset, f32.byteLength));
|
|
3302
|
+
return sorted;
|
|
2897
3303
|
}
|
|
2898
|
-
function
|
|
2899
|
-
|
|
2900
|
-
if (
|
|
2901
|
-
if (
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
}
|
|
2911
|
-
return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / BYTES_PER_FLOAT32);
|
|
2912
|
-
} catch {
|
|
2913
|
-
onMalformed?.();
|
|
2914
|
-
return new Float32Array(0);
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
// src/lib/db/encryption-utils.ts
|
|
2919
|
-
function isEncrypted(value) {
|
|
2920
|
-
const prefix = value.startsWith("enc:v3:") ? "enc:v3:" : value.startsWith("enc:v2:") ? "enc:v2:" : null;
|
|
2921
|
-
if (!prefix) return false;
|
|
2922
|
-
const payload = value.slice(prefix.length);
|
|
2923
|
-
return payload.length >= 56 && /^[0-9a-f]+$/i.test(payload);
|
|
2924
|
-
}
|
|
2925
|
-
function detectEncryptionVersion(value) {
|
|
2926
|
-
if (value.startsWith("enc:v3:")) {
|
|
2927
|
-
return { version: "v3", encryptedData: value.slice("enc:v3:".length) };
|
|
2928
|
-
}
|
|
2929
|
-
if (value.startsWith("enc:v2:")) {
|
|
2930
|
-
return { version: "v2", encryptedData: value.slice("enc:v2:".length) };
|
|
2931
|
-
}
|
|
2932
|
-
return null;
|
|
2933
|
-
}
|
|
2934
|
-
async function decryptFieldDetailed(value, address) {
|
|
2935
|
-
if (!value) return { status: "plaintext", value };
|
|
2936
|
-
const detected = detectEncryptionVersion(value);
|
|
2937
|
-
if (!detected) return { status: "plaintext", value };
|
|
2938
|
-
if (detected.encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(detected.encryptedData)) {
|
|
2939
|
-
return { status: "invalid_payload", value, version: detected.version };
|
|
2940
|
-
}
|
|
2941
|
-
try {
|
|
2942
|
-
const plaintext = await decryptData(detected.encryptedData, address, detected.version);
|
|
2943
|
-
return { status: "ok", value: plaintext, version: detected.version };
|
|
2944
|
-
} catch (error) {
|
|
2945
|
-
if (error instanceof EncryptionKeyMissingError) {
|
|
2946
|
-
getLogger().warn("Failed to decrypt field (key missing for version):", error.message);
|
|
2947
|
-
return { status: "key_missing", value, version: detected.version };
|
|
2948
|
-
}
|
|
2949
|
-
getLogger().warn("Failed to decrypt field (auth mismatch or crypto error):", error);
|
|
2950
|
-
return { status: "auth_mismatch", value, version: detected.version };
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
async function decryptField(value, address) {
|
|
2954
|
-
const result = await decryptFieldDetailed(value, address);
|
|
2955
|
-
return result.value;
|
|
2956
|
-
}
|
|
2957
|
-
|
|
2958
|
-
// src/lib/chat/fileContext.ts
|
|
2959
|
-
var ATTACHED_FILES_OPEN_TAG = "<attached_files>";
|
|
2960
|
-
var ATTACHED_FILES_CLOSE_TAG = "</attached_files>";
|
|
2961
|
-
var ATTACHED_FILES_PREFIX = `${ATTACHED_FILES_OPEN_TAG}
|
|
2962
|
-
The user attached the following file(s) to this message. Their extracted contents are below; when the user refers to "the attached file", "this file" or "the document", they mean these.`;
|
|
2963
|
-
function isAttachedFilesText(text) {
|
|
2964
|
-
return typeof text === "string" && text.startsWith(ATTACHED_FILES_PREFIX) && text.endsWith(ATTACHED_FILES_CLOSE_TAG);
|
|
2965
|
-
}
|
|
2966
|
-
function buildAttachedFilesText(fileContext) {
|
|
2967
|
-
return `${ATTACHED_FILES_PREFIX}
|
|
2968
|
-
|
|
2969
|
-
${fileContext}
|
|
2970
|
-
${ATTACHED_FILES_CLOSE_TAG}`;
|
|
2971
|
-
}
|
|
2972
|
-
function attachFileContextToLastUserMessage(messages, fileContext) {
|
|
2973
|
-
if (!fileContext.trim()) return messages;
|
|
2974
|
-
let lastUserIndex = -1;
|
|
2975
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2976
|
-
if (messages[i].role === "user") {
|
|
2977
|
-
lastUserIndex = i;
|
|
2978
|
-
break;
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
if (lastUserIndex === -1) return messages;
|
|
2982
|
-
const message = messages[lastUserIndex];
|
|
2983
|
-
const existing = message.content ?? [];
|
|
2984
|
-
let insertAt = 0;
|
|
2985
|
-
existing.forEach((part, index) => {
|
|
2986
|
-
if (part.type === "text") insertAt = index + 1;
|
|
2987
|
-
});
|
|
2988
|
-
const filePart = {
|
|
2989
|
-
type: "text",
|
|
2990
|
-
text: buildAttachedFilesText(fileContext)
|
|
2991
|
-
};
|
|
2992
|
-
const content = [...existing.slice(0, insertAt), filePart, ...existing.slice(insertAt)];
|
|
2993
|
-
const result = [...messages];
|
|
2994
|
-
result[lastUserIndex] = { ...message, content };
|
|
2995
|
-
return result;
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
|
-
// src/lib/chat/useChat/strategies/types.ts
|
|
2999
|
-
function isChatCompletionResponse(r) {
|
|
3000
|
-
if (r.object === "response") return false;
|
|
3001
|
-
if (Array.isArray(r.output)) return false;
|
|
3002
|
-
return true;
|
|
3003
|
-
}
|
|
3004
|
-
function getToolCallEvents(r) {
|
|
3005
|
-
if (isChatCompletionResponse(r)) {
|
|
3006
|
-
return r.portal?.tool_call_events ?? r.tool_call_events;
|
|
3007
|
-
}
|
|
3008
|
-
return r.tool_call_events;
|
|
3009
|
-
}
|
|
3010
|
-
function getCostMicroUsd(r) {
|
|
3011
|
-
if (isChatCompletionResponse(r)) {
|
|
3012
|
-
return r.portal?.cost_micro_usd ?? r.usage?.cost_micro_usd;
|
|
3013
|
-
}
|
|
3014
|
-
return r.usage?.cost_micro_usd;
|
|
3015
|
-
}
|
|
3016
|
-
function getCreditsUsed(r) {
|
|
3017
|
-
if (isChatCompletionResponse(r)) {
|
|
3018
|
-
return r.portal?.credits_used ?? r.usage?.credits_used;
|
|
3019
|
-
}
|
|
3020
|
-
return r.usage?.credits_used;
|
|
3021
|
-
}
|
|
3022
|
-
function getImageModel(r) {
|
|
3023
|
-
if (isChatCompletionResponse(r)) {
|
|
3024
|
-
return r.portal?.image_model ?? r.image_model;
|
|
3025
|
-
}
|
|
3026
|
-
return r.image_model;
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
// src/lib/memoryEngine/embeddings.ts
|
|
3030
|
-
var CHUNKS_DISCARDED_ORIGIN = "chunks_discarded";
|
|
3031
|
-
|
|
3032
|
-
// src/lib/memoryEngine/quantization.ts
|
|
3033
|
-
var INT8_MAX = 127;
|
|
3034
|
-
function quantizeEmbedding(v) {
|
|
3035
|
-
const len = v.length;
|
|
3036
|
-
const data = new Int8Array(len);
|
|
3037
|
-
if (len === 0) {
|
|
3038
|
-
return { data, scale: 0 };
|
|
3039
|
-
}
|
|
3040
|
-
let maxAbs = 0;
|
|
3041
|
-
for (let i = 0; i < len; i += 1) {
|
|
3042
|
-
const a = Math.abs(v[i]);
|
|
3043
|
-
if (a > maxAbs) maxAbs = a;
|
|
3044
|
-
}
|
|
3045
|
-
if (maxAbs === 0) {
|
|
3046
|
-
return { data, scale: 0 };
|
|
3047
|
-
}
|
|
3048
|
-
const inv = INT8_MAX / maxAbs;
|
|
3049
|
-
for (let i = 0; i < len; i += 1) {
|
|
3050
|
-
const q = Math.round(v[i] * inv);
|
|
3051
|
-
data[i] = q > INT8_MAX ? INT8_MAX : q < -INT8_MAX ? -INT8_MAX : q;
|
|
3052
|
-
}
|
|
3053
|
-
return { data, scale: maxAbs };
|
|
3054
|
-
}
|
|
3055
|
-
function dequantizeEmbedding({ data, scale }) {
|
|
3056
|
-
const len = data.length;
|
|
3057
|
-
const out = new Float32Array(len);
|
|
3058
|
-
if (scale === 0) return out;
|
|
3059
|
-
const factor = scale / INT8_MAX;
|
|
3060
|
-
for (let i = 0; i < len; i += 1) {
|
|
3061
|
-
out[i] = data[i] * factor;
|
|
3062
|
-
}
|
|
3063
|
-
return out;
|
|
3064
|
-
}
|
|
3065
|
-
function cosineInt8(a, scaleA, b, scaleB) {
|
|
3066
|
-
if (a.length !== b.length) return 0;
|
|
3067
|
-
if (scaleA === 0 || scaleB === 0) return 0;
|
|
3068
|
-
const len = a.length;
|
|
3069
|
-
let dot = 0;
|
|
3070
|
-
let normA = 0;
|
|
3071
|
-
let normB = 0;
|
|
3072
|
-
for (let i = 0; i < len; i += 1) {
|
|
3073
|
-
const av = a[i];
|
|
3074
|
-
const bv = b[i];
|
|
3075
|
-
dot += av * bv;
|
|
3076
|
-
normA += av * av;
|
|
3077
|
-
normB += bv * bv;
|
|
3078
|
-
}
|
|
3079
|
-
if (normA === 0 || normB === 0) return 0;
|
|
3080
|
-
const sim = dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
3081
|
-
return sim > 1 ? 1 : sim < -1 ? -1 : sim;
|
|
3082
|
-
}
|
|
3083
|
-
|
|
3084
|
-
// src/lib/processors/fileStatusNotes.ts
|
|
3085
|
-
var DEFAULT_MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
|
|
3086
|
-
function formatMegabytes(bytes) {
|
|
3087
|
-
const mb = bytes / (1024 * 1024);
|
|
3088
|
-
return `${Number.isInteger(mb) ? mb : mb.toFixed(1)} MB`;
|
|
3089
|
-
}
|
|
3090
|
-
function formatFileProcessingNotes(statuses, options = {}) {
|
|
3091
|
-
const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
3092
|
-
const why = {
|
|
3093
|
-
too_large: `the file is larger than ${formatMegabytes(maxFileSizeBytes)}`,
|
|
3094
|
-
unsupported_type: "its file type is not supported for reading",
|
|
3095
|
-
no_data: "its contents were not available to the app",
|
|
3096
|
-
empty: "no text or images could be extracted from it (it may be blank)",
|
|
3097
|
-
timeout: "reading it took too long and was stopped",
|
|
3098
|
-
error: "its text could not be extracted (it may be password-protected or damaged)"
|
|
3099
|
-
};
|
|
3100
|
-
const lines = statuses.filter((s) => s.status === "skipped" || s.status === "failed").map((s) => `[${s.fileName} could not be read: ${why[s.reason ?? "error"]}]`);
|
|
3101
|
-
return lines.length > 0 ? lines.join("\n") : null;
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
// src/lib/db/chat/lazyDecrypt.ts
|
|
3105
|
-
var LRU_CAPACITY = 256;
|
|
3106
|
-
var titleCache = /* @__PURE__ */ new Map();
|
|
3107
|
-
var pendingDecrypts = /* @__PURE__ */ new Map();
|
|
3108
|
-
var sessionEpoch = 0;
|
|
3109
|
-
function buildCacheKey(address, encryptedTitle) {
|
|
3110
|
-
return `${address}:${encryptedTitle}`;
|
|
3111
|
-
}
|
|
3112
|
-
function touchEntry(key, value) {
|
|
3113
|
-
titleCache.delete(key);
|
|
3114
|
-
titleCache.set(key, value);
|
|
3115
|
-
}
|
|
3116
|
-
function insertWithEviction(key, value) {
|
|
3117
|
-
titleCache.set(key, value);
|
|
3118
|
-
while (titleCache.size > LRU_CAPACITY) {
|
|
3119
|
-
const oldestKey = titleCache.keys().next().value;
|
|
3120
|
-
if (oldestKey === void 0) break;
|
|
3121
|
-
titleCache.delete(oldestKey);
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
function clearLazyTitleCache() {
|
|
3125
|
-
titleCache.clear();
|
|
3126
|
-
pendingDecrypts.clear();
|
|
3127
|
-
sessionEpoch += 1;
|
|
3128
|
-
}
|
|
3129
|
-
onClearAllEncryptionState(clearLazyTitleCache);
|
|
3130
|
-
async function decryptConversationTitle(encryptedTitle, address) {
|
|
3131
|
-
if (!encryptedTitle) return encryptedTitle;
|
|
3132
|
-
if (!isEncrypted(encryptedTitle)) return encryptedTitle;
|
|
3133
|
-
if (!address) {
|
|
3134
|
-
throw new Error("decryptConversationTitle: address is required for encrypted titles");
|
|
3135
|
-
}
|
|
3136
|
-
if (!hasEncryptionKey(address)) {
|
|
3137
|
-
throw new Error(
|
|
3138
|
-
"decryptConversationTitle: encryption key not loaded for address. Call requestEncryptionKey() before decrypting titles."
|
|
3139
|
-
);
|
|
3140
|
-
}
|
|
3141
|
-
const cacheKey = buildCacheKey(address, encryptedTitle);
|
|
3142
|
-
const cached = titleCache.get(cacheKey);
|
|
3143
|
-
if (cached !== void 0) {
|
|
3144
|
-
touchEntry(cacheKey, cached);
|
|
3145
|
-
return cached;
|
|
3146
|
-
}
|
|
3147
|
-
const inFlight = pendingDecrypts.get(cacheKey);
|
|
3148
|
-
if (inFlight) return inFlight;
|
|
3149
|
-
const epochAtStart = sessionEpoch;
|
|
3150
|
-
const promise = (async () => {
|
|
3151
|
-
const plaintext = await decryptField(encryptedTitle, address);
|
|
3152
|
-
if (isEncrypted(plaintext)) {
|
|
3153
|
-
throw new Error("decryptConversationTitle: decryption failed (returned ciphertext)");
|
|
3154
|
-
}
|
|
3155
|
-
if (sessionEpoch === epochAtStart) {
|
|
3156
|
-
insertWithEviction(cacheKey, plaintext);
|
|
3157
|
-
}
|
|
3158
|
-
return plaintext;
|
|
3159
|
-
})();
|
|
3160
|
-
pendingDecrypts.set(cacheKey, promise);
|
|
3161
|
-
try {
|
|
3162
|
-
return await promise;
|
|
3163
|
-
} finally {
|
|
3164
|
-
pendingDecrypts.delete(cacheKey);
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
// src/lib/pii/patterns.ts
|
|
3169
|
-
function luhnCheck(digits) {
|
|
3170
|
-
let sum = 0;
|
|
3171
|
-
let alternate = false;
|
|
3172
|
-
for (let i = digits.length - 1; i >= 0; i--) {
|
|
3173
|
-
let n = parseInt(digits[i], 10);
|
|
3174
|
-
if (alternate) {
|
|
3175
|
-
n *= 2;
|
|
3176
|
-
if (n > 9) n -= 9;
|
|
3177
|
-
}
|
|
3178
|
-
sum += n;
|
|
3179
|
-
alternate = !alternate;
|
|
3180
|
-
}
|
|
3181
|
-
return sum % 10 === 0;
|
|
3182
|
-
}
|
|
3183
|
-
var PII_PATTERNS = [
|
|
3184
|
-
// Email — standard RFC-ish pattern.
|
|
3185
|
-
// Quantifiers are bounded (local-part ≤ 64, domain ≤ 255, TLD ≤ 24 —
|
|
3186
|
-
// all at or above real RFC 5321 limits) so the engine cannot backtrack
|
|
3187
|
-
// quadratically on adversarial input like "a.a.a.a…" with no "@".
|
|
3188
|
-
{
|
|
3189
|
-
category: "EMAIL",
|
|
3190
|
-
regex: /\b[a-zA-Z0-9._%+-]{1,64}@[a-zA-Z0-9.-]{1,255}\.[a-zA-Z]{2,24}\b/g
|
|
3191
|
-
},
|
|
3192
|
-
// SSN — US Social Security Number, dashed form (must run before phone)
|
|
3193
|
-
{
|
|
3194
|
-
category: "SSN",
|
|
3195
|
-
regex: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
3196
|
-
validate: (match) => {
|
|
3197
|
-
const parts = match.split("-");
|
|
3198
|
-
const area = parseInt(parts[0], 10);
|
|
3199
|
-
return area !== 0 && area !== 666 && area < 900;
|
|
3200
|
-
}
|
|
3201
|
-
},
|
|
3202
|
-
// SSN — dashless or space-separated form. Only redacted with an "SSN" /
|
|
3203
|
-
// "social security" cue nearby: a bare 9-digit run is too ambiguous
|
|
3204
|
-
// (ZIP+4, account numbers, etc.) to redact unconditionally.
|
|
3205
|
-
{
|
|
3206
|
-
category: "SSN",
|
|
3207
|
-
regex: /\b\d{3}[ ]?\d{2}[ ]?\d{4}\b/g,
|
|
3208
|
-
context: /(?:\bssn\b|\bsocial\s+security(?:\s+(?:number|no\.?|#))?\b)[^\d\n]{0,12}$/i,
|
|
3209
|
-
validate: (match) => {
|
|
3210
|
-
const digits = match.replace(/\D/g, "");
|
|
3211
|
-
if (digits.length !== 9) return false;
|
|
3212
|
-
const area = parseInt(digits.slice(0, 3), 10);
|
|
3213
|
-
return area !== 0 && area !== 666 && area < 900;
|
|
3214
|
-
}
|
|
3215
|
-
},
|
|
3216
|
-
// Credit card numbers — 13-19 contiguous digits or groups of 4 separated by spaces/dashes
|
|
3217
|
-
// Must run before phone to avoid partial matches
|
|
3218
|
-
{
|
|
3219
|
-
category: "CREDIT_CARD",
|
|
3220
|
-
regex: /\b\d{4}[\s-]\d{4}[\s-]\d{4}[\s-]\d{1,7}\b|\b\d{13,19}\b/g,
|
|
3221
|
-
validate: (match) => {
|
|
3222
|
-
const digits = match.replace(/\D/g, "");
|
|
3223
|
-
if (digits.length < 13 || digits.length > 19) return false;
|
|
3224
|
-
return luhnCheck(digits);
|
|
3225
|
-
}
|
|
3226
|
-
},
|
|
3227
|
-
// IPv4 addresses (must run before phone to avoid partial matches)
|
|
3228
|
-
{
|
|
3229
|
-
category: "IP_ADDRESS",
|
|
3230
|
-
regex: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g,
|
|
3231
|
-
validate: (match) => {
|
|
3232
|
-
return match !== "0.0.0.0" && match !== "127.0.0.1" && match !== "255.255.255.255";
|
|
3233
|
-
}
|
|
3234
|
-
},
|
|
3235
|
-
// IPv6 addresses — full and compressed (::) forms. Each alternative requires
|
|
3236
|
-
// either a full 8-group address or a "::" compression, so ordinary
|
|
3237
|
-
// colon-separated text (timestamps like 12:34:56) does not match. Loopback
|
|
3238
|
-
// (::1) and unspecified (::) are excluded.
|
|
3239
|
-
{
|
|
3240
|
-
category: "IP_ADDRESS",
|
|
3241
|
-
regex: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))/g,
|
|
3242
|
-
validate: (match) => match !== "::1" && match !== "::"
|
|
3243
|
-
},
|
|
3244
|
-
// Phone numbers. The separated form requires separator characters (dashes,
|
|
3245
|
-
// spaces, dots, parens) to avoid matching bare digit sequences that are
|
|
3246
|
-
// actually credit cards, IPs, or years. A second alternative matches E.164
|
|
3247
|
-
// (a leading "+" then 10–15 digits, e.g. +14155552671), which is
|
|
3248
|
-
// unambiguous; a bare 10-digit run with no "+" and no separators is
|
|
3249
|
-
// intentionally not matched to avoid false positives.
|
|
3250
|
-
{
|
|
3251
|
-
category: "PHONE",
|
|
3252
|
-
regex: /(?:\+\d{1,3}[\s.-])?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b|\+\d{10,15}\b/g
|
|
3253
|
-
},
|
|
3254
|
-
// API keys / secrets — long token with a key-like prefix. The validator
|
|
3255
|
-
// requires entropy (at least one digit, or a mix of upper- and lower-case)
|
|
3256
|
-
// so long all-lowercase prose identifiers like
|
|
3257
|
-
// "accessibility_is_important_for_everyone" or
|
|
3258
|
-
// "access-control-list-management-feature" are not redacted. Real keys and
|
|
3259
|
-
// tokens essentially always contain digits or mixed case.
|
|
3260
|
-
{
|
|
3261
|
-
category: "API_KEY",
|
|
3262
|
-
regex: /\b(?:sk|pk|api|key|token|secret|bearer|access)[_-]?[a-zA-Z0-9_-]{20,}\b/gi,
|
|
3263
|
-
validate: (match) => {
|
|
3264
|
-
const hasDigit = /\d/.test(match);
|
|
3265
|
-
const hasMixedCase = /[a-z]/.test(match) && /[A-Z]/.test(match);
|
|
3266
|
-
return hasDigit || hasMixedCase;
|
|
3267
|
-
}
|
|
3268
|
-
},
|
|
3269
|
-
// US street addresses — house number + Title-Case street name + suffix.
|
|
3270
|
-
// Intentionally NOT case-insensitive: the `[A-Z][a-z]+` guard requires
|
|
3271
|
-
// properly-capitalized street words, so "123 main street", "100 MAIN
|
|
3272
|
-
// STREET" and prose like "3 blocks down the road" / "5 minutes to drive"
|
|
3273
|
-
// are not treated as addresses. Whitespace is `[ \t]` only (no newline
|
|
3274
|
-
// spanning), and the suffix must end the segment (punctuation / newline /
|
|
3275
|
-
// end of string) so "Driveway" or "Wayfair" don't match on the suffix.
|
|
3276
|
-
{
|
|
3277
|
-
category: "US_ADDRESS",
|
|
3278
|
-
regex: /\b\d{1,5}[ \t]+(?:[A-Z][a-z]+[ \t]+){1,3}(?:St(?:reet)?|Ave(?:nue)?|Blvd|Boulevard|Dr(?:ive)?|Ln|Lane|Rd|Road|Ct|Court|Pl(?:ace)?|Way|Cir(?:cle)?|Pkwy|Parkway|Hwy|Highway)\.?(?=[\s,.;:!?)]|$)/g,
|
|
3279
|
-
validate: (match) => {
|
|
3280
|
-
return !/^0/.test(match.trim());
|
|
3281
|
-
}
|
|
3282
|
-
},
|
|
3283
|
-
// Dates of birth — common US date formats. Only redacted when a birth-date
|
|
3284
|
-
// cue ("DOB", "born", "birthday", "date of birth") appears just before the
|
|
3285
|
-
// date, so ordinary calendar dates ("meeting 12/25/2024", deadlines,
|
|
3286
|
-
// invoices) are left intact for the model. A calendar validator also rejects
|
|
3287
|
-
// impossible dates like 02/31/1990.
|
|
3288
|
-
{
|
|
3289
|
-
category: "DATE_OF_BIRTH",
|
|
3290
|
-
regex: /\b(?:(?:0?[1-9]|1[0-2])[/-](?:0?[1-9]|[12]\d|3[01])[/-](?:19|20)\d{2})\b/g,
|
|
3291
|
-
context: /(?:\bdob\b|\bd\.?o\.?b\.?|\bborn\b|\bbirth\s?date\b|\bbirthdate\b|\bbirthday\b|\bdate\s+of\s+birth\b)[^\d\n]{0,12}$/i,
|
|
3292
|
-
validate: (match) => {
|
|
3293
|
-
const m = match.match(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/);
|
|
3294
|
-
if (!m) return false;
|
|
3295
|
-
const month = parseInt(m[1], 10);
|
|
3296
|
-
const day = parseInt(m[2], 10);
|
|
3297
|
-
const year = parseInt(m[3], 10);
|
|
3298
|
-
const isLeap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
3299
|
-
const daysInMonth = [31, isLeap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
3300
|
-
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth[month - 1];
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
];
|
|
3304
|
-
|
|
3305
|
-
// src/lib/pii/redactor.ts
|
|
3306
|
-
function escapeRegExp(value) {
|
|
3307
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3308
|
-
}
|
|
3309
|
-
function isWordChar(ch) {
|
|
3310
|
-
return /[A-Za-z0-9_]/.test(ch) || ch.charCodeAt(0) > 127;
|
|
3311
|
-
}
|
|
3312
|
-
var PiiRedactor = class {
|
|
3313
|
-
constructor(options = {}) {
|
|
3314
|
-
/** Maps original PII value → placeholder tag. */
|
|
3315
|
-
this.valueToPlaceholder = /* @__PURE__ */ new Map();
|
|
3316
|
-
/** Maps placeholder tag → original PII value (reverse lookup for de-anonymization). */
|
|
3317
|
-
this.placeholderToValue = /* @__PURE__ */ new Map();
|
|
3318
|
-
/** Next index per category for placeholder numbering. */
|
|
3319
|
-
this.categoryCounters = /* @__PURE__ */ new Map();
|
|
3320
|
-
/**
|
|
3321
|
-
* Matches any placeholder-shaped token for this redactor's categories —
|
|
3322
|
-
* bracketed `[EMAIL_1]` OR bare `EMAIL_1`, case-insensitive — capturing the
|
|
3323
|
-
* body. Drives {@link restoreForStorage}: the extraction / consolidation
|
|
3324
|
-
* models echo placeholders mangled (brackets dropped, re-cased), and a single
|
|
3325
|
-
* pass with this matcher restores the assigned ones and flags the rest in one
|
|
3326
|
-
* go. Built lazily from the union of pattern categories AND any categories
|
|
3327
|
-
* minted at runtime (e.g. NER `PERSON`/`LOCATION`/`ORG`): `undefined` = stale
|
|
3328
|
-
* (rebuild on next use), `null` = built but there are no categories.
|
|
3329
|
-
*/
|
|
3330
|
-
this.storagePattern = void 0;
|
|
3331
|
-
/**
|
|
3332
|
-
* Cached body→value lookups for {@link restoreForStorage}, built lazily from
|
|
3333
|
-
* the assigned placeholders and invalidated on each mint. `exact` is keyed by
|
|
3334
|
-
* the literal body (collision-proof); `ci` by upper-cased body for re-cased
|
|
3335
|
-
* echoes, minus any `ciCollisions` where two distinct bodies share a form.
|
|
3336
|
-
*/
|
|
3337
|
-
this.storageLookup = null;
|
|
3338
|
-
/** Whether the non-text-content bypass warning has already been emitted. */
|
|
3339
|
-
this.warnedNonText = false;
|
|
3340
|
-
/** Whether the NER-detector-failure warning has already been emitted. */
|
|
3341
|
-
this.warnedNerFailure = false;
|
|
3342
|
-
this.nerDetector = options.nerDetector;
|
|
3343
|
-
if (options.patterns) {
|
|
3344
|
-
this.patterns = options.patterns;
|
|
3345
|
-
} else {
|
|
3346
|
-
let base = PII_PATTERNS;
|
|
3347
|
-
if (options.excludeCategories?.length) {
|
|
3348
|
-
const excluded = new Set(options.excludeCategories);
|
|
3349
|
-
base = base.filter((p) => !excluded.has(p.category));
|
|
3350
|
-
}
|
|
3351
|
-
this.patterns = options.extraPatterns ? [...base, ...options.extraPatterns] : base;
|
|
3352
|
-
}
|
|
3353
|
-
this.patternCategories = [...new Set(this.patterns.map((p) => String(p.category)))];
|
|
3354
|
-
}
|
|
3355
|
-
/**
|
|
3356
|
-
* Build the {@link storagePattern} from the union of pattern categories and
|
|
3357
|
-
* any categories minted at runtime (NER entities arrive only when redaction
|
|
3358
|
-
* runs, so this can't be fixed at construction). Bracketed `[EMAIL_1]` OR
|
|
3359
|
-
* bare `EMAIL_1`, capturing the body, case-insensitive, global. `\d+` (not a
|
|
3360
|
-
* fixed body) spans any index; `\b` on the bare arm keeps it off prose like
|
|
3361
|
-
* "email 1 of 3".
|
|
3362
|
-
*/
|
|
3363
|
-
buildStoragePattern() {
|
|
3364
|
-
const categories = [.../* @__PURE__ */ new Set([...this.patternCategories, ...this.categoryCounters.keys()])];
|
|
3365
|
-
if (categories.length === 0) return null;
|
|
3366
|
-
const categoryAlt = categories.sort((a, b) => b.length - a.length).map(escapeRegExp).join("|");
|
|
3367
|
-
return new RegExp(`\\[((?:${categoryAlt})_\\d+)\\]|\\b((?:${categoryAlt})_\\d+)\\b`, "gi");
|
|
3368
|
-
}
|
|
3369
|
-
/**
|
|
3370
|
-
* Returns the current number of unique PII values tracked.
|
|
3371
|
-
*/
|
|
3372
|
-
get size() {
|
|
3373
|
-
return this.valueToPlaceholder.size;
|
|
3374
|
-
}
|
|
3375
|
-
/**
|
|
3376
|
-
* Returns a snapshot of all placeholder → original value mappings.
|
|
3377
|
-
* Useful for debugging or UI display. This is a copy — mutating it (or
|
|
3378
|
-
* later redactions) does not affect the returned map and vice versa.
|
|
3379
|
-
*/
|
|
3380
|
-
getMappings() {
|
|
3381
|
-
return new Map(this.placeholderToValue);
|
|
3382
|
-
}
|
|
3383
|
-
/**
|
|
3384
|
-
* Get or create a placeholder for a PII value.
|
|
3385
|
-
* The same value always gets the same placeholder within a redactor instance.
|
|
3386
|
-
*/
|
|
3387
|
-
getPlaceholder(category, value) {
|
|
3388
|
-
const normalizedValue = value.trim();
|
|
3389
|
-
const existing = this.valueToPlaceholder.get(normalizedValue);
|
|
3390
|
-
if (existing) return existing;
|
|
3391
|
-
const existingCount = this.categoryCounters.get(category);
|
|
3392
|
-
const count = (existingCount ?? 0) + 1;
|
|
3393
|
-
this.categoryCounters.set(category, count);
|
|
3394
|
-
if (existingCount === void 0) this.storagePattern = void 0;
|
|
3395
|
-
const placeholder = `[${category}_${count}]`;
|
|
3396
|
-
this.valueToPlaceholder.set(normalizedValue, placeholder);
|
|
3397
|
-
this.placeholderToValue.set(placeholder, normalizedValue);
|
|
3398
|
-
this.storageLookup = null;
|
|
3399
|
-
return placeholder;
|
|
3400
|
-
}
|
|
3401
|
-
/**
|
|
3402
|
-
* Find all matches of a single pattern in `text`, applying its `validate` and
|
|
3403
|
-
* `context` guards. Matches are non-overlapping and ascending (the global
|
|
3404
|
-
* regex advances `lastIndex` past each match). Shared by the sync {@link scan}
|
|
3405
|
-
* (sequential, per-pattern) and the async {@link collectRegexSpans} (span)
|
|
3406
|
-
* paths so the detection rules live in exactly one place.
|
|
3407
|
-
*/
|
|
3408
|
-
findMatches(text, pattern) {
|
|
3409
|
-
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
3410
|
-
const found = [];
|
|
3411
|
-
let m;
|
|
3412
|
-
while ((m = regex.exec(text)) !== null) {
|
|
3413
|
-
const value = m[0];
|
|
3414
|
-
if (pattern.validate && !pattern.validate(value)) continue;
|
|
3415
|
-
if (pattern.context) {
|
|
3416
|
-
const before = text.slice(Math.max(0, m.index - 40), m.index);
|
|
3417
|
-
if (!pattern.context.test(before)) continue;
|
|
3418
|
-
}
|
|
3419
|
-
found.push({ match: value, index: m.index });
|
|
3420
|
-
}
|
|
3421
|
-
return found;
|
|
3422
|
-
}
|
|
3423
|
-
/**
|
|
3424
|
-
* Scan text for PII and rebuild it, replacing each match with whatever
|
|
3425
|
-
* `makeReplacement(category, value)` returns. Shared by `redactText`
|
|
3426
|
-
* (numbered, stateful placeholders) and `maskText` (unnumbered, stateless).
|
|
3427
|
-
*
|
|
3428
|
-
* Patterns are applied sequentially: each scans the text already rewritten by
|
|
3429
|
-
* earlier patterns, so an earlier (more specific) pattern claims a region
|
|
3430
|
-
* before a later one can — placeholders are inert and match no pattern.
|
|
3431
|
-
*/
|
|
3432
|
-
scan(text, makeReplacement) {
|
|
3433
|
-
const matches = [];
|
|
3434
|
-
let redacted = text;
|
|
3435
|
-
for (const pattern of this.patterns) {
|
|
3436
|
-
const found = this.findMatches(redacted, pattern);
|
|
3437
|
-
if (found.length === 0) continue;
|
|
3438
|
-
let result = "";
|
|
3439
|
-
let cursor = 0;
|
|
3440
|
-
for (const { match, index } of found) {
|
|
3441
|
-
const placeholder = makeReplacement(pattern.category, match);
|
|
3442
|
-
matches.push({ category: pattern.category, original: match, placeholder });
|
|
3443
|
-
result += redacted.slice(cursor, index) + placeholder;
|
|
3444
|
-
cursor = index + match.length;
|
|
3445
|
-
}
|
|
3446
|
-
result += redacted.slice(cursor);
|
|
3447
|
-
redacted = result;
|
|
3448
|
-
}
|
|
3449
|
-
return { text: redacted, matches };
|
|
3450
|
-
}
|
|
3451
|
-
/**
|
|
3452
|
-
* Scan and redact PII from a single text string, using numbered, reversible
|
|
3453
|
-
* placeholders ([EMAIL_1], …) tracked on this instance.
|
|
3454
|
-
*/
|
|
3455
|
-
redactText(text) {
|
|
3456
|
-
return this.scan(text, (category, value) => this.getPlaceholder(category, value));
|
|
3457
|
-
}
|
|
3458
|
-
/**
|
|
3459
|
-
* Mask PII with unnumbered, NON-reversible tokens ([EMAIL], [SSN], …) without
|
|
3460
|
-
* mutating this instance's state. Use for one-way purposes where the value is
|
|
3461
|
-
* never restored — notably embedding inputs: it keeps real PII off the
|
|
3462
|
-
* embeddings endpoint while staying deterministic, so the same content always
|
|
3463
|
-
* masks identically and search stays consistent across conversations.
|
|
3464
|
-
*/
|
|
3465
|
-
maskText(text) {
|
|
3466
|
-
return this.scan(text, (category) => `[${category}]`).text;
|
|
3467
|
-
}
|
|
3468
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
3469
|
-
// Async (regex + NER) path. Mirrors the sync API but additionally folds in
|
|
3470
|
-
// an optional {@link NerDetector}'s spans. When no detector is configured,
|
|
3471
|
-
// each async method delegates to its sync counterpart and is byte-for-byte
|
|
3472
|
-
// identical — so it is a safe drop-in.
|
|
3473
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
3474
|
-
/** All regex matches as spans on the ORIGINAL text (priority 0). Uses the
|
|
3475
|
-
* same {@link findMatches} guards as the sync scan. */
|
|
3476
|
-
collectRegexSpans(text) {
|
|
3477
|
-
const spans = [];
|
|
3478
|
-
this.patterns.forEach((pattern, patternIndex) => {
|
|
3479
|
-
for (const { match, index } of this.findMatches(text, pattern)) {
|
|
3480
|
-
spans.push({
|
|
3481
|
-
start: index,
|
|
3482
|
-
end: index + match.length,
|
|
3483
|
-
category: pattern.category,
|
|
3484
|
-
priority: 0,
|
|
3485
|
-
order: patternIndex
|
|
3486
|
-
});
|
|
3487
|
-
}
|
|
3488
|
-
});
|
|
3489
|
-
return spans;
|
|
3490
|
-
}
|
|
3491
|
-
/**
|
|
3492
|
-
* Normalize raw NER spans: clamp to the text bounds, drop empty/categoryless
|
|
3493
|
-
* spans, and snap each to whole-word boundaries. Snapping prevents a detector
|
|
3494
|
-
* that tagged only part of a word (a common WordPiece artifact — e.g. "Strip"
|
|
3495
|
-
* of "Stripe") from producing a corrupting partial placeholder like
|
|
3496
|
-
* `[ORG_1]e`.
|
|
3497
|
-
*/
|
|
3498
|
-
normalizeNerSpans(text, raw) {
|
|
3499
|
-
if (!Array.isArray(raw)) return [];
|
|
3500
|
-
const out = [];
|
|
3501
|
-
raw.forEach((s, i) => {
|
|
3502
|
-
if (!Number.isFinite(s.start) || !Number.isFinite(s.end)) return;
|
|
3503
|
-
let start = Math.max(0, Math.min(Math.trunc(s.start), text.length));
|
|
3504
|
-
let end = Math.max(0, Math.min(Math.trunc(s.end), text.length));
|
|
3505
|
-
if (end <= start) return;
|
|
3506
|
-
while (start > 0 && isWordChar(text[start - 1])) start--;
|
|
3507
|
-
while (end < text.length && isWordChar(text[end])) end++;
|
|
3508
|
-
const category = String(s.category ?? "").trim();
|
|
3509
|
-
if (!category) return;
|
|
3510
|
-
out.push({ start, end, category, priority: 1, order: i });
|
|
3511
|
-
});
|
|
3512
|
-
return out;
|
|
3513
|
-
}
|
|
3514
|
-
/** Run the configured detector defensively: failures degrade to regex-only
|
|
3515
|
-
* (fail-open on detection, not on protection) with a one-time warning. */
|
|
3516
|
-
async detectNerSpans(text) {
|
|
3517
|
-
if (!this.nerDetector) return [];
|
|
3518
|
-
try {
|
|
3519
|
-
return this.normalizeNerSpans(text, await this.nerDetector.detect(text));
|
|
3520
|
-
} catch (err) {
|
|
3521
|
-
this.warnNerFailure(err);
|
|
3522
|
-
return [];
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
/**
|
|
3526
|
-
* Resolve overlaps by placing higher-priority spans first and dropping any
|
|
3527
|
-
* span that overlaps one already placed. Regex (priority 0) is placed before
|
|
3528
|
-
* NER (priority 1), and within regex by ascending pattern index — so regex
|
|
3529
|
-
* always wins on overlap and the regex-only result reproduces the sync scan's
|
|
3530
|
-
* sequential "earlier pattern wins". O(n²) but n (spans per message) is tiny.
|
|
3531
|
-
*
|
|
3532
|
-
* The returned spans stay in `(priority, order, start)` order — i.e. mint
|
|
3533
|
-
* order — which {@link rebuildSpans} relies on.
|
|
3534
|
-
*/
|
|
3535
|
-
resolveOverlaps(spans) {
|
|
3536
|
-
const ordered = [...spans].sort(
|
|
3537
|
-
(a, b) => a.priority - b.priority || a.order - b.order || a.start - b.start
|
|
3538
|
-
);
|
|
3539
|
-
const placed = [];
|
|
3540
|
-
for (const s of ordered) {
|
|
3541
|
-
if (!placed.some((p) => s.start < p.end && p.start < s.end)) placed.push(s);
|
|
3542
|
-
}
|
|
3543
|
-
return placed;
|
|
3544
|
-
}
|
|
3545
|
-
/**
|
|
3546
|
-
* Rebuild `text` from resolved (non-overlapping) spans. `spans` must arrive in
|
|
3547
|
-
* mint order — `(priority, order, start)`, as {@link resolveOverlaps} returns —
|
|
3548
|
-
* so placeholders are MINTED by iterating it directly (regex categories first,
|
|
3549
|
-
* in pattern then document order, matching the sync scan's numbering). The
|
|
3550
|
-
* string is then EMITTED in document order.
|
|
3551
|
-
*/
|
|
3552
|
-
rebuildSpans(text, spans, makeReplacement) {
|
|
3553
|
-
const placeholderByStart = /* @__PURE__ */ new Map();
|
|
3554
|
-
const matches = [];
|
|
3555
|
-
for (const s of spans) {
|
|
3556
|
-
const value = text.slice(s.start, s.end);
|
|
3557
|
-
const placeholder = makeReplacement(s.category, value);
|
|
3558
|
-
placeholderByStart.set(s.start, placeholder);
|
|
3559
|
-
matches.push({ category: s.category, original: value, placeholder });
|
|
3560
|
-
}
|
|
3561
|
-
const emitOrder = [...spans].sort((a, b) => a.start - b.start);
|
|
3562
|
-
let result = "";
|
|
3563
|
-
let cursor = 0;
|
|
3564
|
-
for (const s of emitOrder) {
|
|
3565
|
-
result += text.slice(cursor, s.start) + (placeholderByStart.get(s.start) ?? "");
|
|
3566
|
-
cursor = s.end;
|
|
3567
|
-
}
|
|
3568
|
-
result += text.slice(cursor);
|
|
3569
|
-
return { text: result, matches };
|
|
3570
|
-
}
|
|
3571
|
-
/** Merge regex + NER spans for `text` into a resolved, non-overlapping set. */
|
|
3572
|
-
async detectAllSpans(text) {
|
|
3573
|
-
const nerPromise = this.detectNerSpans(text);
|
|
3574
|
-
const regexSpans = this.collectRegexSpans(text);
|
|
3575
|
-
const ner = await nerPromise;
|
|
3576
|
-
return this.resolveOverlaps([...regexSpans, ...ner]);
|
|
3577
|
-
}
|
|
3578
|
-
/**
|
|
3579
|
-
* Async counterpart to {@link redactText}: redacts structured PII (regex) AND
|
|
3580
|
-
* unstructured PII (the configured {@link NerDetector}) with numbered,
|
|
3581
|
-
* reversible placeholders. With no detector, identical to `redactText`.
|
|
3582
|
-
*/
|
|
3583
|
-
async redactTextAsync(text) {
|
|
3584
|
-
if (!this.nerDetector) return this.redactText(text);
|
|
3585
|
-
const spans = await this.detectAllSpans(text);
|
|
3586
|
-
return this.rebuildSpans(
|
|
3587
|
-
text,
|
|
3588
|
-
spans,
|
|
3589
|
-
(category, value) => this.getPlaceholder(category, value)
|
|
3590
|
-
);
|
|
3591
|
-
}
|
|
3592
|
-
/**
|
|
3593
|
-
* Async counterpart to {@link maskText}: stateless `[CATEGORY]` masking over
|
|
3594
|
-
* regex + NER matches. With no detector, identical to `maskText`.
|
|
3595
|
-
*/
|
|
3596
|
-
async maskTextAsync(text) {
|
|
3597
|
-
if (!this.nerDetector) return this.maskText(text);
|
|
3598
|
-
const spans = await this.detectAllSpans(text);
|
|
3599
|
-
return this.rebuildSpans(text, spans, (category) => `[${category}]`).text;
|
|
3600
|
-
}
|
|
3601
|
-
/**
|
|
3602
|
-
* Async counterpart to {@link redactMessages}. Text parts are redacted
|
|
3603
|
-
* sequentially so placeholder numbering stays deterministic across the
|
|
3604
|
-
* conversation. With no detector, identical to `redactMessages`.
|
|
3605
|
-
*/
|
|
3606
|
-
async redactMessagesAsync(messages) {
|
|
3607
|
-
if (!this.nerDetector) return this.redactMessages(messages);
|
|
3608
|
-
const allMatches = [];
|
|
3609
|
-
const redactedMessages = [];
|
|
3610
|
-
for (const msg of messages) {
|
|
3611
|
-
if (!msg.content) {
|
|
3612
|
-
redactedMessages.push(msg);
|
|
3613
|
-
continue;
|
|
3614
|
-
}
|
|
3615
|
-
const newContent = [];
|
|
3616
|
-
for (const part of msg.content) {
|
|
3617
|
-
if (part.type === "text" && part.text) {
|
|
3618
|
-
const result = await this.redactTextAsync(part.text);
|
|
3619
|
-
allMatches.push(...result.matches);
|
|
3620
|
-
newContent.push(result.matches.length > 0 ? { ...part, text: result.text } : part);
|
|
3621
|
-
} else {
|
|
3622
|
-
if (part.type !== "text") this.warnNonTextBypass();
|
|
3623
|
-
newContent.push(part);
|
|
3624
|
-
}
|
|
3304
|
+
function scoreTools(promptEmbeddings, tools) {
|
|
3305
|
+
const scores = /* @__PURE__ */ new Map();
|
|
3306
|
+
if (!promptEmbeddings || promptEmbeddings.length === 0) return scores;
|
|
3307
|
+
if (!tools || tools.length === 0) return scores;
|
|
3308
|
+
const embeddings = Array.isArray(promptEmbeddings[0]) ? promptEmbeddings : [promptEmbeddings];
|
|
3309
|
+
for (const tool of tools) {
|
|
3310
|
+
if (!tool.embedding || tool.embedding.length === 0) continue;
|
|
3311
|
+
let maxSimilarity = -Infinity;
|
|
3312
|
+
try {
|
|
3313
|
+
for (const embedding of embeddings) {
|
|
3314
|
+
const similarity = cosineSimilarity(embedding, tool.embedding);
|
|
3315
|
+
if (similarity > maxSimilarity) maxSimilarity = similarity;
|
|
3625
3316
|
}
|
|
3626
|
-
|
|
3317
|
+
} catch {
|
|
3318
|
+
continue;
|
|
3627
3319
|
}
|
|
3628
|
-
|
|
3629
|
-
}
|
|
3630
|
-
/**
|
|
3631
|
-
* Redact PII from an array of LlmapiMessage objects.
|
|
3632
|
-
* Returns new message objects — originals are not mutated.
|
|
3633
|
-
*
|
|
3634
|
-
* All roles' text content is redacted, not just user/system: tool results
|
|
3635
|
-
* can return PII fetched from external systems, and assistant history is
|
|
3636
|
-
* persisted with original (de-anonymized) values, so both must be re-redacted
|
|
3637
|
-
* before they go back over the wire. Placeholders the model already emitted
|
|
3638
|
-
* are inert here (they don't match any PII pattern), so re-redaction is safe.
|
|
3639
|
-
*/
|
|
3640
|
-
redactMessages(messages) {
|
|
3641
|
-
const allMatches = [];
|
|
3642
|
-
const redactedMessages = messages.map((msg) => {
|
|
3643
|
-
if (!msg.content) return msg;
|
|
3644
|
-
const newContent = msg.content.map((part) => {
|
|
3645
|
-
if (part.type === "text" && part.text) {
|
|
3646
|
-
const result = this.redactText(part.text);
|
|
3647
|
-
allMatches.push(...result.matches);
|
|
3648
|
-
if (result.matches.length > 0) {
|
|
3649
|
-
return { ...part, text: result.text };
|
|
3650
|
-
}
|
|
3651
|
-
} else if (part.type !== "text") {
|
|
3652
|
-
this.warnNonTextBypass();
|
|
3653
|
-
}
|
|
3654
|
-
return part;
|
|
3655
|
-
});
|
|
3656
|
-
return { ...msg, content: newContent };
|
|
3657
|
-
});
|
|
3658
|
-
return { messages: redactedMessages, matches: allMatches };
|
|
3659
|
-
}
|
|
3660
|
-
/** Emit the non-text bypass warning at most once per redactor instance. */
|
|
3661
|
-
warnNonTextBypass() {
|
|
3662
|
-
if (this.warnedNonText) return;
|
|
3663
|
-
this.warnedNonText = true;
|
|
3664
|
-
console.warn(
|
|
3665
|
-
"[PiiRedactor] A message contains non-text content (images, files, or attachments) that is NOT scanned for PII \u2014 only text parts are redacted."
|
|
3666
|
-
);
|
|
3667
|
-
}
|
|
3668
|
-
/** Emit the NER-detector-failure warning at most once per redactor instance. */
|
|
3669
|
-
warnNerFailure(err) {
|
|
3670
|
-
if (this.warnedNerFailure) return;
|
|
3671
|
-
this.warnedNerFailure = true;
|
|
3672
|
-
console.warn(
|
|
3673
|
-
"[PiiRedactor] The NER detector threw; falling back to regex-only redaction for this call's unstructured PII. The detector is still retried on subsequent calls; this warning is emitted only once.",
|
|
3674
|
-
err
|
|
3675
|
-
);
|
|
3320
|
+
if (maxSimilarity > -Infinity) scores.set(tool.name, maxSimilarity);
|
|
3676
3321
|
}
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3322
|
+
return scores;
|
|
3323
|
+
}
|
|
3324
|
+
var BUILT_IN_TOOL_SETS = [
|
|
3325
|
+
{
|
|
3326
|
+
name: "app-generation",
|
|
3327
|
+
systemPrompt: APP_BUILDER_PROMPT,
|
|
3328
|
+
members: [
|
|
3329
|
+
"create_file",
|
|
3330
|
+
"patch_file",
|
|
3331
|
+
"delete_file",
|
|
3332
|
+
"read_file",
|
|
3333
|
+
"list_files",
|
|
3334
|
+
"audit_design",
|
|
3335
|
+
"critique_design",
|
|
3336
|
+
"verify_app"
|
|
3337
|
+
],
|
|
3338
|
+
anchors: ["create_file", "patch_file"],
|
|
3339
|
+
anchorMinSimilarity: 0.55
|
|
3340
|
+
},
|
|
3341
|
+
{
|
|
3342
|
+
name: "slides",
|
|
3343
|
+
members: ["plan_deck", "add_slide", "read_slides", "patch_slides"],
|
|
3344
|
+
anchors: ["plan_deck", "patch_slides"],
|
|
3345
|
+
anchorMinSimilarity: 0.53
|
|
3346
|
+
},
|
|
3347
|
+
{
|
|
3348
|
+
name: "documents",
|
|
3349
|
+
systemPrompt: DOCUMENT_BUILDER_PROMPT,
|
|
3350
|
+
members: ["create_document", "read_document", "patch_document"],
|
|
3351
|
+
anchors: ["create_document", "patch_document"],
|
|
3352
|
+
anchorMinSimilarity: 0.53
|
|
3353
|
+
},
|
|
3354
|
+
{
|
|
3355
|
+
name: "github",
|
|
3356
|
+
members: ["github_get_authenticated_user", "github_api"],
|
|
3357
|
+
anchors: ["github_api"],
|
|
3358
|
+
anchorMinSimilarity: 0.55
|
|
3359
|
+
},
|
|
3360
|
+
{
|
|
3361
|
+
name: "gmail",
|
|
3362
|
+
members: [
|
|
3363
|
+
"gmail_search_messages",
|
|
3364
|
+
"gmail_get_message",
|
|
3365
|
+
"gmail_create_draft",
|
|
3366
|
+
"gmail_send_message"
|
|
3367
|
+
],
|
|
3368
|
+
anchors: ["gmail_search_messages", "gmail_send_message"],
|
|
3369
|
+
anchorMinSimilarity: 0.53
|
|
3370
|
+
},
|
|
3371
|
+
{
|
|
3372
|
+
name: "google-calendar",
|
|
3373
|
+
members: [
|
|
3374
|
+
"google_calendar_list_events",
|
|
3375
|
+
"google_calendar_create_event",
|
|
3376
|
+
"google_calendar_update_event"
|
|
3377
|
+
],
|
|
3378
|
+
anchors: ["google_calendar_list_events", "google_calendar_create_event"],
|
|
3379
|
+
anchorMinSimilarity: 0.53
|
|
3380
|
+
},
|
|
3381
|
+
{
|
|
3382
|
+
name: "google-drive",
|
|
3383
|
+
members: [
|
|
3384
|
+
"google_drive_search",
|
|
3385
|
+
"google_drive_list_recent",
|
|
3386
|
+
"google_drive_get_content",
|
|
3387
|
+
"google_drive_create_file",
|
|
3388
|
+
"google_drive_update_file"
|
|
3389
|
+
],
|
|
3390
|
+
anchors: ["google_drive_search", "google_drive_get_content", "google_drive_create_file"],
|
|
3391
|
+
anchorMinSimilarity: 0.53
|
|
3392
|
+
},
|
|
3393
|
+
{
|
|
3394
|
+
name: "notion",
|
|
3395
|
+
members: ["notion-search", "notion-fetch", "notion-create-pages", "notion-update-page"],
|
|
3396
|
+
anchors: ["notion-search", "notion-create-pages"],
|
|
3397
|
+
anchorMinSimilarity: 0.53
|
|
3398
|
+
},
|
|
3399
|
+
{
|
|
3400
|
+
name: "x",
|
|
3401
|
+
members: ["x_get_me", "x_get_my_posts"],
|
|
3402
|
+
anchors: ["x_get_me", "x_get_my_posts"],
|
|
3403
|
+
anchorMinSimilarity: 0.53
|
|
3404
|
+
},
|
|
3405
|
+
{
|
|
3406
|
+
name: "slack",
|
|
3407
|
+
members: [
|
|
3408
|
+
"slack_get_me",
|
|
3409
|
+
"slack_list_channels",
|
|
3410
|
+
"slack_search_messages",
|
|
3411
|
+
"slack_list_users",
|
|
3412
|
+
"slack_get_channel_history",
|
|
3413
|
+
"slack_get_thread_replies",
|
|
3414
|
+
"slack_post_message"
|
|
3415
|
+
],
|
|
3416
|
+
anchors: ["slack_search_messages", "slack_list_channels", "slack_post_message"],
|
|
3417
|
+
anchorMinSimilarity: 0.53
|
|
3418
|
+
},
|
|
3419
|
+
{
|
|
3420
|
+
name: "dropbox",
|
|
3421
|
+
members: ["dropbox_list_folders", "dropbox_get_file_content", "dropbox_search"],
|
|
3422
|
+
anchors: ["dropbox_list_folders", "dropbox_search"],
|
|
3423
|
+
anchorMinSimilarity: 0.53
|
|
3424
|
+
},
|
|
3425
|
+
{
|
|
3426
|
+
name: "restaurant-booking",
|
|
3427
|
+
members: [
|
|
3428
|
+
"AnumaPaymentsMCP-anuma_find_restaurant",
|
|
3429
|
+
"AnumaPaymentsMCP-anuma_check_restaurant_availability",
|
|
3430
|
+
"AnumaPaymentsMCP-anuma_book_restaurant",
|
|
3431
|
+
"AnumaPaymentsMCP-anuma_list_reservations",
|
|
3432
|
+
"AnumaPaymentsMCP-anuma_cancel_reservation",
|
|
3433
|
+
"AnumaPaymentsMCP-anuma_discover_restaurants"
|
|
3434
|
+
],
|
|
3435
|
+
anchors: []
|
|
3436
|
+
},
|
|
3437
|
+
{
|
|
3438
|
+
name: "restaurant-book",
|
|
3439
|
+
members: [
|
|
3440
|
+
"AnumaPaymentsMCP-anuma_find_restaurant",
|
|
3441
|
+
"AnumaPaymentsMCP-anuma_check_restaurant_availability",
|
|
3442
|
+
"AnumaPaymentsMCP-anuma_book_restaurant"
|
|
3443
|
+
],
|
|
3444
|
+
anchors: []
|
|
3445
|
+
},
|
|
3446
|
+
{
|
|
3447
|
+
name: "restaurant-cancel",
|
|
3448
|
+
members: [
|
|
3449
|
+
"AnumaPaymentsMCP-anuma_list_reservations",
|
|
3450
|
+
"AnumaPaymentsMCP-anuma_cancel_reservation"
|
|
3451
|
+
],
|
|
3452
|
+
anchors: []
|
|
3694
3453
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
* redaction (a model hallucination, or an ambiguous re-cased collision).
|
|
3703
|
-
* Callers drop/degrade such facts so an opaque token never reaches the vault.
|
|
3704
|
-
*
|
|
3705
|
-
* One left-to-right pass over the ORIGINAL text. Two properties matter:
|
|
3706
|
-
* - a restored value is never re-scanned — so a real value that happens to
|
|
3707
|
-
* contain a `<CATEGORY>_<n>` substring (e.g. the email `ssn_1@example.com`,
|
|
3708
|
-
* or an API key containing `EMAIL_1`) is neither corrupted nor false-flagged;
|
|
3709
|
-
* - `unresolved` is derived from the lookup miss during this pass, not from a
|
|
3710
|
-
* regex re-scan of the output — so it can't trip over a restored value.
|
|
3711
|
-
*/
|
|
3712
|
-
restoreForStorage(text) {
|
|
3713
|
-
if (this.storagePattern === void 0) this.storagePattern = this.buildStoragePattern();
|
|
3714
|
-
if (!this.storagePattern || this.placeholderToValue.size === 0) {
|
|
3715
|
-
return { text, unresolved: false };
|
|
3716
|
-
}
|
|
3717
|
-
this.storageLookup ?? (this.storageLookup = this.buildStorageLookup());
|
|
3718
|
-
const { exact, ci, ciCollisions } = this.storageLookup;
|
|
3719
|
-
let unresolved = false;
|
|
3720
|
-
const restored = text.replace(
|
|
3721
|
-
this.storagePattern,
|
|
3722
|
-
(match, bracketed, bare) => {
|
|
3723
|
-
const body = bracketed ?? bare ?? "";
|
|
3724
|
-
const exactHit = exact.get(body);
|
|
3725
|
-
if (exactHit !== void 0) return exactHit;
|
|
3726
|
-
const key = body.toUpperCase();
|
|
3727
|
-
if (!ciCollisions.has(key)) {
|
|
3728
|
-
const ciHit = ci.get(key);
|
|
3729
|
-
if (ciHit !== void 0) return ciHit;
|
|
3730
|
-
}
|
|
3731
|
-
unresolved = true;
|
|
3732
|
-
return match;
|
|
3733
|
-
}
|
|
3454
|
+
];
|
|
3455
|
+
function applyToolSets(matchedNames, availableNames, scores, toolSets = BUILT_IN_TOOL_SETS, independentThreshold = 0.65) {
|
|
3456
|
+
const activatedSets = [];
|
|
3457
|
+
for (const ts of toolSets) {
|
|
3458
|
+
const minSim = ts.anchorMinSimilarity ?? 0.6;
|
|
3459
|
+
const triggered = ts.anchors.some(
|
|
3460
|
+
(anchor) => matchedNames.has(anchor) && (scores.get(anchor) ?? 0) >= minSim
|
|
3734
3461
|
);
|
|
3735
|
-
|
|
3462
|
+
if (triggered) activatedSets.push(ts);
|
|
3736
3463
|
}
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
const ci = /* @__PURE__ */ new Map();
|
|
3747
|
-
const ciCollisions = /* @__PURE__ */ new Set();
|
|
3748
|
-
for (const [placeholder, value] of this.placeholderToValue) {
|
|
3749
|
-
const body = placeholder.slice(1, -1);
|
|
3750
|
-
exact.set(body, value);
|
|
3751
|
-
const key = body.toUpperCase();
|
|
3752
|
-
if (ci.has(key) && ci.get(key) !== value) ciCollisions.add(key);
|
|
3753
|
-
else ci.set(key, value);
|
|
3464
|
+
if (activatedSets.length === 0) return matchedNames;
|
|
3465
|
+
const setMembers = /* @__PURE__ */ new Set();
|
|
3466
|
+
for (const ts of activatedSets) {
|
|
3467
|
+
for (const member of ts.members) setMembers.add(member);
|
|
3468
|
+
}
|
|
3469
|
+
const result = /* @__PURE__ */ new Set();
|
|
3470
|
+
for (const member of setMembers) {
|
|
3471
|
+
if (availableNames.has(member)) {
|
|
3472
|
+
result.add(member);
|
|
3754
3473
|
}
|
|
3755
|
-
return { exact, ci, ciCollisions };
|
|
3756
3474
|
}
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
this.categoryCounters.clear();
|
|
3764
|
-
this.storageLookup = null;
|
|
3765
|
-
this.storagePattern = void 0;
|
|
3766
|
-
this.warnedNonText = false;
|
|
3767
|
-
this.warnedNerFailure = false;
|
|
3475
|
+
for (const name of matchedNames) {
|
|
3476
|
+
if (setMembers.has(name)) continue;
|
|
3477
|
+
const score = scores.get(name) ?? 0;
|
|
3478
|
+
if (score >= independentThreshold) {
|
|
3479
|
+
result.add(name);
|
|
3480
|
+
}
|
|
3768
3481
|
}
|
|
3769
|
-
|
|
3770
|
-
function isPiiRedactor(value) {
|
|
3771
|
-
return typeof value === "object" && value !== null && typeof value.redactMessages === "function" && typeof value.deAnonymize === "function";
|
|
3772
|
-
}
|
|
3773
|
-
function resolvePiiRedactor(piiRedaction) {
|
|
3774
|
-
if (piiRedaction === true) return new PiiRedactor();
|
|
3775
|
-
if (!piiRedaction) return void 0;
|
|
3776
|
-
if (isPiiRedactor(piiRedaction)) return piiRedaction;
|
|
3777
|
-
console.warn(
|
|
3778
|
-
"[PiiRedactor] `piiRedaction` is neither `true` nor a PiiRedactor instance; PII redaction is DISABLED for this request."
|
|
3779
|
-
);
|
|
3780
|
-
return void 0;
|
|
3482
|
+
return result;
|
|
3781
3483
|
}
|
|
3782
|
-
function
|
|
3783
|
-
|
|
3784
|
-
const
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
emit(redactor.deAnonymize(buffer.slice(0, m.index)));
|
|
3795
|
-
buffer = buffer.slice(m.index);
|
|
3796
|
-
}
|
|
3797
|
-
},
|
|
3798
|
-
flush() {
|
|
3799
|
-
if (buffer) {
|
|
3800
|
-
emit(redactor.deAnonymize(buffer));
|
|
3801
|
-
buffer = "";
|
|
3484
|
+
function expandToolSetsAdditive(matchedNames, availableNames, scores, toolSets, activeSetNames) {
|
|
3485
|
+
const result = new Set(matchedNames);
|
|
3486
|
+
for (const ts of toolSets) {
|
|
3487
|
+
let triggered = activeSetNames?.has(ts.name) ?? false;
|
|
3488
|
+
if (!triggered) {
|
|
3489
|
+
const minSim = ts.anchorMinSimilarity ?? 0.6;
|
|
3490
|
+
triggered = ts.anchors.some((a) => (scores.get(a) ?? 0) >= minSim);
|
|
3491
|
+
}
|
|
3492
|
+
if (!triggered) continue;
|
|
3493
|
+
for (const member of ts.members) {
|
|
3494
|
+
if (availableNames.has(member)) {
|
|
3495
|
+
result.add(member);
|
|
3802
3496
|
}
|
|
3803
3497
|
}
|
|
3498
|
+
}
|
|
3499
|
+
return result;
|
|
3500
|
+
}
|
|
3501
|
+
function activatedToolSetNames(scores, toolSets = BUILT_IN_TOOL_SETS, activeSetNames) {
|
|
3502
|
+
const activated = /* @__PURE__ */ new Set();
|
|
3503
|
+
for (const ts of toolSets) {
|
|
3504
|
+
if (activeSetNames?.has(ts.name)) {
|
|
3505
|
+
activated.add(ts.name);
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3508
|
+
const minSim = ts.anchorMinSimilarity ?? 0.6;
|
|
3509
|
+
if (ts.anchors.some((a) => (scores.get(a) ?? 0) >= minSim)) {
|
|
3510
|
+
activated.add(ts.name);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
return activated;
|
|
3514
|
+
}
|
|
3515
|
+
function toolSetSystemPrompts(selectedToolNames, toolSets = BUILT_IN_TOOL_SETS, activatedSetNames) {
|
|
3516
|
+
const selected = selectedToolNames instanceof Set ? selectedToolNames : new Set(selectedToolNames);
|
|
3517
|
+
const prompts = [];
|
|
3518
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3519
|
+
for (const ts of toolSets) {
|
|
3520
|
+
const prompt = ts.systemPrompt;
|
|
3521
|
+
if (!prompt || seen.has(prompt)) continue;
|
|
3522
|
+
const active = activatedSetNames ? activatedSetNames.has(ts.name) : ts.anchors.some((anchor) => selected.has(anchor));
|
|
3523
|
+
if (active) {
|
|
3524
|
+
prompts.push(prompt);
|
|
3525
|
+
seen.add(prompt);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
return prompts;
|
|
3529
|
+
}
|
|
3530
|
+
function createServerToolsFilter(options = {}) {
|
|
3531
|
+
const exclude = new Set(options.excludeTools ?? []);
|
|
3532
|
+
const sets = options.toolSets ?? [];
|
|
3533
|
+
const matchOpts = options.matchOptions;
|
|
3534
|
+
const filter = (embeddings, tools) => {
|
|
3535
|
+
const matches = findMatchingTools(embeddings, tools, matchOpts);
|
|
3536
|
+
if (matches.length === 0) return [];
|
|
3537
|
+
const matchedNames = new Set(matches.map((m) => m.tool.name));
|
|
3538
|
+
let finalNames;
|
|
3539
|
+
if (sets.length > 0) {
|
|
3540
|
+
const scores = scoreTools(embeddings, tools);
|
|
3541
|
+
const selectedScores = new Map(
|
|
3542
|
+
[...scores].filter(([name]) => matchedNames.has(name) && !exclude.has(name))
|
|
3543
|
+
);
|
|
3544
|
+
const availableNames = new Set(tools.map((t) => t.name));
|
|
3545
|
+
finalNames = expandToolSetsAdditive(matchedNames, availableNames, selectedScores, sets);
|
|
3546
|
+
} else {
|
|
3547
|
+
finalNames = matchedNames;
|
|
3548
|
+
}
|
|
3549
|
+
for (const name of exclude) finalNames.delete(name);
|
|
3550
|
+
return [...finalNames];
|
|
3804
3551
|
};
|
|
3552
|
+
return Object.defineProperty(filter, "excludeTools", {
|
|
3553
|
+
value: Object.freeze([...exclude]),
|
|
3554
|
+
enumerable: false
|
|
3555
|
+
});
|
|
3805
3556
|
}
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3557
|
+
var DEFAULT_EXCLUDED_SERVER_TOOLS = [
|
|
3558
|
+
"AnumaVisionMCP-anuma_analyze_image",
|
|
3559
|
+
"OpenMeteoMCP-weather_forecast",
|
|
3560
|
+
"AnumaSequentialThinkingMCP-sequentialthinking"
|
|
3561
|
+
];
|
|
3562
|
+
var DEFAULT_SERVER_TOOLS_MATCH_OPTIONS = {
|
|
3563
|
+
limit: 5,
|
|
3564
|
+
minSimilarity: 0.5
|
|
3565
|
+
};
|
|
3566
|
+
var SERVER_TOOL_DEPENDENCY_SETS = [
|
|
3567
|
+
{
|
|
3568
|
+
name: "web-research",
|
|
3569
|
+
members: [
|
|
3570
|
+
"AnumaJinaMCP-search_web",
|
|
3571
|
+
"AnumaSearchMCP-anuma_scrape_url",
|
|
3572
|
+
"AnumaJinaMCP-parallel_search_web"
|
|
3573
|
+
],
|
|
3574
|
+
anchors: ["AnumaJinaMCP-search_web"],
|
|
3575
|
+
anchorMinSimilarity: 0.5
|
|
3576
|
+
},
|
|
3577
|
+
{
|
|
3578
|
+
name: "openmeteo-geocode",
|
|
3579
|
+
members: ["OpenMeteoMCP-geocoding"],
|
|
3580
|
+
anchors: [
|
|
3581
|
+
"OpenMeteoMCP-weather_forecast",
|
|
3582
|
+
"OpenMeteoMCP-air_quality",
|
|
3583
|
+
"OpenMeteoMCP-weather_archive",
|
|
3584
|
+
"OpenMeteoMCP-marine_weather",
|
|
3585
|
+
"OpenMeteoMCP-flood_forecast",
|
|
3586
|
+
"OpenMeteoMCP-climate_projection",
|
|
3587
|
+
"OpenMeteoMCP-elevation"
|
|
3588
|
+
],
|
|
3589
|
+
anchorMinSimilarity: 0.5
|
|
3590
|
+
},
|
|
3591
|
+
{
|
|
3592
|
+
name: "restaurant-cancel-lookup",
|
|
3593
|
+
members: ["AnumaPaymentsMCP-anuma_list_reservations"],
|
|
3594
|
+
anchors: ["AnumaPaymentsMCP-anuma_cancel_reservation"],
|
|
3595
|
+
anchorMinSimilarity: 0.5
|
|
3596
|
+
}
|
|
3597
|
+
];
|
|
3598
|
+
var defaultServerToolsFilter = createServerToolsFilter({
|
|
3599
|
+
excludeTools: DEFAULT_EXCLUDED_SERVER_TOOLS,
|
|
3600
|
+
matchOptions: DEFAULT_SERVER_TOOLS_MATCH_OPTIONS,
|
|
3601
|
+
toolSets: SERVER_TOOL_DEPENDENCY_SETS
|
|
3602
|
+
});
|
|
3603
|
+
async function selectServerSideTools(options) {
|
|
3604
|
+
const {
|
|
3605
|
+
prompt,
|
|
3606
|
+
getToken,
|
|
3607
|
+
apiKey,
|
|
3608
|
+
baseUrl,
|
|
3609
|
+
cacheExpirationMs,
|
|
3610
|
+
forceRefresh,
|
|
3611
|
+
embeddingModel,
|
|
3612
|
+
limit,
|
|
3613
|
+
minSimilarity,
|
|
3614
|
+
apiType = "responses"
|
|
3615
|
+
} = options;
|
|
3616
|
+
if (!getToken && !apiKey) {
|
|
3617
|
+
throw new Error("Either getToken or apiKey must be provided");
|
|
3618
|
+
}
|
|
3619
|
+
if (!prompt || prompt.trim().length < MIN_CONTENT_LENGTH_FOR_TOOLS) {
|
|
3620
|
+
return [];
|
|
3621
|
+
}
|
|
3622
|
+
const tools = await getServerTools({
|
|
3623
|
+
getToken,
|
|
3624
|
+
apiKey,
|
|
3625
|
+
baseUrl,
|
|
3626
|
+
cacheExpirationMs,
|
|
3627
|
+
forceRefresh
|
|
3628
|
+
});
|
|
3629
|
+
if (tools.length === 0) {
|
|
3630
|
+
return [];
|
|
3631
|
+
}
|
|
3632
|
+
const embeddingOptions = {
|
|
3633
|
+
getToken,
|
|
3634
|
+
apiKey,
|
|
3635
|
+
baseUrl,
|
|
3636
|
+
model: embeddingModel
|
|
3637
|
+
};
|
|
3638
|
+
let promptEmbeddings;
|
|
3639
|
+
if (shouldChunkMessage(prompt, DEFAULT_CHUNK_SIZE)) {
|
|
3640
|
+
const chunks = chunkText(prompt);
|
|
3641
|
+
promptEmbeddings = await generateEmbeddings(
|
|
3642
|
+
chunks.map((c) => c.text),
|
|
3643
|
+
embeddingOptions
|
|
3644
|
+
);
|
|
3645
|
+
} else {
|
|
3646
|
+
promptEmbeddings = await generateEmbedding(prompt, embeddingOptions);
|
|
3647
|
+
}
|
|
3648
|
+
const matchOptions = { filterAmbiguous: true, relevanceRatio: 0.85 };
|
|
3649
|
+
if (limit !== void 0) matchOptions.limit = limit;
|
|
3650
|
+
if (minSimilarity !== void 0) matchOptions.minSimilarity = minSimilarity;
|
|
3651
|
+
const matches = findMatchingTools(promptEmbeddings, tools, matchOptions);
|
|
3652
|
+
if (matches.length === 0) {
|
|
3653
|
+
return [];
|
|
3654
|
+
}
|
|
3655
|
+
const matchedTools = matches.map((m) => m.tool);
|
|
3656
|
+
if (apiType === "completions") {
|
|
3657
|
+
return matchedTools.map((t) => toCompletionsFormat(t));
|
|
3658
|
+
}
|
|
3659
|
+
return matchedTools.map(toResponsesFormat);
|
|
3811
3660
|
}
|
|
3812
3661
|
export {
|
|
3813
3662
|
BUILT_IN_TOOL_SETS,
|