@exulu/backend 2.3.0 → 3.1.0
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/{chunk-2242AI5L.js → chunk-ZDH5S2WF.js} +875 -277
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-C7L4PY6P.js → convert-exulu-tools-to-ai-sdk-tools-FN6WZSIQ.js} +1 -1
- package/dist/index.cjs +9337 -5830
- package/dist/index.d.cts +45 -9
- package/dist/index.d.ts +45 -9
- package/dist/index.js +8080 -5291
- package/ee/agentic-retrieval/pipeline/hyde.test.ts +1 -1
- package/ee/agentic-retrieval/pipeline/hyde.ts +6 -3
- package/ee/agentic-retrieval/pipeline/memory.test.ts +5 -1
- package/ee/agentic-retrieval/pipeline/memory.ts +71 -104
- package/ee/agentic-retrieval/pipeline/micro-call.test.ts +112 -0
- package/ee/agentic-retrieval/pipeline/micro-call.ts +98 -0
- package/ee/agentic-retrieval/pipeline/prefilter.test.ts +1 -0
- package/ee/agentic-retrieval/pipeline/prefilter.ts +11 -20
- package/ee/agentic-retrieval/pipeline/routing.test.ts +44 -1
- package/ee/agentic-retrieval/pipeline/routing.ts +31 -56
- package/ee/queues/decorator.ts +11 -0
- package/ee/queues/prune-job-results.test.ts +41 -0
- package/ee/queues/prune-job-results.ts +5 -4
- package/ee/schemas.ts +96 -1
- package/ee/workers.flow.test.ts +236 -0
- package/ee/workers.ts +409 -168
- package/package.json +6 -1
|
@@ -1285,69 +1285,132 @@ async function resolveModel(input) {
|
|
|
1285
1285
|
return { languageModel, model, exuluProvider, apiKey };
|
|
1286
1286
|
}
|
|
1287
1287
|
|
|
1288
|
-
// src/exulu/
|
|
1289
|
-
var
|
|
1288
|
+
// src/exulu/auth/validate.ts
|
|
1289
|
+
var OAUTH_REQUIRED_STRING_FIELDS = [
|
|
1290
1290
|
"authorizationUrl",
|
|
1291
1291
|
"tokenUrl",
|
|
1292
1292
|
"clientId",
|
|
1293
1293
|
"clientSecret"
|
|
1294
1294
|
];
|
|
1295
|
-
var
|
|
1296
|
-
|
|
1297
|
-
|
|
1295
|
+
var validateAuthConfig = (toolId, config) => {
|
|
1296
|
+
if (config.authType === "oauth") {
|
|
1297
|
+
for (const field of OAUTH_REQUIRED_STRING_FIELDS) {
|
|
1298
|
+
const value = config[field];
|
|
1299
|
+
if (!value || typeof value !== "string") {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`ExuluTool "${toolId}": oauth.${field} is required and must be a non-empty string.`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (!Array.isArray(config.scopes)) {
|
|
1298
1306
|
throw new Error(
|
|
1299
|
-
`ExuluTool "${toolId}": oauth
|
|
1307
|
+
`ExuluTool "${toolId}": oauth.scopes must be an array of strings (use [] to request no scopes).`
|
|
1300
1308
|
);
|
|
1301
1309
|
}
|
|
1310
|
+
if (config.provider !== void 0) {
|
|
1311
|
+
if (typeof config.provider !== "string" || config.provider.length === 0 || config.provider.trim() !== config.provider) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`ExuluTool "${toolId}": oauth.provider must be a non-empty string with no leading or trailing whitespace when set.`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (!process.env.BACKEND) {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
`ExuluTool "${toolId}": oauth requires the BACKEND environment variable (the backend's public base URL) to build the redirect URI.`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
return;
|
|
1302
1323
|
}
|
|
1303
|
-
if (
|
|
1304
|
-
throw new Error(
|
|
1305
|
-
`ExuluTool "${toolId}": oauth.scopes must be an array of strings (use [] to request no scopes).`
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
1308
|
-
if (config.provider !== void 0) {
|
|
1324
|
+
if (config.authType === "user_credentials") {
|
|
1309
1325
|
if (typeof config.provider !== "string" || config.provider.length === 0 || config.provider.trim() !== config.provider) {
|
|
1310
1326
|
throw new Error(
|
|
1311
|
-
`ExuluTool "${toolId}":
|
|
1327
|
+
`ExuluTool "${toolId}": user_credentials.provider must be a non-empty string with no leading or trailing whitespace.`
|
|
1312
1328
|
);
|
|
1313
1329
|
}
|
|
1330
|
+
if (!Array.isArray(config.fields) || config.fields.length === 0) {
|
|
1331
|
+
throw new Error(
|
|
1332
|
+
`ExuluTool "${toolId}": user_credentials.fields must contain at least one field.`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1336
|
+
for (let i = 0; i < config.fields.length; i++) {
|
|
1337
|
+
const field = config.fields[i];
|
|
1338
|
+
const name = field.name;
|
|
1339
|
+
if (typeof name !== "string" || name.length === 0 || name.trim() !== name) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`ExuluTool "${toolId}": user_credentials.fields[${i}].name must be a non-empty string with no leading or trailing whitespace.`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
if (seenNames.has(name)) {
|
|
1345
|
+
throw new Error(
|
|
1346
|
+
`ExuluTool "${toolId}": user_credentials.fields has duplicate field name '${name}'.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
seenNames.add(name);
|
|
1350
|
+
const type = field.type;
|
|
1351
|
+
if (type !== "text" && type !== "password") {
|
|
1352
|
+
throw new Error(
|
|
1353
|
+
`ExuluTool "${toolId}": user_credentials.fields[${i}].type must be 'text' or 'password' (got '${type}').`
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (!process.env.BACKEND) {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
`ExuluTool "${toolId}": user_credentials requires the BACKEND environment variable (the backend's public base URL) to build the credential submit URL.`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
return;
|
|
1314
1363
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1364
|
+
throw new Error(
|
|
1365
|
+
`ExuluTool "${toolId}": auth.authType '${config.authType}' is not supported.`
|
|
1366
|
+
);
|
|
1320
1367
|
};
|
|
1321
1368
|
|
|
1322
|
-
// src/exulu/
|
|
1369
|
+
// src/exulu/auth/provider-key.ts
|
|
1323
1370
|
var providerKeyFor = (toolId, config) => config.provider && config.provider.length > 0 ? config.provider : toolId;
|
|
1324
1371
|
|
|
1325
|
-
// src/exulu/
|
|
1372
|
+
// src/exulu/auth/registry.ts
|
|
1326
1373
|
var byProvider = /* @__PURE__ */ new Map();
|
|
1327
1374
|
var byTool = /* @__PURE__ */ new Map();
|
|
1328
|
-
var
|
|
1375
|
+
var STABLE_OAUTH_STRING_FIELDS = [
|
|
1329
1376
|
"authorizationUrl",
|
|
1330
1377
|
"tokenUrl",
|
|
1331
1378
|
"clientId",
|
|
1332
1379
|
"clientSecret"
|
|
1333
1380
|
];
|
|
1334
1381
|
var assertCompatible = (providerKey, toolId, existing, next) => {
|
|
1335
|
-
|
|
1336
|
-
|
|
1382
|
+
if (existing.authType !== next.authType) {
|
|
1383
|
+
throw new Error(
|
|
1384
|
+
`ExuluTool "${toolId}": auth.authType '${next.authType}' disagrees with another tool that shares provider "${providerKey}" using authType '${existing.authType}'.`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
if (existing.authType === "oauth" && next.authType === "oauth") {
|
|
1388
|
+
for (const field of STABLE_OAUTH_STRING_FIELDS) {
|
|
1389
|
+
if (existing[field] !== next[field]) {
|
|
1390
|
+
throw new Error(
|
|
1391
|
+
`ExuluTool "${toolId}": oauth.${field} disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must use identical authorizationUrl/tokenUrl/clientId/clientSecret.`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const a = new Set(existing.scopes);
|
|
1396
|
+
const b = new Set(next.scopes);
|
|
1397
|
+
if (a.size !== b.size || [...a].some((s) => !b.has(s))) {
|
|
1337
1398
|
throw new Error(
|
|
1338
|
-
`ExuluTool "${toolId}": oauth
|
|
1399
|
+
`ExuluTool "${toolId}": oauth.scopes disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must declare the same scope superset. Existing: [${[...a].sort().join(", ")}]. This tool: [${[...b].sort().join(", ")}].`
|
|
1339
1400
|
);
|
|
1340
1401
|
}
|
|
1402
|
+
return;
|
|
1341
1403
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1404
|
+
if (existing.authType === "user_credentials" && next.authType === "user_credentials") {
|
|
1405
|
+
if (JSON.stringify(existing.fields) !== JSON.stringify(next.fields)) {
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
`ExuluTool "${toolId}": user_credentials.fields disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must declare structurally identical fields (same names, types, and order).`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
return;
|
|
1348
1411
|
}
|
|
1349
1412
|
};
|
|
1350
|
-
var
|
|
1413
|
+
var authRegistry = {
|
|
1351
1414
|
register: (toolId, config) => {
|
|
1352
1415
|
const providerKey = providerKeyFor(toolId, config);
|
|
1353
1416
|
const existing = byProvider.get(providerKey);
|
|
@@ -1367,60 +1430,74 @@ var oauthRegistry = {
|
|
|
1367
1430
|
}
|
|
1368
1431
|
};
|
|
1369
1432
|
|
|
1370
|
-
// src/exulu/
|
|
1433
|
+
// src/exulu/auth/flow.ts
|
|
1371
1434
|
import CryptoJS3 from "crypto-js";
|
|
1372
1435
|
import { createHash, randomBytes } from "crypto";
|
|
1373
1436
|
|
|
1374
|
-
// src/exulu/
|
|
1437
|
+
// src/exulu/auth/credential-store.ts
|
|
1375
1438
|
import CryptoJS2 from "crypto-js";
|
|
1376
|
-
var TABLE = "
|
|
1439
|
+
var TABLE = "user_credentials";
|
|
1377
1440
|
var encrypt = (value) => CryptoJS2.AES.encrypt(value, process.env.NEXTAUTH_SECRET).toString();
|
|
1378
1441
|
var decrypt = (value) => CryptoJS2.AES.decrypt(value, process.env.NEXTAUTH_SECRET).toString(CryptoJS2.enc.Utf8);
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
return null;
|
|
1385
|
-
}
|
|
1386
|
-
return {
|
|
1387
|
-
accessToken: decrypt(row.access_token),
|
|
1388
|
-
refreshToken: row.refresh_token ? decrypt(row.refresh_token) : null,
|
|
1389
|
-
tokenType: row.token_type ?? null,
|
|
1390
|
-
scopes: row.scopes ?? null,
|
|
1391
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : null
|
|
1392
|
-
};
|
|
1393
|
-
},
|
|
1394
|
-
// toolId is stored on every written row as an audit trail — which tool
|
|
1395
|
-
// triggered the last grant/refresh — but does NOT participate in the key.
|
|
1396
|
-
upsert: async (providerKey, userId, toolId, record) => {
|
|
1397
|
-
const { db: db2 } = await postgresClient();
|
|
1398
|
-
const existing = await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).first();
|
|
1399
|
-
const values = {
|
|
1400
|
-
provider: providerKey,
|
|
1401
|
-
tool_id: toolId,
|
|
1402
|
-
access_token: encrypt(record.accessToken),
|
|
1403
|
-
// Providers like Google only send a refresh_token on first consent;
|
|
1404
|
-
// never overwrite a stored one with nothing.
|
|
1405
|
-
refresh_token: record.refreshToken ? encrypt(record.refreshToken) : existing?.refresh_token ?? null,
|
|
1406
|
-
token_type: record.tokenType ?? null,
|
|
1407
|
-
scopes: record.scopes ?? null,
|
|
1408
|
-
expires_at: record.expiresAt ?? null,
|
|
1409
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1410
|
-
};
|
|
1411
|
-
if (existing) {
|
|
1412
|
-
await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).update(values);
|
|
1413
|
-
} else {
|
|
1414
|
-
await db2.from(TABLE).insert({ user_id: userId, ...values });
|
|
1415
|
-
}
|
|
1416
|
-
},
|
|
1417
|
-
delete: async (providerKey, userId) => {
|
|
1418
|
-
const { db: db2 } = await postgresClient();
|
|
1419
|
-
await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).del();
|
|
1442
|
+
async function get(provider, userId) {
|
|
1443
|
+
const { db: db2 } = await postgresClient();
|
|
1444
|
+
const row = await db2.from(TABLE).where({ provider, user_id: String(userId) }).first();
|
|
1445
|
+
if (!row) {
|
|
1446
|
+
return null;
|
|
1420
1447
|
}
|
|
1421
|
-
|
|
1448
|
+
return {
|
|
1449
|
+
provider,
|
|
1450
|
+
userId,
|
|
1451
|
+
authType: row.auth_type,
|
|
1452
|
+
data: JSON.parse(decrypt(row.data))
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
async function upsert(record) {
|
|
1456
|
+
const { db: db2 } = await postgresClient();
|
|
1457
|
+
const encrypted = encrypt(JSON.stringify(record.data));
|
|
1458
|
+
await db2.from(TABLE).insert({
|
|
1459
|
+
provider: record.provider,
|
|
1460
|
+
user_id: String(record.userId),
|
|
1461
|
+
auth_type: record.authType,
|
|
1462
|
+
data: encrypted,
|
|
1463
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
1464
|
+
}).onConflict(["provider", "user_id"]).merge({ auth_type: record.authType, data: encrypted, updated_at: /* @__PURE__ */ new Date() });
|
|
1465
|
+
}
|
|
1466
|
+
async function listByUser(userId) {
|
|
1467
|
+
const { db: db2 } = await postgresClient();
|
|
1468
|
+
const list = await db2.from(TABLE).where({ user_id: String(userId) }).orderBy("provider");
|
|
1469
|
+
return list.map((row) => ({
|
|
1470
|
+
provider: row.provider,
|
|
1471
|
+
authType: row.auth_type,
|
|
1472
|
+
createdAt: row.created_at,
|
|
1473
|
+
updatedAt: row.updated_at
|
|
1474
|
+
}));
|
|
1475
|
+
}
|
|
1476
|
+
async function del(provider, userId) {
|
|
1477
|
+
const { db: db2 } = await postgresClient();
|
|
1478
|
+
await db2.from(TABLE).where({ provider, user_id: String(userId) }).del();
|
|
1479
|
+
}
|
|
1480
|
+
var credentialStore = { get, upsert, listByUser, delete: del };
|
|
1422
1481
|
|
|
1423
|
-
// src/exulu/
|
|
1482
|
+
// src/exulu/auth/flow.ts
|
|
1483
|
+
function oauthRecordToBlob(r) {
|
|
1484
|
+
return {
|
|
1485
|
+
accessToken: r.accessToken,
|
|
1486
|
+
refreshToken: r.refreshToken ?? null,
|
|
1487
|
+
tokenType: r.tokenType ?? null,
|
|
1488
|
+
scopes: r.scopes ?? null,
|
|
1489
|
+
expiresAt: r.expiresAt ? r.expiresAt.toISOString() : null
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
function oauthBlobToRecord(b) {
|
|
1493
|
+
return {
|
|
1494
|
+
accessToken: b.accessToken,
|
|
1495
|
+
refreshToken: b.refreshToken,
|
|
1496
|
+
tokenType: b.tokenType,
|
|
1497
|
+
scopes: b.scopes,
|
|
1498
|
+
expiresAt: b.expiresAt ? new Date(b.expiresAt) : null
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1424
1501
|
var OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
1425
1502
|
var STATE_TTL_MS = 10 * 60 * 1e3;
|
|
1426
1503
|
var EXPIRY_SKEW_MS = 30 * 1e3;
|
|
@@ -1563,7 +1640,8 @@ var getValidAccessToken = async ({
|
|
|
1563
1640
|
toolId,
|
|
1564
1641
|
config
|
|
1565
1642
|
}) => {
|
|
1566
|
-
const
|
|
1643
|
+
const credRow = await credentialStore.get(providerKey, userId);
|
|
1644
|
+
const stored = credRow ? oauthBlobToRecord(credRow.data) : null;
|
|
1567
1645
|
if (!stored) {
|
|
1568
1646
|
return null;
|
|
1569
1647
|
}
|
|
@@ -1572,7 +1650,7 @@ var getValidAccessToken = async ({
|
|
|
1572
1650
|
return stored;
|
|
1573
1651
|
}
|
|
1574
1652
|
if (!stored.refreshToken) {
|
|
1575
|
-
await
|
|
1653
|
+
await credentialStore.delete(providerKey, userId);
|
|
1576
1654
|
return null;
|
|
1577
1655
|
}
|
|
1578
1656
|
try {
|
|
@@ -1580,20 +1658,127 @@ var getValidAccessToken = async ({
|
|
|
1580
1658
|
if (!refreshed.refreshToken) {
|
|
1581
1659
|
refreshed.refreshToken = stored.refreshToken;
|
|
1582
1660
|
}
|
|
1583
|
-
await
|
|
1661
|
+
await credentialStore.upsert({
|
|
1662
|
+
provider: providerKey,
|
|
1663
|
+
userId,
|
|
1664
|
+
authType: "oauth",
|
|
1665
|
+
data: oauthRecordToBlob(refreshed)
|
|
1666
|
+
});
|
|
1584
1667
|
return refreshed;
|
|
1585
1668
|
} catch (error) {
|
|
1586
1669
|
console.error(
|
|
1587
1670
|
`[EXULU] OAuth token refresh failed for provider "${providerKey}" tool "${toolId}" user ${userId}:`,
|
|
1588
1671
|
error
|
|
1589
1672
|
);
|
|
1590
|
-
await
|
|
1673
|
+
await credentialStore.delete(providerKey, userId);
|
|
1591
1674
|
return null;
|
|
1592
1675
|
}
|
|
1593
1676
|
};
|
|
1594
1677
|
|
|
1595
|
-
// src/exulu/
|
|
1596
|
-
|
|
1678
|
+
// src/exulu/auth/state.ts
|
|
1679
|
+
async function getValidUserCredentials(cfg, userId) {
|
|
1680
|
+
const row = await credentialStore.get(cfg.provider, userId);
|
|
1681
|
+
if (!row || row.authType !== "user_credentials") return null;
|
|
1682
|
+
const values = {};
|
|
1683
|
+
for (const [k, v] of Object.entries(row.data)) {
|
|
1684
|
+
if (typeof v !== "string") return null;
|
|
1685
|
+
values[k] = v;
|
|
1686
|
+
}
|
|
1687
|
+
return values;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/exulu/auth/credentials-request.ts
|
|
1691
|
+
var DEFAULT_TTL_SECONDS = 15 * 60;
|
|
1692
|
+
function buildCredentialRequest(cfg, opts) {
|
|
1693
|
+
const ttl = opts.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
1694
|
+
const claims = {
|
|
1695
|
+
provider: cfg.provider,
|
|
1696
|
+
userId: opts.userId,
|
|
1697
|
+
expiresAt: Math.floor(Date.now() / 1e3) + ttl
|
|
1698
|
+
};
|
|
1699
|
+
const nonce = encrypt(JSON.stringify(claims));
|
|
1700
|
+
return {
|
|
1701
|
+
provider: cfg.provider,
|
|
1702
|
+
fields: cfg.fields,
|
|
1703
|
+
submitUrl: `${opts.baseUrl.replace(/\/+$/, "")}/credentials/submit`,
|
|
1704
|
+
nonce
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function verifyCredentialNonce(nonce) {
|
|
1708
|
+
let claims;
|
|
1709
|
+
try {
|
|
1710
|
+
claims = JSON.parse(decrypt(nonce));
|
|
1711
|
+
} catch {
|
|
1712
|
+
throw new Error("Invalid credential nonce");
|
|
1713
|
+
}
|
|
1714
|
+
if (typeof claims.provider !== "string" || typeof claims.userId !== "string" || typeof claims.expiresAt !== "number") {
|
|
1715
|
+
throw new Error("Malformed credential nonce claims");
|
|
1716
|
+
}
|
|
1717
|
+
if (claims.expiresAt < Math.floor(Date.now() / 1e3)) {
|
|
1718
|
+
throw new Error("Credential nonce expired");
|
|
1719
|
+
}
|
|
1720
|
+
return claims;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// src/exulu/auth/short-circuit.ts
|
|
1724
|
+
function credentialRequestResult(request) {
|
|
1725
|
+
return { credentialRequest: request, result: null };
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/exulu/auth/errors.ts
|
|
1729
|
+
var CredentialInvalidError = class extends Error {
|
|
1730
|
+
provider;
|
|
1731
|
+
reason;
|
|
1732
|
+
constructor(provider, reason) {
|
|
1733
|
+
super(reason ? `Credential invalid for provider '${provider}': ${reason}` : `Credential invalid for provider '${provider}'`);
|
|
1734
|
+
this.name = "CredentialInvalidError";
|
|
1735
|
+
this.provider = provider;
|
|
1736
|
+
this.reason = reason;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
// src/exulu/auth/wrap-execute.ts
|
|
1741
|
+
var wrapExecuteWithAuth = (toolId, config, execute) => {
|
|
1742
|
+
if (config.authType === "oauth") {
|
|
1743
|
+
return wrapExecuteWithOauthInternal(toolId, config, execute);
|
|
1744
|
+
}
|
|
1745
|
+
if (config.authType === "user_credentials") {
|
|
1746
|
+
return wrapUserCredentials(toolId, config, execute);
|
|
1747
|
+
}
|
|
1748
|
+
throw new Error(`ExuluTool "${toolId}": unknown authType`);
|
|
1749
|
+
};
|
|
1750
|
+
var wrapUserCredentials = (toolId, config, execute) => {
|
|
1751
|
+
return async (inputs, options) => {
|
|
1752
|
+
const userId = inputs?.user?.id;
|
|
1753
|
+
if (!userId) {
|
|
1754
|
+
return {
|
|
1755
|
+
result: `The "${toolId}" tool requires user-supplied credentials, which needs a signed-in user. No user identity is available for this run.`
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
const baseUrl = (process.env.BACKEND ?? "").replace(/\/+$/, "");
|
|
1759
|
+
if (!baseUrl) {
|
|
1760
|
+
return {
|
|
1761
|
+
result: `The "${toolId}" tool requires the BACKEND env var to build the credential submit URL.`
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
const values = await getValidUserCredentials(config, userId);
|
|
1765
|
+
if (!values) {
|
|
1766
|
+
const request = buildCredentialRequest(config, { baseUrl, userId: String(userId) });
|
|
1767
|
+
return credentialRequestResult(request);
|
|
1768
|
+
}
|
|
1769
|
+
try {
|
|
1770
|
+
return await execute({ ...inputs, credentials: values }, options);
|
|
1771
|
+
} catch (e) {
|
|
1772
|
+
if (e instanceof CredentialInvalidError && e.provider === config.provider) {
|
|
1773
|
+
await credentialStore.delete(config.provider, userId);
|
|
1774
|
+
const request = buildCredentialRequest(config, { baseUrl, userId: String(userId) });
|
|
1775
|
+
return credentialRequestResult(request);
|
|
1776
|
+
}
|
|
1777
|
+
throw e;
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
};
|
|
1781
|
+
var wrapExecuteWithOauthInternal = (toolId, config, execute) => {
|
|
1597
1782
|
return async (inputs, options) => {
|
|
1598
1783
|
const userId = inputs?.user?.id;
|
|
1599
1784
|
if (!userId) {
|
|
@@ -1633,7 +1818,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1633
1818
|
type;
|
|
1634
1819
|
tool;
|
|
1635
1820
|
needsApproval;
|
|
1636
|
-
|
|
1821
|
+
authentication;
|
|
1637
1822
|
config;
|
|
1638
1823
|
constructor({
|
|
1639
1824
|
id,
|
|
@@ -1645,7 +1830,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1645
1830
|
execute,
|
|
1646
1831
|
config,
|
|
1647
1832
|
needsApproval,
|
|
1648
|
-
|
|
1833
|
+
authentication: authentication2
|
|
1649
1834
|
}) {
|
|
1650
1835
|
if (!PUBLIC_TOOL_TYPES.includes(type)) {
|
|
1651
1836
|
throw new Error(
|
|
@@ -1654,11 +1839,11 @@ var ExuluTool = class _ExuluTool {
|
|
|
1654
1839
|
)}. The "agent" and "context" types are managed by Exulu internally and cannot be set on a tool.`
|
|
1655
1840
|
);
|
|
1656
1841
|
}
|
|
1657
|
-
if (
|
|
1658
|
-
|
|
1659
|
-
|
|
1842
|
+
if (authentication2) {
|
|
1843
|
+
validateAuthConfig(id, authentication2);
|
|
1844
|
+
authRegistry.register(id, authentication2);
|
|
1660
1845
|
}
|
|
1661
|
-
this.
|
|
1846
|
+
this.authentication = authentication2;
|
|
1662
1847
|
this.id = id;
|
|
1663
1848
|
this.config = config;
|
|
1664
1849
|
this.needsApproval = needsApproval ?? true;
|
|
@@ -1670,7 +1855,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1670
1855
|
this.tool = tool({
|
|
1671
1856
|
description,
|
|
1672
1857
|
inputSchema: inputSchema || z.object({}),
|
|
1673
|
-
execute:
|
|
1858
|
+
execute: authentication2 ? wrapExecuteWithAuth(id, authentication2, execute) : execute
|
|
1674
1859
|
});
|
|
1675
1860
|
}
|
|
1676
1861
|
/**
|
|
@@ -1718,7 +1903,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1718
1903
|
});
|
|
1719
1904
|
providerapikey = resolved.apiKey;
|
|
1720
1905
|
}
|
|
1721
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
1906
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-FN6WZSIQ.js");
|
|
1722
1907
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
1723
1908
|
[this],
|
|
1724
1909
|
[],
|
|
@@ -2230,11 +2415,14 @@ function buildProjectKbProfileDefaults(items) {
|
|
|
2230
2415
|
}
|
|
2231
2416
|
|
|
2232
2417
|
// ee/agentic-retrieval/pipeline/routing.ts
|
|
2233
|
-
import { generateText as generateText2, Output as Output2 } from "ai";
|
|
2234
2418
|
import { z as z5 } from "zod";
|
|
2235
2419
|
|
|
2420
|
+
// ee/agentic-retrieval/pipeline/micro-call.ts
|
|
2421
|
+
import { generateText, NoOutputGeneratedError, Output } from "ai";
|
|
2422
|
+
|
|
2236
2423
|
// src/utils/with-retry.ts
|
|
2237
|
-
async function withRetry(generateFn, maxRetries = 3) {
|
|
2424
|
+
async function withRetry(generateFn, maxRetries = 3, opts = {}) {
|
|
2425
|
+
const { shouldRetry, baseDelayMs = 1e3 } = opts;
|
|
2238
2426
|
let lastError;
|
|
2239
2427
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2240
2428
|
try {
|
|
@@ -2242,18 +2430,66 @@ async function withRetry(generateFn, maxRetries = 3) {
|
|
|
2242
2430
|
} catch (error) {
|
|
2243
2431
|
lastError = error;
|
|
2244
2432
|
console.error(`[EXULU] generateText attempt ${attempt} failed:`, error);
|
|
2245
|
-
if (attempt === maxRetries) {
|
|
2433
|
+
if (attempt === maxRetries || shouldRetry && !shouldRetry(error)) {
|
|
2246
2434
|
throw error;
|
|
2247
2435
|
}
|
|
2248
|
-
await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) *
|
|
2436
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * baseDelayMs));
|
|
2249
2437
|
}
|
|
2250
2438
|
}
|
|
2251
2439
|
throw lastError;
|
|
2252
2440
|
}
|
|
2253
2441
|
|
|
2442
|
+
// ee/agentic-retrieval/pipeline/micro-call.ts
|
|
2443
|
+
var MICRO_CALL_MAX_OUTPUT_TOKENS = 2e3;
|
|
2444
|
+
function microCallProviderOptions(model) {
|
|
2445
|
+
const modelId = typeof model === "string" ? model : model?.modelId;
|
|
2446
|
+
return typeof modelId === "string" && /gemini/i.test(modelId) ? { litellm: { reasoningEffort: "disable" } } : void 0;
|
|
2447
|
+
}
|
|
2448
|
+
async function microCall(args) {
|
|
2449
|
+
const {
|
|
2450
|
+
model,
|
|
2451
|
+
system,
|
|
2452
|
+
prompt,
|
|
2453
|
+
messages,
|
|
2454
|
+
schema,
|
|
2455
|
+
temperature = 0,
|
|
2456
|
+
maxOutputTokens = MICRO_CALL_MAX_OUTPUT_TOKENS,
|
|
2457
|
+
maxAttempts = 3,
|
|
2458
|
+
retryBaseDelayMs
|
|
2459
|
+
} = args;
|
|
2460
|
+
return withRetry(
|
|
2461
|
+
async () => {
|
|
2462
|
+
const result = await generateText({
|
|
2463
|
+
model,
|
|
2464
|
+
temperature,
|
|
2465
|
+
system,
|
|
2466
|
+
prompt,
|
|
2467
|
+
messages,
|
|
2468
|
+
...schema ? { output: Output.object({ schema }) } : {},
|
|
2469
|
+
maxOutputTokens,
|
|
2470
|
+
// withRetry owns retries. The SDK's internal retries on top of it
|
|
2471
|
+
// tripled request volume per attempt while the provider was already
|
|
2472
|
+
// rate-limiting.
|
|
2473
|
+
maxRetries: 0,
|
|
2474
|
+
providerOptions: microCallProviderOptions(model)
|
|
2475
|
+
});
|
|
2476
|
+
return {
|
|
2477
|
+
output: schema ? result.output : void 0,
|
|
2478
|
+
text: result.text
|
|
2479
|
+
};
|
|
2480
|
+
},
|
|
2481
|
+
maxAttempts,
|
|
2482
|
+
{
|
|
2483
|
+
// Empty output is deterministic for identical params — retrying only
|
|
2484
|
+
// added latency before the degraded path. Fail fast instead.
|
|
2485
|
+
shouldRetry: (error) => !NoOutputGeneratedError.isInstance(error),
|
|
2486
|
+
...retryBaseDelayMs !== void 0 ? { baseDelayMs: retryBaseDelayMs } : {}
|
|
2487
|
+
}
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2254
2491
|
// ee/agentic-retrieval/pipeline/prefilter.ts
|
|
2255
2492
|
import Fuse from "fuse.js";
|
|
2256
|
-
import { generateText, Output } from "ai";
|
|
2257
2493
|
import { z as z4 } from "zod";
|
|
2258
2494
|
|
|
2259
2495
|
// ee/agentic-retrieval/pipeline/text-utils.ts
|
|
@@ -2492,22 +2728,15 @@ async function resolveIdentifierPins({
|
|
|
2492
2728
|
identifierSets.map(async (set) => {
|
|
2493
2729
|
if (!set.contexts.length) return;
|
|
2494
2730
|
try {
|
|
2495
|
-
const { output } = await
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
matches: z4.array(z4.string()).optional()
|
|
2505
|
-
})
|
|
2506
|
-
}),
|
|
2507
|
-
maxOutputTokens: 300
|
|
2508
|
-
}),
|
|
2509
|
-
3
|
|
2510
|
-
);
|
|
2731
|
+
const { output } = await microCall({
|
|
2732
|
+
model,
|
|
2733
|
+
system: set.strategy === "exact" ? EXACT_EXTRACTION_PROMPT(set) : FUZZY_EXTRACTION_PROMPT(set),
|
|
2734
|
+
messages: [{ role: "user", content: question }],
|
|
2735
|
+
schema: z4.object({
|
|
2736
|
+
hasMatches: z4.boolean(),
|
|
2737
|
+
matches: z4.array(z4.string()).optional()
|
|
2738
|
+
})
|
|
2739
|
+
});
|
|
2511
2740
|
if (!output?.hasMatches || !output.matches?.length) return;
|
|
2512
2741
|
steps.push({ text: `Detected ${set.name} in the question: ${output.matches.join(", ")}` });
|
|
2513
2742
|
await Promise.all(
|
|
@@ -2605,24 +2834,17 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
|
|
|
2605
2834
|
const [docPageRaw, explicitKBRaw] = await Promise.all([
|
|
2606
2835
|
(async () => {
|
|
2607
2836
|
try {
|
|
2608
|
-
return await
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
pageNumber: z5.number().int().nullable().optional()
|
|
2620
|
-
})
|
|
2621
|
-
}),
|
|
2622
|
-
maxOutputTokens: 300
|
|
2623
|
-
}),
|
|
2624
|
-
3
|
|
2625
|
-
);
|
|
2837
|
+
return await microCall({
|
|
2838
|
+
model,
|
|
2839
|
+
system: buildDocPagePrompt(knownIdentifiers),
|
|
2840
|
+
messages: [{ role: "user", content: question }],
|
|
2841
|
+
schema: z5.object({
|
|
2842
|
+
hasFilenameHint: z5.boolean(),
|
|
2843
|
+
filenameHints: z5.array(z5.string()).optional(),
|
|
2844
|
+
hasPageHint: z5.boolean(),
|
|
2845
|
+
pageNumber: z5.number().int().nullable().optional()
|
|
2846
|
+
})
|
|
2847
|
+
});
|
|
2626
2848
|
} catch (err) {
|
|
2627
2849
|
steps.push({ text: "Doc/page detection failed \u2014 skipping filename and page hints." });
|
|
2628
2850
|
return {
|
|
@@ -2637,23 +2859,16 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
|
|
|
2637
2859
|
})(),
|
|
2638
2860
|
(async () => {
|
|
2639
2861
|
try {
|
|
2640
|
-
return await
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
explicitlyRequestedKnowledgeBases: z5.array(
|
|
2648
|
-
z5.enum(enabledContexts.map((c) => c.id))
|
|
2649
|
-
)
|
|
2650
|
-
})
|
|
2651
|
-
}),
|
|
2652
|
-
messages: [{ role: "user", content: question }],
|
|
2653
|
-
maxOutputTokens: 200
|
|
2862
|
+
return await microCall({
|
|
2863
|
+
model,
|
|
2864
|
+
system: kbSystemPrompt,
|
|
2865
|
+
schema: z5.object({
|
|
2866
|
+
explicitlyRequestedKnowledgeBases: z5.array(
|
|
2867
|
+
z5.enum(enabledContexts.map((c) => c.id))
|
|
2868
|
+
)
|
|
2654
2869
|
}),
|
|
2655
|
-
|
|
2656
|
-
);
|
|
2870
|
+
messages: [{ role: "user", content: question }]
|
|
2871
|
+
});
|
|
2657
2872
|
} catch (err) {
|
|
2658
2873
|
return { output: { explicitlyRequestedKnowledgeBases: [] } };
|
|
2659
2874
|
}
|
|
@@ -2736,22 +2951,15 @@ ${extraInstructions}
|
|
|
2736
2951
|
</instructions>`;
|
|
2737
2952
|
}
|
|
2738
2953
|
try {
|
|
2739
|
-
const { output: classified } = await
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
reason: z5.string()
|
|
2749
|
-
})
|
|
2750
|
-
}),
|
|
2751
|
-
maxOutputTokens: 200
|
|
2752
|
-
}),
|
|
2753
|
-
3
|
|
2754
|
-
);
|
|
2954
|
+
const { output: classified } = await microCall({
|
|
2955
|
+
model,
|
|
2956
|
+
system: classifyPrompt,
|
|
2957
|
+
messages: [{ role: "user", content: question }],
|
|
2958
|
+
schema: z5.object({
|
|
2959
|
+
ruleId: z5.enum(ruleIds),
|
|
2960
|
+
reason: z5.string()
|
|
2961
|
+
})
|
|
2962
|
+
});
|
|
2755
2963
|
const matchedRule = routingRules.find((r) => r.id === classified.ruleId);
|
|
2756
2964
|
if (matchedRule) {
|
|
2757
2965
|
const main = matchedRule.main.filter((id) => enabledIds.has(id));
|
|
@@ -2806,7 +3014,6 @@ ${extraInstructions}
|
|
|
2806
3014
|
}
|
|
2807
3015
|
|
|
2808
3016
|
// ee/agentic-retrieval/pipeline/memory.ts
|
|
2809
|
-
import { generateText as generateText3, Output as Output3 } from "ai";
|
|
2810
3017
|
import { z as z6 } from "zod";
|
|
2811
3018
|
|
|
2812
3019
|
// ee/agentic-retrieval/pipeline/multi-query.ts
|
|
@@ -3032,32 +3239,25 @@ async function runMemoryPhase({
|
|
|
3032
3239
|
`;
|
|
3033
3240
|
let relevantMemoryChunks = [];
|
|
3034
3241
|
try {
|
|
3035
|
-
const { output: output_relevant_memory } = await
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
role: "user",
|
|
3043
|
-
content: `
|
|
3242
|
+
const { output: output_relevant_memory } = await microCall({
|
|
3243
|
+
model,
|
|
3244
|
+
system: CHECK_MEMORIES_FOR_RELEVANT_INFORMATION,
|
|
3245
|
+
messages: [
|
|
3246
|
+
{
|
|
3247
|
+
role: "user",
|
|
3248
|
+
content: `
|
|
3044
3249
|
<user_question>${question}</user_question>
|
|
3045
3250
|
<relevant_keywords>${keywords.join(", ")}</relevant_keywords>
|
|
3046
3251
|
<important_keyword>${importantKeyword}</important_keyword>
|
|
3047
3252
|
`
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
}),
|
|
3057
|
-
maxOutputTokens: 400
|
|
3058
|
-
}),
|
|
3059
|
-
3
|
|
3060
|
-
);
|
|
3253
|
+
}
|
|
3254
|
+
],
|
|
3255
|
+
schema: z6.object({
|
|
3256
|
+
relevantChunkIds: z6.array(z6.string()).describe(
|
|
3257
|
+
"The chunk_ids (UUIDs at the start of each bullet) of chunks containing information relevant to the user's question. Empty array if none are relevant."
|
|
3258
|
+
)
|
|
3259
|
+
})
|
|
3260
|
+
});
|
|
3061
3261
|
const ids = new Set(output_relevant_memory?.relevantChunkIds ?? []);
|
|
3062
3262
|
relevantMemoryChunks = ids.size === 0 ? [] : retrieved_memory.filter((c) => ids.has(c.chunk_id));
|
|
3063
3263
|
} catch (e) {
|
|
@@ -3149,41 +3349,34 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3149
3349
|
`;
|
|
3150
3350
|
const [overrideResult, fileResult, queryResult] = await Promise.all([
|
|
3151
3351
|
// Override check: strict gate to decide if memory should be authoritative
|
|
3152
|
-
memoryConfig.override ?
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
role: "user",
|
|
3160
|
-
content: `
|
|
3352
|
+
memoryConfig.override ? microCall({
|
|
3353
|
+
model,
|
|
3354
|
+
system: CHECK_MEMORY_OVERRIDE,
|
|
3355
|
+
messages: [
|
|
3356
|
+
{
|
|
3357
|
+
role: "user",
|
|
3358
|
+
content: `
|
|
3161
3359
|
<user_question>${question}</user_question>
|
|
3162
3360
|
<relevant_keywords>${keywords.join(", ")}</relevant_keywords>
|
|
3163
3361
|
<important_keyword>${importantKeyword}</important_keyword>
|
|
3164
3362
|
`
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
}),
|
|
3183
|
-
maxOutputTokens: 300
|
|
3184
|
-
}),
|
|
3185
|
-
3
|
|
3186
|
-
).catch(() => ({
|
|
3363
|
+
}
|
|
3364
|
+
],
|
|
3365
|
+
schema: z6.object({
|
|
3366
|
+
overrides: z6.boolean().describe(
|
|
3367
|
+
"True ONLY if a memory chunk directly and sufficiently answers the user's question and should be authoritative over the documents. Be strict; when unsure, false."
|
|
3368
|
+
),
|
|
3369
|
+
confidence: z6.enum(["high", "medium", "low"]).describe(
|
|
3370
|
+
"Confidence that the selected memory chunk(s) fully and directly answer the question."
|
|
3371
|
+
),
|
|
3372
|
+
authoritativeChunkIds: z6.array(z6.string()).describe(
|
|
3373
|
+
"The chunk_ids of the memory chunk(s) that directly answer the question. Empty if overrides is false."
|
|
3374
|
+
),
|
|
3375
|
+
reason: z6.string().describe(
|
|
3376
|
+
"One short sentence: why this memory does or does not directly answer the question."
|
|
3377
|
+
)
|
|
3378
|
+
})
|
|
3379
|
+
}).catch(() => ({
|
|
3187
3380
|
output: {
|
|
3188
3381
|
overrides: false,
|
|
3189
3382
|
confidence: "low",
|
|
@@ -3199,44 +3392,30 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3199
3392
|
}
|
|
3200
3393
|
}),
|
|
3201
3394
|
// File prioritization: detect explicit document-pinning instructions in memory
|
|
3202
|
-
memoryConfig.filePrioritization ?
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
fileNameHints: z6.array(z6.string()).optional()
|
|
3212
|
-
})
|
|
3213
|
-
}),
|
|
3214
|
-
maxOutputTokens: 300
|
|
3215
|
-
}),
|
|
3216
|
-
3
|
|
3217
|
-
).catch(() => ({
|
|
3395
|
+
memoryConfig.filePrioritization ? microCall({
|
|
3396
|
+
model,
|
|
3397
|
+
system: "You are a helpful assistant that will strictly follow the user's instructions.",
|
|
3398
|
+
messages: [{ role: "user", content: PROMPT_EXTRACT_PRIORITIZED_FILES }],
|
|
3399
|
+
schema: z6.object({
|
|
3400
|
+
shouldPrioritizeFiles: z6.boolean(),
|
|
3401
|
+
fileNameHints: z6.array(z6.string()).optional()
|
|
3402
|
+
})
|
|
3403
|
+
}).catch(() => ({
|
|
3218
3404
|
output: { shouldPrioritizeFiles: false, fileNameHints: [] }
|
|
3219
3405
|
})) : Promise.resolve({
|
|
3220
3406
|
output: { shouldPrioritizeFiles: false, fileNameHints: [] }
|
|
3221
3407
|
}),
|
|
3222
3408
|
// Query augmentation: expand keywords with synonyms/abbreviations from memory
|
|
3223
|
-
memoryConfig.queryAugmentation && hasAugmentationContent ?
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
updatedImportantKeyword: z6.string()
|
|
3234
|
-
})
|
|
3235
|
-
}),
|
|
3236
|
-
maxOutputTokens: 600
|
|
3237
|
-
}),
|
|
3238
|
-
3
|
|
3239
|
-
).catch(() => ({
|
|
3409
|
+
memoryConfig.queryAugmentation && hasAugmentationContent ? microCall({
|
|
3410
|
+
model,
|
|
3411
|
+
system: "You are a helpful assistant that will strictly follow the user's instructions.",
|
|
3412
|
+
messages: [{ role: "user", content: QUERY_AUGMENTATION_PROMPT }],
|
|
3413
|
+
schema: z6.object({
|
|
3414
|
+
updatedUserQuestion: z6.string(),
|
|
3415
|
+
updatedRelevantKeywords: z6.array(z6.string()),
|
|
3416
|
+
updatedImportantKeyword: z6.string()
|
|
3417
|
+
})
|
|
3418
|
+
}).catch(() => ({
|
|
3240
3419
|
output: {
|
|
3241
3420
|
updatedUserQuestion: question,
|
|
3242
3421
|
updatedRelevantKeywords: [],
|
|
@@ -3318,7 +3497,6 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3318
3497
|
}
|
|
3319
3498
|
|
|
3320
3499
|
// ee/agentic-retrieval/pipeline/hyde.ts
|
|
3321
|
-
import { generateText as generateText4 } from "ai";
|
|
3322
3500
|
var hydeCache = /* @__PURE__ */ new Map();
|
|
3323
3501
|
var HYDE_CACHE_MAX = 200;
|
|
3324
3502
|
function hydeCacheKey(originalQuestion, relevantKeywords, styleHint, importantKeyword) {
|
|
@@ -3387,11 +3565,11 @@ IMPORTANT:
|
|
|
3387
3565
|
prompt += `
|
|
3388
3566
|
Question: "${originalQuestion}"
|
|
3389
3567
|
Relevant keywords: ${relevantKeywords.join(", ")}`;
|
|
3390
|
-
const { text } = await
|
|
3568
|
+
const { text } = await microCall({
|
|
3391
3569
|
model,
|
|
3392
3570
|
prompt,
|
|
3393
3571
|
temperature: 0.3,
|
|
3394
|
-
|
|
3572
|
+
maxAttempts: 1
|
|
3395
3573
|
});
|
|
3396
3574
|
const passage = (text || "").trim();
|
|
3397
3575
|
return passage.length > 0 ? passage : null;
|
|
@@ -4333,6 +4511,388 @@ var createNewMemoryItemTool = (agent, context) => {
|
|
|
4333
4511
|
});
|
|
4334
4512
|
};
|
|
4335
4513
|
|
|
4514
|
+
// src/templates/tools/context-write-tools.ts
|
|
4515
|
+
import { z as z10 } from "zod";
|
|
4516
|
+
|
|
4517
|
+
// src/exulu/table-names.ts
|
|
4518
|
+
var getTableName = (id) => sanitizeName(id) + "_items";
|
|
4519
|
+
var getChunksTableName = (id) => sanitizeName(id) + "_chunks";
|
|
4520
|
+
|
|
4521
|
+
// src/utils/check-item-write-access.ts
|
|
4522
|
+
var checkItemWriteAccess = async (context, record, user) => {
|
|
4523
|
+
if (!user) {
|
|
4524
|
+
return false;
|
|
4525
|
+
}
|
|
4526
|
+
if (user.super_admin === true) {
|
|
4527
|
+
return true;
|
|
4528
|
+
}
|
|
4529
|
+
if (user.type === "api" && (!user.scope_mode || user.scope_mode === "admin")) {
|
|
4530
|
+
return true;
|
|
4531
|
+
}
|
|
4532
|
+
if (record.rights_mode === "public") {
|
|
4533
|
+
return true;
|
|
4534
|
+
}
|
|
4535
|
+
if (record.rights_mode === "private") {
|
|
4536
|
+
return record.created_by != null && String(record.created_by) === String(user.id);
|
|
4537
|
+
}
|
|
4538
|
+
const validRightsModes = ["users", "roles", "teams"];
|
|
4539
|
+
if (!validRightsModes.includes(record.rights_mode)) {
|
|
4540
|
+
return false;
|
|
4541
|
+
}
|
|
4542
|
+
const entity = getTableName(context.id);
|
|
4543
|
+
const { db: db2 } = await postgresClient();
|
|
4544
|
+
if (record.rights_mode === "users") {
|
|
4545
|
+
const grant = await db2.from("rbac").where({
|
|
4546
|
+
entity,
|
|
4547
|
+
target_resource_id: record.id,
|
|
4548
|
+
access_type: "User",
|
|
4549
|
+
user_id: user.id,
|
|
4550
|
+
rights: "write"
|
|
4551
|
+
}).first();
|
|
4552
|
+
return !!grant;
|
|
4553
|
+
}
|
|
4554
|
+
if (record.rights_mode === "roles") {
|
|
4555
|
+
const roleId = typeof user.role === "string" ? user.role : user.role?.id;
|
|
4556
|
+
if (!roleId) {
|
|
4557
|
+
return false;
|
|
4558
|
+
}
|
|
4559
|
+
const grant = await db2.from("rbac").where({
|
|
4560
|
+
entity,
|
|
4561
|
+
target_resource_id: record.id,
|
|
4562
|
+
access_type: "Role",
|
|
4563
|
+
role_id: roleId,
|
|
4564
|
+
rights: "write"
|
|
4565
|
+
}).first();
|
|
4566
|
+
return !!grant;
|
|
4567
|
+
}
|
|
4568
|
+
if (record.rights_mode === "teams") {
|
|
4569
|
+
const teamId = typeof user.team === "string" ? user.team : user.team?.id;
|
|
4570
|
+
if (!teamId) {
|
|
4571
|
+
return false;
|
|
4572
|
+
}
|
|
4573
|
+
const grant = await db2.from("rbac").where({
|
|
4574
|
+
entity,
|
|
4575
|
+
target_resource_id: record.id,
|
|
4576
|
+
access_type: "Team",
|
|
4577
|
+
team_id: teamId,
|
|
4578
|
+
rights: "write"
|
|
4579
|
+
}).first();
|
|
4580
|
+
return !!grant;
|
|
4581
|
+
}
|
|
4582
|
+
return false;
|
|
4583
|
+
};
|
|
4584
|
+
|
|
4585
|
+
// src/templates/tools/kb-editor-config.ts
|
|
4586
|
+
import { z as z9 } from "zod";
|
|
4587
|
+
var KB_EDITOR_TOOL_ID = "knowledge_base_editor";
|
|
4588
|
+
var permissionsSchema = z9.object({
|
|
4589
|
+
create: z9.boolean().catch(false).default(false),
|
|
4590
|
+
update: z9.boolean().catch(false).default(false)
|
|
4591
|
+
});
|
|
4592
|
+
var emptyConfig = () => ({
|
|
4593
|
+
enabled: false,
|
|
4594
|
+
knowledgeBases: {},
|
|
4595
|
+
skipApproval: false
|
|
4596
|
+
});
|
|
4597
|
+
var parseKbEditorConfig = (tools) => {
|
|
4598
|
+
let entries = tools;
|
|
4599
|
+
if (typeof entries === "string") {
|
|
4600
|
+
try {
|
|
4601
|
+
entries = JSON.parse(entries);
|
|
4602
|
+
} catch {
|
|
4603
|
+
return emptyConfig();
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
if (!Array.isArray(entries)) {
|
|
4607
|
+
return emptyConfig();
|
|
4608
|
+
}
|
|
4609
|
+
const entry = entries.find((t) => t?.id === KB_EDITOR_TOOL_ID);
|
|
4610
|
+
if (!entry) {
|
|
4611
|
+
return emptyConfig();
|
|
4612
|
+
}
|
|
4613
|
+
const rawValue = (name) => {
|
|
4614
|
+
const row = Array.isArray(entry.config) ? entry.config.find((c) => c?.name === name) : void 0;
|
|
4615
|
+
return row?.value ?? row?.variable ?? row?.default;
|
|
4616
|
+
};
|
|
4617
|
+
let kbsRaw = rawValue("knowledge_bases");
|
|
4618
|
+
if (typeof kbsRaw === "string" && kbsRaw) {
|
|
4619
|
+
try {
|
|
4620
|
+
kbsRaw = JSON.parse(kbsRaw);
|
|
4621
|
+
} catch {
|
|
4622
|
+
kbsRaw = {};
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
const knowledgeBases = {};
|
|
4626
|
+
if (kbsRaw && typeof kbsRaw === "object" && !Array.isArray(kbsRaw)) {
|
|
4627
|
+
for (const [contextId, value] of Object.entries(kbsRaw)) {
|
|
4628
|
+
const parsed = permissionsSchema.safeParse(value);
|
|
4629
|
+
if (parsed.success && (parsed.data.create || parsed.data.update)) {
|
|
4630
|
+
knowledgeBases[contextId] = parsed.data;
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
const skipRaw = rawValue("skip_approval");
|
|
4635
|
+
const skipApproval = skipRaw === true || skipRaw === "true" || skipRaw === 1;
|
|
4636
|
+
return { enabled: true, knowledgeBases, skipApproval };
|
|
4637
|
+
};
|
|
4638
|
+
|
|
4639
|
+
// src/templates/tools/context-write-tools.ts
|
|
4640
|
+
var MAX_CONTEXT_SEGMENT = 68;
|
|
4641
|
+
var RESERVED_INPUT_KEYS = /* @__PURE__ */ new Set([
|
|
4642
|
+
"model",
|
|
4643
|
+
"user",
|
|
4644
|
+
"contexts",
|
|
4645
|
+
"memory",
|
|
4646
|
+
"req",
|
|
4647
|
+
"upload",
|
|
4648
|
+
"sessionID",
|
|
4649
|
+
"sessionItems",
|
|
4650
|
+
"providerapikey",
|
|
4651
|
+
"allExuluTools",
|
|
4652
|
+
"currentTools",
|
|
4653
|
+
"exuluConfig",
|
|
4654
|
+
"toolVariablesConfig",
|
|
4655
|
+
"oauth"
|
|
4656
|
+
]);
|
|
4657
|
+
var buildWriteSchema = (context, mode) => {
|
|
4658
|
+
const shape = {};
|
|
4659
|
+
const contentKeys = [];
|
|
4660
|
+
const addContent = (key, schema, required) => {
|
|
4661
|
+
shape[key] = required && mode === "create" ? schema : schema.optional();
|
|
4662
|
+
contentKeys.push(key);
|
|
4663
|
+
};
|
|
4664
|
+
if (mode === "update") {
|
|
4665
|
+
shape["id"] = z10.string().optional().describe("The id of the item to update.");
|
|
4666
|
+
shape["external_id"] = z10.string().optional().describe("The external_id of the item to update, if the id is unknown. Lookup only \u2014 it is never changed.");
|
|
4667
|
+
}
|
|
4668
|
+
addContent("name", z10.string().describe("The name of the item."), true);
|
|
4669
|
+
addContent("description", z10.string().describe("A description of the item."), false);
|
|
4670
|
+
addContent("tags", z10.array(z10.string()).describe("Tags for the item."), false);
|
|
4671
|
+
if (mode === "create") {
|
|
4672
|
+
addContent(
|
|
4673
|
+
"external_id",
|
|
4674
|
+
z10.string().describe("An optional external identifier for the item, e.g. an id from a source system."),
|
|
4675
|
+
false
|
|
4676
|
+
);
|
|
4677
|
+
}
|
|
4678
|
+
for (const field of context.fields ?? []) {
|
|
4679
|
+
if (field.type === "file" || field.type === "uuid") continue;
|
|
4680
|
+
if (field.calculated === true || field.editable === false) continue;
|
|
4681
|
+
if (field.hidden === true) continue;
|
|
4682
|
+
if (RESERVED_INPUT_KEYS.has(field.name)) continue;
|
|
4683
|
+
let schema;
|
|
4684
|
+
switch (field.type) {
|
|
4685
|
+
case "enum":
|
|
4686
|
+
schema = z10.string().describe(
|
|
4687
|
+
`The ${field.name} of the item. Must be one of: ${(field.enumValues ?? []).join(", ")}`
|
|
4688
|
+
);
|
|
4689
|
+
break;
|
|
4690
|
+
case "json":
|
|
4691
|
+
schema = z10.string().describe(`The ${field.name} of the item, as a valid JSON string.`);
|
|
4692
|
+
break;
|
|
4693
|
+
case "markdown":
|
|
4694
|
+
schema = z10.string().describe(`The ${field.name} of the item, as a valid Markdown string.`);
|
|
4695
|
+
break;
|
|
4696
|
+
case "date":
|
|
4697
|
+
schema = z10.string().describe(`The ${field.name} of the item, as an ISO-8601 date string.`);
|
|
4698
|
+
break;
|
|
4699
|
+
case "number":
|
|
4700
|
+
schema = z10.number().describe(`The ${field.name} of the item.`);
|
|
4701
|
+
break;
|
|
4702
|
+
case "boolean":
|
|
4703
|
+
schema = z10.boolean().describe(`The ${field.name} of the item.`);
|
|
4704
|
+
break;
|
|
4705
|
+
default:
|
|
4706
|
+
schema = z10.string().describe(`The ${field.name} of the item.`);
|
|
4707
|
+
break;
|
|
4708
|
+
}
|
|
4709
|
+
addContent(field.name, schema, field.required === true);
|
|
4710
|
+
}
|
|
4711
|
+
return { shape, contentKeys };
|
|
4712
|
+
};
|
|
4713
|
+
var canonicalizeEnumFields = (context, params) => {
|
|
4714
|
+
for (const field of context.fields ?? []) {
|
|
4715
|
+
if (field.type !== "enum" || !field.enumValues?.length) continue;
|
|
4716
|
+
const raw = params[field.name];
|
|
4717
|
+
if (raw === void 0 || raw === null || raw === "") continue;
|
|
4718
|
+
const rawStr = String(raw);
|
|
4719
|
+
const canonical = field.enumValues.find((v) => v.toUpperCase() === rawStr.toUpperCase());
|
|
4720
|
+
if (canonical === void 0) {
|
|
4721
|
+
return `Invalid value "${rawStr}" for field "${field.name}". Allowed values: ${field.enumValues.join(", ")}.`;
|
|
4722
|
+
}
|
|
4723
|
+
params[field.name] = canonical;
|
|
4724
|
+
}
|
|
4725
|
+
return void 0;
|
|
4726
|
+
};
|
|
4727
|
+
var pickContent = (params, contentKeys) => {
|
|
4728
|
+
const item = {};
|
|
4729
|
+
for (const key of contentKeys) {
|
|
4730
|
+
if (params[key] !== void 0) {
|
|
4731
|
+
item[key] = params[key];
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
return item;
|
|
4735
|
+
};
|
|
4736
|
+
var jobNote = (job) => job ? ` Processing/embeddings queued (job: ${job}); changes become searchable when the job completes.` : "";
|
|
4737
|
+
var createContextWriteTools = (context, perms, skipApproval) => {
|
|
4738
|
+
const tools = [];
|
|
4739
|
+
const segment = sanitizeName(context.id).slice(0, MAX_CONTEXT_SEGMENT);
|
|
4740
|
+
const contextLabel = context.description ? ` ${context.description}` : "";
|
|
4741
|
+
if (perms.create) {
|
|
4742
|
+
const { shape, contentKeys } = buildWriteSchema(context, "create");
|
|
4743
|
+
tools.push(
|
|
4744
|
+
new ExuluTool({
|
|
4745
|
+
id: `create_${segment}_item`,
|
|
4746
|
+
name: `Create ${context.name} item`,
|
|
4747
|
+
category: "knowledge_base_editing",
|
|
4748
|
+
description: `Create a new item in the "${context.name}" knowledge base.${contextLabel}`,
|
|
4749
|
+
type: "function",
|
|
4750
|
+
inputSchema: z10.object(shape),
|
|
4751
|
+
config: [],
|
|
4752
|
+
needsApproval: !skipApproval,
|
|
4753
|
+
execute: async (params) => {
|
|
4754
|
+
const { user, exuluConfig } = params;
|
|
4755
|
+
if (!user?.id) {
|
|
4756
|
+
return { result: "Knowledge base writes require an authenticated user." };
|
|
4757
|
+
}
|
|
4758
|
+
try {
|
|
4759
|
+
const enumError = canonicalizeEnumFields(context, params);
|
|
4760
|
+
if (enumError) {
|
|
4761
|
+
return { result: enumError };
|
|
4762
|
+
}
|
|
4763
|
+
const item = pickContent(params, contentKeys);
|
|
4764
|
+
item.created_by = String(user.id);
|
|
4765
|
+
const { item: created, job } = await context.createItem(
|
|
4766
|
+
item,
|
|
4767
|
+
exuluConfig,
|
|
4768
|
+
user?.id,
|
|
4769
|
+
user?.role?.id,
|
|
4770
|
+
false
|
|
4771
|
+
);
|
|
4772
|
+
if (!created?.id) {
|
|
4773
|
+
return { result: `Failed to create item in "${context.name}".` };
|
|
4774
|
+
}
|
|
4775
|
+
return {
|
|
4776
|
+
result: `Created item ${created.id} in knowledge base "${context.name}".${jobNote(job)}`
|
|
4777
|
+
};
|
|
4778
|
+
} catch (error) {
|
|
4779
|
+
console.error(`[EXULU] Error creating item in context ${context.id}`, error);
|
|
4780
|
+
return {
|
|
4781
|
+
result: `Failed to create item in "${context.name}": ${error instanceof Error ? error.message : String(error)}`
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
})
|
|
4786
|
+
);
|
|
4787
|
+
}
|
|
4788
|
+
if (perms.update) {
|
|
4789
|
+
const { shape, contentKeys } = buildWriteSchema(context, "update");
|
|
4790
|
+
const NOT_FOUND = `Item not found in "${context.name}" or you don't have write access to it.`;
|
|
4791
|
+
tools.push(
|
|
4792
|
+
new ExuluTool({
|
|
4793
|
+
id: `update_${segment}_item`,
|
|
4794
|
+
name: `Update ${context.name} item`,
|
|
4795
|
+
category: "knowledge_base_editing",
|
|
4796
|
+
description: `Update an existing item in the "${context.name}" knowledge base. Provide the item's id (or external_id) plus only the fields to change; omitted fields keep their values.`,
|
|
4797
|
+
type: "function",
|
|
4798
|
+
inputSchema: z10.object(shape),
|
|
4799
|
+
config: [],
|
|
4800
|
+
needsApproval: !skipApproval,
|
|
4801
|
+
execute: async (params) => {
|
|
4802
|
+
const { user, exuluConfig } = params;
|
|
4803
|
+
if (!user?.id) {
|
|
4804
|
+
return { result: "Knowledge base writes require an authenticated user." };
|
|
4805
|
+
}
|
|
4806
|
+
try {
|
|
4807
|
+
if (!params.id && !params.external_id) {
|
|
4808
|
+
return { result: "Provide the id or external_id of the item to update." };
|
|
4809
|
+
}
|
|
4810
|
+
const existing = await context.getItem({
|
|
4811
|
+
item: { id: params.id, external_id: params.external_id }
|
|
4812
|
+
});
|
|
4813
|
+
if (!existing?.id) {
|
|
4814
|
+
return { result: NOT_FOUND };
|
|
4815
|
+
}
|
|
4816
|
+
const allowed = await checkItemWriteAccess(context, existing, user);
|
|
4817
|
+
if (!allowed) {
|
|
4818
|
+
return { result: NOT_FOUND };
|
|
4819
|
+
}
|
|
4820
|
+
const enumError = canonicalizeEnumFields(context, params);
|
|
4821
|
+
if (enumError) {
|
|
4822
|
+
return { result: enumError };
|
|
4823
|
+
}
|
|
4824
|
+
const patch = pickContent(params, contentKeys);
|
|
4825
|
+
if (Object.keys(patch).length === 0) {
|
|
4826
|
+
return { result: "No fields to update were provided." };
|
|
4827
|
+
}
|
|
4828
|
+
patch.id = existing.id;
|
|
4829
|
+
const { job } = await context.updateItem(patch, exuluConfig, user?.id, user?.role?.id);
|
|
4830
|
+
const fresh = await context.getItem({ item: { id: existing.id } });
|
|
4831
|
+
const summary = { id: existing.id };
|
|
4832
|
+
for (const key of contentKeys) {
|
|
4833
|
+
if (fresh?.[key] !== void 0 && fresh?.[key] !== null) {
|
|
4834
|
+
summary[key] = fresh[key];
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
return {
|
|
4838
|
+
result: `Updated item ${existing.id} in knowledge base "${context.name}".${jobNote(job)}
|
|
4839
|
+
Current item: ${JSON.stringify(summary)}`
|
|
4840
|
+
};
|
|
4841
|
+
} catch (error) {
|
|
4842
|
+
console.error(`[EXULU] Error updating item in context ${context.id}`, error);
|
|
4843
|
+
return {
|
|
4844
|
+
result: `Failed to update item in "${context.name}": ${error instanceof Error ? error.message : String(error)}`
|
|
4845
|
+
};
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
})
|
|
4849
|
+
);
|
|
4850
|
+
}
|
|
4851
|
+
return tools;
|
|
4852
|
+
};
|
|
4853
|
+
var createKbEditorPickerTool = () => new ExuluTool({
|
|
4854
|
+
id: KB_EDITOR_TOOL_ID,
|
|
4855
|
+
name: "Knowledge base editor",
|
|
4856
|
+
category: "default",
|
|
4857
|
+
description: "Let this agent create or update items in selected knowledge bases during chat. Configure per knowledge base whether the agent may create and/or update items.",
|
|
4858
|
+
type: "function",
|
|
4859
|
+
inputSchema: z10.object({}),
|
|
4860
|
+
config: [
|
|
4861
|
+
{
|
|
4862
|
+
name: "knowledge_bases",
|
|
4863
|
+
description: "JSON record of context id to { create: boolean, update: boolean }. Contexts absent here get no write access.",
|
|
4864
|
+
type: "json"
|
|
4865
|
+
},
|
|
4866
|
+
{
|
|
4867
|
+
name: "skip_approval",
|
|
4868
|
+
description: "Run knowledge base writes without asking for approval in the chat.",
|
|
4869
|
+
type: "boolean",
|
|
4870
|
+
default: false
|
|
4871
|
+
}
|
|
4872
|
+
],
|
|
4873
|
+
execute: async () => ({
|
|
4874
|
+
result: "This entry is configuration-only; Exulu expands it into per-context create/update tools at runtime."
|
|
4875
|
+
})
|
|
4876
|
+
});
|
|
4877
|
+
var collectKbWriteTools = (agent, contexts) => {
|
|
4878
|
+
if (!agent?.tools || !contexts?.length) {
|
|
4879
|
+
return [];
|
|
4880
|
+
}
|
|
4881
|
+
const config = parseKbEditorConfig(agent.tools);
|
|
4882
|
+
if (!config.enabled) {
|
|
4883
|
+
return [];
|
|
4884
|
+
}
|
|
4885
|
+
const tools = [];
|
|
4886
|
+
for (const [contextId, perms] of Object.entries(config.knowledgeBases)) {
|
|
4887
|
+
const context = contexts.find((c) => c.id === contextId);
|
|
4888
|
+
if (!context) {
|
|
4889
|
+
continue;
|
|
4890
|
+
}
|
|
4891
|
+
tools.push(...createContextWriteTools(context, perms, config.skipApproval));
|
|
4892
|
+
}
|
|
4893
|
+
return tools;
|
|
4894
|
+
};
|
|
4895
|
+
|
|
4336
4896
|
// ee/invoke-skills/create-sandbox.ts
|
|
4337
4897
|
import {
|
|
4338
4898
|
SandboxManager
|
|
@@ -5468,7 +6028,7 @@ ${body}`
|
|
|
5468
6028
|
// ee/invoke-skills/create-sandbox.ts
|
|
5469
6029
|
import { createBashTool } from "bash-tool";
|
|
5470
6030
|
import { tool as tool2 } from "ai";
|
|
5471
|
-
import { z as
|
|
6031
|
+
import { z as z11 } from "zod";
|
|
5472
6032
|
import CryptoJS4 from "crypto-js";
|
|
5473
6033
|
var getAllExuluVariables = async () => {
|
|
5474
6034
|
const { db: db2 } = await postgresClient();
|
|
@@ -5886,9 +6446,9 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
5886
6446
|
});
|
|
5887
6447
|
const writeFileTool = tool2({
|
|
5888
6448
|
description: 'Write content to a file in the sandbox. Creates parent directories if needed. Paths are always resolved against the session sandbox root \u2014 both relative paths ("skills/foo.md") and leading-slash paths ("/skills/foo.md") work and reach the same file. When the path is under the session artifact tree, the file is also uploaded to S3 and a short-lived presigned URL is returned in the tool output.',
|
|
5889
|
-
inputSchema:
|
|
5890
|
-
path:
|
|
5891
|
-
content:
|
|
6449
|
+
inputSchema: z11.object({
|
|
6450
|
+
path: z11.string().describe("The path where the file should be written. Relative paths and leading-slash paths are both resolved against the session sandbox root."),
|
|
6451
|
+
content: z11.string().describe("The content to write to the file")
|
|
5892
6452
|
}),
|
|
5893
6453
|
execute: async ({ path, content }) => {
|
|
5894
6454
|
const resolvedPath = resolveSessionPath(path, sessionDir);
|
|
@@ -5907,8 +6467,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
5907
6467
|
});
|
|
5908
6468
|
const readFileTool = tool2({
|
|
5909
6469
|
description: 'Read the contents of a file from the sandbox. Paths are always resolved against the session sandbox root \u2014 both relative paths ("skills/foo.md") and leading-slash paths ("/skills/foo.md") work and reach the same file. If the file does not exist, the error message is surfaced verbatim.',
|
|
5910
|
-
inputSchema:
|
|
5911
|
-
path:
|
|
6470
|
+
inputSchema: z11.object({
|
|
6471
|
+
path: z11.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
|
|
5912
6472
|
}),
|
|
5913
6473
|
execute: async ({ path }) => {
|
|
5914
6474
|
const resolvedPath = resolveSessionPath(path, sessionDir);
|
|
@@ -5919,8 +6479,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
5919
6479
|
const originalBashTool = tools.bash;
|
|
5920
6480
|
const bashTool = tool2({
|
|
5921
6481
|
description: originalBashTool.description ?? "",
|
|
5922
|
-
inputSchema:
|
|
5923
|
-
command:
|
|
6482
|
+
inputSchema: z11.object({
|
|
6483
|
+
command: z11.string().describe("The bash command to execute.")
|
|
5924
6484
|
}),
|
|
5925
6485
|
execute: async (args, opts) => {
|
|
5926
6486
|
const before = persistenceEnabled ? await snapshotSessionArtifacts() : null;
|
|
@@ -6167,8 +6727,27 @@ var guardExtractedFileText = async (filename, text, ctx) => {
|
|
|
6167
6727
|
${notice}`;
|
|
6168
6728
|
};
|
|
6169
6729
|
|
|
6730
|
+
// src/exulu/auth/scrub-text.ts
|
|
6731
|
+
var credentialScrubText = (provider, fieldLabels) => `A secure credential form for provider "${provider}"` + (fieldLabels.length ? ` (fields: ${fieldLabels.join(", ")})` : "") + ` is shown to the user in the chat UI. Never ask for these values in chat. After the user confirms saving, call the tool again.`;
|
|
6732
|
+
var SCRUBBED_CREDENTIAL_TEXT = "A secure credential form was shown to the user in the chat UI. Never ask for credential values in chat. After the user confirms saving, call the tool again.";
|
|
6733
|
+
var SCRUBBED_OAUTH_TEXT = "Authorization is required. A Connect button was shown to the user in the chat UI. Do not relay any URL in chat. After the user confirms connecting, call the tool again.";
|
|
6734
|
+
|
|
6735
|
+
// src/templates/tools/auth-tool-model-output.ts
|
|
6736
|
+
var buildAuthToolModelOutput = (tool3) => ({ output }) => {
|
|
6737
|
+
if (output && typeof output === "object" && output.credentialRequest) {
|
|
6738
|
+
const auth = tool3.authentication;
|
|
6739
|
+
const labels = auth?.authType === "user_credentials" ? auth.fields.map((f) => f.label) : [];
|
|
6740
|
+
const provider = output.credentialRequest.provider ?? (auth && "provider" in auth ? auth.provider : "unknown");
|
|
6741
|
+
return { type: "text", value: credentialScrubText(provider, labels) };
|
|
6742
|
+
}
|
|
6743
|
+
if (output && typeof output === "object" && output.oauth?.authorizationUrl) {
|
|
6744
|
+
return { type: "text", value: SCRUBBED_OAUTH_TEXT };
|
|
6745
|
+
}
|
|
6746
|
+
return { type: "json", value: output ?? null };
|
|
6747
|
+
};
|
|
6748
|
+
|
|
6170
6749
|
// src/templates/tools/session-file-read-tool.ts
|
|
6171
|
-
import { z as
|
|
6750
|
+
import { z as z12 } from "zod";
|
|
6172
6751
|
var DEFAULT_LIMIT = 250;
|
|
6173
6752
|
var MAX_CONTENT_CHARS = 16e3;
|
|
6174
6753
|
var createSessionFileReadTool = ({
|
|
@@ -6220,10 +6799,10 @@ var createSessionFileReadTool = ({
|
|
|
6220
6799
|
name: "read_session_file",
|
|
6221
6800
|
needsApproval: false,
|
|
6222
6801
|
description: "Read a line range from a file stored in this session's files \u2014 including offloaded tool outputs (tool-output-*.txt) and uploaded documents. Use offset (1-based line number) and limit to page through large files instead of reading everything at once.",
|
|
6223
|
-
inputSchema:
|
|
6224
|
-
filename:
|
|
6225
|
-
offset:
|
|
6226
|
-
limit:
|
|
6802
|
+
inputSchema: z12.object({
|
|
6803
|
+
filename: z12.string().describe('Exact session file name as referenced in a truncation notice, e.g. "tool-output-web_search-a1b2c3d4.txt"'),
|
|
6804
|
+
offset: z12.number().int().min(1).optional().describe("1-based first line to read (default 1)"),
|
|
6805
|
+
limit: z12.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT})`)
|
|
6227
6806
|
}),
|
|
6228
6807
|
type: "function",
|
|
6229
6808
|
category: "session",
|
|
@@ -6236,7 +6815,7 @@ var createSessionFileReadTool = ({
|
|
|
6236
6815
|
};
|
|
6237
6816
|
|
|
6238
6817
|
// src/templates/tools/parse-document-tool.ts
|
|
6239
|
-
import { z as
|
|
6818
|
+
import { z as z13 } from "zod";
|
|
6240
6819
|
import { extname } from "path";
|
|
6241
6820
|
import { parseOfficeAsync } from "officeparser";
|
|
6242
6821
|
|
|
@@ -6399,11 +6978,11 @@ ${text.trim()}`).join("\n");
|
|
|
6399
6978
|
name: "parse_document",
|
|
6400
6979
|
needsApproval: false,
|
|
6401
6980
|
description: `Extract the text of an uploaded PDF or Office document from this session's files, with "--- page N ---" markers for PDFs so you can locate content by page. Free and fast (no OCR): works only on documents with a real text layer. To SEE a page or an image inside a document, use view_document_page.`,
|
|
6402
|
-
inputSchema:
|
|
6403
|
-
filename:
|
|
6404
|
-
pages:
|
|
6405
|
-
offset:
|
|
6406
|
-
limit:
|
|
6981
|
+
inputSchema: z13.object({
|
|
6982
|
+
filename: z13.string().describe('Exact session file name, e.g. "report.pdf"'),
|
|
6983
|
+
pages: z13.string().optional().describe('PDF page or range to extract, e.g. "2" or "1-5" (default: all pages) (PDF only)'),
|
|
6984
|
+
offset: z13.number().int().min(1).optional().describe("1-based first output line to read (default 1)"),
|
|
6985
|
+
limit: z13.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT2})`)
|
|
6407
6986
|
}),
|
|
6408
6987
|
type: "function",
|
|
6409
6988
|
category: "session",
|
|
@@ -6416,7 +6995,7 @@ ${text.trim()}`).join("\n");
|
|
|
6416
6995
|
};
|
|
6417
6996
|
|
|
6418
6997
|
// src/templates/tools/view-document-page-tool.ts
|
|
6419
|
-
import { z as
|
|
6998
|
+
import { z as z14 } from "zod";
|
|
6420
6999
|
import { extname as extname3 } from "path";
|
|
6421
7000
|
|
|
6422
7001
|
// src/sessions/pdf-preview-cache.ts
|
|
@@ -6686,9 +7265,9 @@ var createViewDocumentPageTool = ({
|
|
|
6686
7265
|
name: "view_document_page",
|
|
6687
7266
|
needsApproval: false,
|
|
6688
7267
|
description: "LOOK at a page of an uploaded PDF/Office document, or at an uploaded image, from this session's files. The rendered image is attached as a user message directly after this tool result so you can visually analyze photos, charts, scans, and layouts. Use parse_document first to find which page you need. Requires a vision-capable model.",
|
|
6689
|
-
inputSchema:
|
|
6690
|
-
filename:
|
|
6691
|
-
page:
|
|
7268
|
+
inputSchema: z14.object({
|
|
7269
|
+
filename: z14.string().describe('Exact session file name, e.g. "report.pdf" or "screenshot.png"'),
|
|
7270
|
+
page: z14.number().int().min(1).optional().describe("Page number to render (default 1; ignored for image files)")
|
|
6692
7271
|
}),
|
|
6693
7272
|
type: "function",
|
|
6694
7273
|
category: "session",
|
|
@@ -6863,6 +7442,11 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
6863
7442
|
currentTools.push(createNewMemoryTool);
|
|
6864
7443
|
}
|
|
6865
7444
|
}
|
|
7445
|
+
for (const kbWriteTool of collectKbWriteTools(agent, contexts)) {
|
|
7446
|
+
if (!disabled.has(kbWriteTool.id)) {
|
|
7447
|
+
currentTools.push(kbWriteTool);
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
6866
7450
|
console.log("[EXULU] Convert tools array to object, session items", sessionItems);
|
|
6867
7451
|
if (sessionItems) {
|
|
6868
7452
|
const sessionItemsRetrievalTool = await createSessionItemsRetrievalTool({
|
|
@@ -7015,6 +7599,10 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
7015
7599
|
// Vercel AI SDK uses the sanitized tool name as the key, so this matches.
|
|
7016
7600
|
needsApproval: approvedTools?.includes("tool-" + cur.name) || !cur.needsApproval ? false : true,
|
|
7017
7601
|
// todo make configurable
|
|
7602
|
+
// Auth-wrapped tools: the model sees scrub text instead of the
|
|
7603
|
+
// credentialRequest/oauth payload; the UI stream keeps the raw
|
|
7604
|
+
// output (spec 2026-07-22 §1.2).
|
|
7605
|
+
...cur.authentication ? { toModelOutput: buildAuthToolModelOutput(cur) } : {},
|
|
7018
7606
|
async *execute(inputs, options) {
|
|
7019
7607
|
console.log(
|
|
7020
7608
|
"[EXULU] Executing tool",
|
|
@@ -7167,6 +7755,8 @@ export {
|
|
|
7167
7755
|
getS3SignedUploadUrl,
|
|
7168
7756
|
createUppyRoutes,
|
|
7169
7757
|
sanitizeName,
|
|
7758
|
+
getTableName,
|
|
7759
|
+
getChunksTableName,
|
|
7170
7760
|
LITELLM_UI_PATH,
|
|
7171
7761
|
isLiteLLMEnabled,
|
|
7172
7762
|
setLiteLLMPackageRoot,
|
|
@@ -7195,12 +7785,18 @@ export {
|
|
|
7195
7785
|
ResolveModelError,
|
|
7196
7786
|
resolveModel,
|
|
7197
7787
|
exuluApp,
|
|
7198
|
-
|
|
7199
|
-
|
|
7788
|
+
authRegistry,
|
|
7789
|
+
encrypt,
|
|
7790
|
+
decrypt,
|
|
7791
|
+
credentialStore,
|
|
7200
7792
|
OAUTH_CALLBACK_PATH,
|
|
7201
7793
|
decryptOauthState,
|
|
7202
7794
|
exchangeCodeForTokens,
|
|
7795
|
+
verifyCredentialNonce,
|
|
7796
|
+
CredentialInvalidError,
|
|
7203
7797
|
sanitizeToolName,
|
|
7798
|
+
KB_EDITOR_TOOL_ID,
|
|
7799
|
+
createKbEditorPickerTool,
|
|
7204
7800
|
reportSystemDependencies,
|
|
7205
7801
|
downloadKeyIntoSandbox,
|
|
7206
7802
|
truncateToolOutput,
|
|
@@ -7214,6 +7810,8 @@ export {
|
|
|
7214
7810
|
ContextCompactionRequiredError,
|
|
7215
7811
|
mapStreamErrorMessage,
|
|
7216
7812
|
guardExtractedFileText,
|
|
7813
|
+
SCRUBBED_CREDENTIAL_TEXT,
|
|
7814
|
+
SCRUBBED_OAUTH_TEXT,
|
|
7217
7815
|
PreviewRenderError,
|
|
7218
7816
|
getPdfPreviewBytes,
|
|
7219
7817
|
imageAttachmentGuard,
|