@infuro/cms-core 1.0.19 → 1.0.20
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/admin.cjs +534 -11
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.js +564 -23
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +727 -36
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +705 -18
- package/dist/api.js.map +1 -1
- package/dist/{index-GMn7-9PX.d.ts → index--GBYw5JE.d.ts} +84 -2
- package/dist/{index-D2C1O9b4.d.cts → index-DGtM2Gsk.d.cts} +84 -2
- package/dist/index.cjs +1701 -713
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -13
- package/dist/index.d.ts +99 -13
- package/dist/index.js +1554 -574
- package/dist/index.js.map +1 -1
- package/dist/migrations/1775300000000-LlmAgents.ts +57 -0
- package/dist/migrations/1775300000001-LlmAgentsValidationRulesText.ts +43 -0
- package/dist/migrations/1775300000002-SeedLlmAgentsPermissions.ts +33 -0
- package/dist/migrations/1775300000003-LlmAgentKnowledgeDocuments.ts +50 -0
- package/dist/migrations/1775400000000-KnowledgeBaseVectorDimension384.ts +32 -0
- package/package.json +8 -6
package/dist/api.cjs
CHANGED
|
@@ -29,6 +29,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
mod
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
33
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
34
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
35
|
+
if (decorator = decorators[i])
|
|
36
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
37
|
+
if (kind && result) __defProp(target, key, result);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
32
40
|
|
|
33
41
|
// src/plugins/erp/erp-queue.ts
|
|
34
42
|
async function queueErp(cms, payload) {
|
|
@@ -432,6 +440,7 @@ __export(api_exports, {
|
|
|
432
440
|
createForgotPasswordHandler: () => createForgotPasswordHandler,
|
|
433
441
|
createFormBySlugHandler: () => createFormBySlugHandler,
|
|
434
442
|
createInviteAcceptHandler: () => createInviteAcceptHandler,
|
|
443
|
+
createLlmAgentKnowledgeHandlers: () => createLlmAgentKnowledgeHandlers,
|
|
435
444
|
createMediaZipExtractHandler: () => createMediaZipExtractHandler,
|
|
436
445
|
createSetPasswordHandler: () => createSetPasswordHandler,
|
|
437
446
|
createSettingsApiHandlers: () => createSettingsApiHandlers,
|
|
@@ -441,7 +450,11 @@ __export(api_exports, {
|
|
|
441
450
|
createUserAvatarHandler: () => createUserAvatarHandler,
|
|
442
451
|
createUserProfileHandler: () => createUserProfileHandler,
|
|
443
452
|
createUsersApiHandlers: () => createUsersApiHandlers,
|
|
444
|
-
getPublicSettingsGroup: () => getPublicSettingsGroup
|
|
453
|
+
getPublicSettingsGroup: () => getPublicSettingsGroup,
|
|
454
|
+
mergeGuardrailsIntoSystemPrompt: () => mergeGuardrailsIntoSystemPrompt,
|
|
455
|
+
parseLlmAgentValidationRules: () => parseLlmAgentValidationRules,
|
|
456
|
+
validateUserMessageAgainstAgentRules: () => validateUserMessageAgainstAgentRules,
|
|
457
|
+
validateUserMessageAgainstStructuredRules: () => validateUserMessageAgainstStructuredRules
|
|
445
458
|
});
|
|
446
459
|
module.exports = __toCommonJS(api_exports);
|
|
447
460
|
|
|
@@ -1364,7 +1377,7 @@ function createUserAuthApiRouter(config) {
|
|
|
1364
1377
|
}
|
|
1365
1378
|
|
|
1366
1379
|
// src/api/cms-handlers.ts
|
|
1367
|
-
var
|
|
1380
|
+
var import_typeorm5 = require("typeorm");
|
|
1368
1381
|
init_email_queue();
|
|
1369
1382
|
init_erp_queue();
|
|
1370
1383
|
|
|
@@ -1384,6 +1397,86 @@ async function assertCaptchaOk(getCms, body, req, json) {
|
|
|
1384
1397
|
return json({ error: result.message }, { status: result.status });
|
|
1385
1398
|
}
|
|
1386
1399
|
|
|
1400
|
+
// src/entities/llm-agent.entity.ts
|
|
1401
|
+
var import_typeorm3 = require("typeorm");
|
|
1402
|
+
var LlmAgent = class {
|
|
1403
|
+
id;
|
|
1404
|
+
name;
|
|
1405
|
+
slug;
|
|
1406
|
+
systemInstruction;
|
|
1407
|
+
model;
|
|
1408
|
+
temperature;
|
|
1409
|
+
maxTokens;
|
|
1410
|
+
validationRules;
|
|
1411
|
+
enabled;
|
|
1412
|
+
createdAt;
|
|
1413
|
+
updatedAt;
|
|
1414
|
+
deletedAt;
|
|
1415
|
+
deleted;
|
|
1416
|
+
createdBy;
|
|
1417
|
+
updatedBy;
|
|
1418
|
+
deletedBy;
|
|
1419
|
+
};
|
|
1420
|
+
__decorateClass([
|
|
1421
|
+
(0, import_typeorm3.PrimaryGeneratedColumn)()
|
|
1422
|
+
], LlmAgent.prototype, "id", 2);
|
|
1423
|
+
__decorateClass([
|
|
1424
|
+
(0, import_typeorm3.Column)("varchar")
|
|
1425
|
+
], LlmAgent.prototype, "name", 2);
|
|
1426
|
+
__decorateClass([
|
|
1427
|
+
(0, import_typeorm3.Column)("varchar")
|
|
1428
|
+
], LlmAgent.prototype, "slug", 2);
|
|
1429
|
+
__decorateClass([
|
|
1430
|
+
(0, import_typeorm3.Column)("text", { name: "system_instruction", default: "" })
|
|
1431
|
+
], LlmAgent.prototype, "systemInstruction", 2);
|
|
1432
|
+
__decorateClass([
|
|
1433
|
+
(0, import_typeorm3.Column)("varchar", { nullable: true })
|
|
1434
|
+
], LlmAgent.prototype, "model", 2);
|
|
1435
|
+
__decorateClass([
|
|
1436
|
+
(0, import_typeorm3.Column)("double precision", { name: "temperature", nullable: true })
|
|
1437
|
+
], LlmAgent.prototype, "temperature", 2);
|
|
1438
|
+
__decorateClass([
|
|
1439
|
+
(0, import_typeorm3.Column)("int", { name: "max_tokens", nullable: true })
|
|
1440
|
+
], LlmAgent.prototype, "maxTokens", 2);
|
|
1441
|
+
__decorateClass([
|
|
1442
|
+
(0, import_typeorm3.Column)("text", { name: "validation_rules", nullable: true })
|
|
1443
|
+
], LlmAgent.prototype, "validationRules", 2);
|
|
1444
|
+
__decorateClass([
|
|
1445
|
+
(0, import_typeorm3.Column)("boolean", { default: true })
|
|
1446
|
+
], LlmAgent.prototype, "enabled", 2);
|
|
1447
|
+
__decorateClass([
|
|
1448
|
+
(0, import_typeorm3.Column)({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
1449
|
+
], LlmAgent.prototype, "createdAt", 2);
|
|
1450
|
+
__decorateClass([
|
|
1451
|
+
(0, import_typeorm3.Column)({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
1452
|
+
], LlmAgent.prototype, "updatedAt", 2);
|
|
1453
|
+
__decorateClass([
|
|
1454
|
+
(0, import_typeorm3.Column)({ type: "timestamp", nullable: true })
|
|
1455
|
+
], LlmAgent.prototype, "deletedAt", 2);
|
|
1456
|
+
__decorateClass([
|
|
1457
|
+
(0, import_typeorm3.Column)("boolean", { default: false })
|
|
1458
|
+
], LlmAgent.prototype, "deleted", 2);
|
|
1459
|
+
__decorateClass([
|
|
1460
|
+
(0, import_typeorm3.Column)("int", { nullable: true })
|
|
1461
|
+
], LlmAgent.prototype, "createdBy", 2);
|
|
1462
|
+
__decorateClass([
|
|
1463
|
+
(0, import_typeorm3.Column)("int", { nullable: true })
|
|
1464
|
+
], LlmAgent.prototype, "updatedBy", 2);
|
|
1465
|
+
__decorateClass([
|
|
1466
|
+
(0, import_typeorm3.Column)("int", { nullable: true })
|
|
1467
|
+
], LlmAgent.prototype, "deletedBy", 2);
|
|
1468
|
+
LlmAgent = __decorateClass([
|
|
1469
|
+
(0, import_typeorm3.Entity)("llm_agents")
|
|
1470
|
+
], LlmAgent);
|
|
1471
|
+
function llmAgentToChatAgentOptions(agent) {
|
|
1472
|
+
return {
|
|
1473
|
+
systemPrompt: agent.systemInstruction?.trim() || void 0,
|
|
1474
|
+
model: agent.model?.trim() || void 0,
|
|
1475
|
+
temperature: agent.temperature ?? void 0,
|
|
1476
|
+
max_tokens: agent.maxTokens ?? void 0
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1387
1480
|
// src/lib/media-folder-path.ts
|
|
1388
1481
|
function sanitizeMediaFolderPath(input) {
|
|
1389
1482
|
if (input == null) return "";
|
|
@@ -1415,7 +1508,7 @@ async function relativePathFromMediaParentId(dataSource, entityMap, parentId) {
|
|
|
1415
1508
|
}
|
|
1416
1509
|
|
|
1417
1510
|
// src/lib/media-zip-extract.ts
|
|
1418
|
-
var
|
|
1511
|
+
var import_typeorm4 = require("typeorm");
|
|
1419
1512
|
var ZIP_MIME_TYPES = /* @__PURE__ */ new Set(["application/zip", "application/x-zip-compressed"]);
|
|
1420
1513
|
var MAX_ENTRIES = 2e3;
|
|
1421
1514
|
var MAX_TOTAL_UNCOMPRESSED = 80 * 1024 * 1024;
|
|
@@ -1466,7 +1559,7 @@ function guessMimeType(fileName) {
|
|
|
1466
1559
|
async function findOrCreateFolder(dataSource, entityMap, parentId, name) {
|
|
1467
1560
|
const safe = sanitizeStorageSegment(name);
|
|
1468
1561
|
const repo = dataSource.getRepository(entityMap.media);
|
|
1469
|
-
const where = parentId == null ? { kind: "folder", filename: safe, parentId: (0,
|
|
1562
|
+
const where = parentId == null ? { kind: "folder", filename: safe, parentId: (0, import_typeorm4.IsNull)() } : { kind: "folder", filename: safe, parentId };
|
|
1470
1563
|
const existing = await repo.findOne({ where });
|
|
1471
1564
|
if (existing) return existing.id;
|
|
1472
1565
|
const row = await repo.save(
|
|
@@ -1573,6 +1666,82 @@ async function extractZipMediaIntoParentTree(opts) {
|
|
|
1573
1666
|
}
|
|
1574
1667
|
|
|
1575
1668
|
// src/api/cms-handlers.ts
|
|
1669
|
+
function historyBeforeCurrentUser(history, currentUserContent) {
|
|
1670
|
+
const last = history[history.length - 1];
|
|
1671
|
+
if (last?.role === "user" && last.content === currentUserContent) {
|
|
1672
|
+
return history.slice(0, -1);
|
|
1673
|
+
}
|
|
1674
|
+
return [...history];
|
|
1675
|
+
}
|
|
1676
|
+
function pickStructuredRules(rules) {
|
|
1677
|
+
return {
|
|
1678
|
+
maxUserChars: rules.maxUserChars,
|
|
1679
|
+
maxMessageLength: rules.maxMessageLength,
|
|
1680
|
+
minUserChars: rules.minUserChars,
|
|
1681
|
+
minMessageLength: rules.minMessageLength,
|
|
1682
|
+
blockedSubstrings: rules.blockedSubstrings
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function guardrailsTextFromJsonObject(rules) {
|
|
1686
|
+
const g = typeof rules.guardrails === "string" && rules.guardrails.trim() || typeof rules.outputRules === "string" && rules.outputRules.trim() || typeof rules.outputInstructions === "string" && rules.outputInstructions.trim() || "";
|
|
1687
|
+
return g || null;
|
|
1688
|
+
}
|
|
1689
|
+
function parseLlmAgentValidationRules(validationRulesText) {
|
|
1690
|
+
const raw = validationRulesText?.trim();
|
|
1691
|
+
if (!raw) return { structured: {}, guardrailsForPrompt: null };
|
|
1692
|
+
try {
|
|
1693
|
+
const parsed = JSON.parse(raw);
|
|
1694
|
+
if (typeof parsed === "string") {
|
|
1695
|
+
const s = parsed.trim();
|
|
1696
|
+
return { structured: {}, guardrailsForPrompt: s || null };
|
|
1697
|
+
}
|
|
1698
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1699
|
+
return { structured: {}, guardrailsForPrompt: raw };
|
|
1700
|
+
}
|
|
1701
|
+
const rules = parsed;
|
|
1702
|
+
return {
|
|
1703
|
+
structured: pickStructuredRules(rules),
|
|
1704
|
+
guardrailsForPrompt: guardrailsTextFromJsonObject(rules)
|
|
1705
|
+
};
|
|
1706
|
+
} catch {
|
|
1707
|
+
return { structured: {}, guardrailsForPrompt: raw };
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function validateUserMessageAgainstStructuredRules(message, structured) {
|
|
1711
|
+
const maxLen = structured.maxUserChars ?? structured.maxMessageLength;
|
|
1712
|
+
if (typeof maxLen === "number" && Number.isFinite(maxLen) && maxLen >= 0 && message.length > maxLen) {
|
|
1713
|
+
return { ok: false, error: `Message exceeds maximum length (${maxLen} characters)` };
|
|
1714
|
+
}
|
|
1715
|
+
const minLen = structured.minUserChars ?? structured.minMessageLength;
|
|
1716
|
+
if (typeof minLen === "number" && Number.isFinite(minLen) && minLen > 0 && message.length < minLen) {
|
|
1717
|
+
return { ok: false, error: `Message is shorter than minimum length (${minLen} characters)` };
|
|
1718
|
+
}
|
|
1719
|
+
const blocked = structured.blockedSubstrings;
|
|
1720
|
+
if (Array.isArray(blocked) && blocked.length > 0) {
|
|
1721
|
+
const lower = message.toLowerCase();
|
|
1722
|
+
for (const s of blocked) {
|
|
1723
|
+
if (typeof s !== "string" || !s.trim()) continue;
|
|
1724
|
+
if (lower.includes(s.toLowerCase())) {
|
|
1725
|
+
return { ok: false, error: "Message contains disallowed content" };
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return { ok: true };
|
|
1730
|
+
}
|
|
1731
|
+
function validateUserMessageAgainstAgentRules(message, validationRulesText) {
|
|
1732
|
+
const { structured } = parseLlmAgentValidationRules(validationRulesText);
|
|
1733
|
+
return validateUserMessageAgainstStructuredRules(message, structured);
|
|
1734
|
+
}
|
|
1735
|
+
function mergeGuardrailsIntoSystemPrompt(baseSystem, guardrailsForPrompt) {
|
|
1736
|
+
const g = guardrailsForPrompt?.trim();
|
|
1737
|
+
const b = (baseSystem ?? "").trim();
|
|
1738
|
+
if (!g) return b;
|
|
1739
|
+
const block = `### Output guardrails (follow in every reply)
|
|
1740
|
+
${g}`;
|
|
1741
|
+
return b ? `${b}
|
|
1742
|
+
|
|
1743
|
+
${block}` : block;
|
|
1744
|
+
}
|
|
1576
1745
|
function createDashboardStatsHandler(config) {
|
|
1577
1746
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
1578
1747
|
return async function GET(req) {
|
|
@@ -1595,8 +1764,8 @@ function createDashboardStatsHandler(config) {
|
|
|
1595
1764
|
repo("form_submissions")?.count() ?? 0,
|
|
1596
1765
|
repo("users")?.count({ where: { deleted: false } }) ?? 0,
|
|
1597
1766
|
repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
|
|
1598
|
-
repo("contacts")?.count({ where: { createdAt: (0,
|
|
1599
|
-
repo("form_submissions")?.count({ where: { createdAt: (0,
|
|
1767
|
+
repo("contacts")?.count({ where: { createdAt: (0, import_typeorm5.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
|
|
1768
|
+
repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm5.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
|
|
1600
1769
|
repo("contacts")?.createQueryBuilder("c").select("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')", "type").addSelect("COUNT(*)", "count").where("c.deleted = :deleted", { deleted: false }).groupBy("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')").getRawMany() ?? []
|
|
1601
1770
|
]);
|
|
1602
1771
|
return json({
|
|
@@ -1645,19 +1814,19 @@ function createEcommerceAnalyticsHandler(config) {
|
|
|
1645
1814
|
const productRepo = dataSource.getRepository(entityMap.products);
|
|
1646
1815
|
const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
|
|
1647
1816
|
orderRepo.find({
|
|
1648
|
-
where: { deleted: false, createdAt: (0,
|
|
1817
|
+
where: { deleted: false, createdAt: (0, import_typeorm5.MoreThanOrEqual)(start), orderKind: "sale", status: (0, import_typeorm5.In)(["confirmed", "processing", "completed"]) },
|
|
1649
1818
|
select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
|
|
1650
1819
|
}),
|
|
1651
1820
|
orderRepo.find({
|
|
1652
|
-
where: { deleted: false, createdAt: (0,
|
|
1821
|
+
where: { deleted: false, createdAt: (0, import_typeorm5.MoreThanOrEqual)(start), orderKind: "return" },
|
|
1653
1822
|
select: ["id", "createdAt", "total"]
|
|
1654
1823
|
}),
|
|
1655
1824
|
orderRepo.find({
|
|
1656
|
-
where: { deleted: false, createdAt: (0,
|
|
1825
|
+
where: { deleted: false, createdAt: (0, import_typeorm5.MoreThanOrEqual)(start), orderKind: "replacement" },
|
|
1657
1826
|
select: ["id", "createdAt", "total"]
|
|
1658
1827
|
}),
|
|
1659
1828
|
paymentRepo.find({
|
|
1660
|
-
where: { deleted: false, createdAt: (0,
|
|
1829
|
+
where: { deleted: false, createdAt: (0, import_typeorm5.MoreThanOrEqual)(start) },
|
|
1661
1830
|
select: ["id", "status", "method", "amount", "createdAt"]
|
|
1662
1831
|
}),
|
|
1663
1832
|
productRepo.find({
|
|
@@ -1667,7 +1836,7 @@ function createEcommerceAnalyticsHandler(config) {
|
|
|
1667
1836
|
]);
|
|
1668
1837
|
const saleOrderIds = salesOrders.map((o) => o.id);
|
|
1669
1838
|
const orderItems = saleOrderIds.length ? await itemRepo.find({
|
|
1670
|
-
where: { orderId: (0,
|
|
1839
|
+
where: { orderId: (0, import_typeorm5.In)(saleOrderIds) },
|
|
1671
1840
|
select: ["id", "orderId", "productId", "quantity", "total"]
|
|
1672
1841
|
}) : [];
|
|
1673
1842
|
const grossSales = salesOrders.reduce((sum, o) => sum + toNum(o.subtotal), 0);
|
|
@@ -2355,8 +2524,8 @@ function createUsersApiHandlers(config) {
|
|
|
2355
2524
|
const sortOrder = url.searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
|
|
2356
2525
|
const search = url.searchParams.get("search");
|
|
2357
2526
|
const where = search ? [
|
|
2358
|
-
{ name: (0,
|
|
2359
|
-
{ email: (0,
|
|
2527
|
+
{ name: (0, import_typeorm5.ILike)(`%${search}%`), deleted: false },
|
|
2528
|
+
{ email: (0, import_typeorm5.ILike)(`%${search}%`), deleted: false }
|
|
2360
2529
|
] : { deleted: false };
|
|
2361
2530
|
const [data, total] = await userRepo().findAndCount({
|
|
2362
2531
|
skip,
|
|
@@ -2654,9 +2823,24 @@ function createSettingsApiHandlers(config) {
|
|
|
2654
2823
|
}
|
|
2655
2824
|
var KB_CHUNK_LIMIT = 10;
|
|
2656
2825
|
var KB_CONTEXT_MAX_CHARS = 4e3;
|
|
2826
|
+
var RAG_LOG = "[rag-context]";
|
|
2657
2827
|
function getQueryTerms(message) {
|
|
2658
2828
|
return message.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 2).slice(0, 6);
|
|
2659
2829
|
}
|
|
2830
|
+
function normalizeChatModeSetting(raw) {
|
|
2831
|
+
if (raw === "external" || raw === "llm") return raw;
|
|
2832
|
+
return "whatsapp";
|
|
2833
|
+
}
|
|
2834
|
+
async function loadLlmSettingsMap(dataSource, entityMap) {
|
|
2835
|
+
if (!entityMap.configs) return {};
|
|
2836
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
2837
|
+
const rows = await repo.find({ where: { settings: "llm", deleted: false } });
|
|
2838
|
+
const out = {};
|
|
2839
|
+
for (const row of rows) {
|
|
2840
|
+
out[row.key] = row.value;
|
|
2841
|
+
}
|
|
2842
|
+
return out;
|
|
2843
|
+
}
|
|
2660
2844
|
function createChatHandlers(config) {
|
|
2661
2845
|
const { dataSource, entityMap, json, getCms } = config;
|
|
2662
2846
|
const contactRepo = () => dataSource.getRepository(entityMap.contacts);
|
|
@@ -2664,6 +2848,26 @@ function createChatHandlers(config) {
|
|
|
2664
2848
|
const msgRepo = () => dataSource.getRepository(entityMap.chat_messages);
|
|
2665
2849
|
const chunkRepo = () => dataSource.getRepository(entityMap.knowledge_base_chunks);
|
|
2666
2850
|
return {
|
|
2851
|
+
async publicConfig(_req) {
|
|
2852
|
+
try {
|
|
2853
|
+
const map = await loadLlmSettingsMap(dataSource, entityMap);
|
|
2854
|
+
const mode = normalizeChatModeSetting(map.chatMode);
|
|
2855
|
+
const body = {
|
|
2856
|
+
enabled: map.enabled !== "false",
|
|
2857
|
+
chatMode: mode,
|
|
2858
|
+
agentSlug: mode === "llm" ? (map.attachedAgentSlug ?? "").trim() : "",
|
|
2859
|
+
botName: map.botName ?? "",
|
|
2860
|
+
icon: map.icon ?? "",
|
|
2861
|
+
iconImageUrl: map.iconImageUrl ?? "",
|
|
2862
|
+
iconBackgroundColor: map.iconBackgroundColor ?? "#6366f1",
|
|
2863
|
+
headerColor: map.headerColor ?? "#6366f1",
|
|
2864
|
+
whatsappPhone: map.whatsappPhone ?? ""
|
|
2865
|
+
};
|
|
2866
|
+
return json(body);
|
|
2867
|
+
} catch {
|
|
2868
|
+
return json({ error: "Failed to load chat config" }, { status: 500 });
|
|
2869
|
+
}
|
|
2870
|
+
},
|
|
2667
2871
|
async identify(req) {
|
|
2668
2872
|
try {
|
|
2669
2873
|
const body = await req.json();
|
|
@@ -2711,20 +2915,78 @@ function createChatHandlers(config) {
|
|
|
2711
2915
|
relations: ["messages"]
|
|
2712
2916
|
});
|
|
2713
2917
|
if (!conv) return json({ error: "Conversation not found" }, { status: 404 });
|
|
2714
|
-
const msgRepoInst = msgRepo();
|
|
2715
|
-
await msgRepoInst.save(msgRepoInst.create({ conversationId, role: "user", content: message }));
|
|
2716
2918
|
const cms = await getCms();
|
|
2717
2919
|
const llm = cms.getPlugin("llm");
|
|
2718
2920
|
if (!llm?.chat) return json({ error: "LLM not configured" }, { status: 503 });
|
|
2921
|
+
const llmSettings = await loadLlmSettingsMap(dataSource, entityMap);
|
|
2922
|
+
const supportMode = normalizeChatModeSetting(llmSettings.chatMode);
|
|
2923
|
+
let effectiveSlug = (body?.agentSlug ?? "").trim();
|
|
2924
|
+
if (!effectiveSlug && supportMode === "llm" && entityMap.llm_agents) {
|
|
2925
|
+
effectiveSlug = (llmSettings.attachedAgentSlug ?? "").trim();
|
|
2926
|
+
}
|
|
2927
|
+
let agentRow = null;
|
|
2928
|
+
if (effectiveSlug) {
|
|
2929
|
+
if (!entityMap.llm_agents) {
|
|
2930
|
+
return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
|
|
2931
|
+
}
|
|
2932
|
+
const agentRepo = dataSource.getRepository(
|
|
2933
|
+
entityMap.llm_agents
|
|
2934
|
+
);
|
|
2935
|
+
agentRow = await agentRepo.findOne({
|
|
2936
|
+
where: { slug: effectiveSlug, deleted: false, enabled: true }
|
|
2937
|
+
});
|
|
2938
|
+
if (!agentRow && (body?.agentSlug ?? "").trim()) {
|
|
2939
|
+
return json({ error: "Agent not found or disabled", agentSlug: effectiveSlug }, { status: 404 });
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
console.info(RAG_LOG, "step 1 | resolve agent", {
|
|
2943
|
+
agentSlug: effectiveSlug || "(none)",
|
|
2944
|
+
agentFound: !!agentRow,
|
|
2945
|
+
agentId: agentRow?.id ?? null
|
|
2946
|
+
});
|
|
2947
|
+
const parsedValidation = agentRow ? parseLlmAgentValidationRules(agentRow.validationRules) : { structured: {}, guardrailsForPrompt: null };
|
|
2948
|
+
if (agentRow) {
|
|
2949
|
+
const v = validateUserMessageAgainstStructuredRules(message, parsedValidation.structured);
|
|
2950
|
+
if (!v.ok) return json({ error: v.error, reason: "validation_failed" }, { status: 400 });
|
|
2951
|
+
}
|
|
2952
|
+
let kbDocumentScope;
|
|
2953
|
+
const Junction = entityMap.llm_agent_knowledge_documents;
|
|
2954
|
+
if (agentRow && Junction) {
|
|
2955
|
+
const linkRepo = dataSource.getRepository(Junction);
|
|
2956
|
+
const links = await linkRepo.find({ where: { agentId: agentRow.id } });
|
|
2957
|
+
const ids = [...new Set(links.map((l) => l.documentId))];
|
|
2958
|
+
if (ids.length > 0) kbDocumentScope = ids;
|
|
2959
|
+
}
|
|
2960
|
+
console.info(RAG_LOG, "step 2 | knowledge scope", {
|
|
2961
|
+
scopedDocumentIds: kbDocumentScope ?? "(all documents)",
|
|
2962
|
+
documentCount: kbDocumentScope?.length ?? "all"
|
|
2963
|
+
});
|
|
2964
|
+
const msgRepoInst = msgRepo();
|
|
2965
|
+
await msgRepoInst.save(msgRepoInst.create({ conversationId, role: "user", content: message }));
|
|
2719
2966
|
let contextParts = [];
|
|
2967
|
+
console.info(RAG_LOG, "step 3 | embed user query", {
|
|
2968
|
+
messageChars: message.length,
|
|
2969
|
+
embedAvailable: !!llm.embed
|
|
2970
|
+
});
|
|
2720
2971
|
const queryEmbedding = llm.embed ? await llm.embed(message) : null;
|
|
2972
|
+
console.info(RAG_LOG, "step 4 | query embedding result", {
|
|
2973
|
+
dimensions: queryEmbedding?.length ?? 0,
|
|
2974
|
+
hasEmbedding: !!(queryEmbedding && queryEmbedding.length > 0)
|
|
2975
|
+
});
|
|
2721
2976
|
if (queryEmbedding && queryEmbedding.length > 0) {
|
|
2722
2977
|
const vectorStr = "[" + queryEmbedding.join(",") + "]";
|
|
2723
2978
|
try {
|
|
2724
|
-
const rows = await dataSource.query(
|
|
2979
|
+
const rows = kbDocumentScope?.length ? await dataSource.query(
|
|
2980
|
+
`SELECT id, content FROM knowledge_base_chunks WHERE embedding IS NOT NULL AND "documentId" = ANY($3::int[]) ORDER BY embedding <=> $1::vector LIMIT $2`,
|
|
2981
|
+
[vectorStr, KB_CHUNK_LIMIT, kbDocumentScope]
|
|
2982
|
+
) : await dataSource.query(
|
|
2725
2983
|
`SELECT id, content FROM knowledge_base_chunks WHERE embedding IS NOT NULL ORDER BY embedding <=> $1::vector LIMIT $2`,
|
|
2726
2984
|
[vectorStr, KB_CHUNK_LIMIT]
|
|
2727
2985
|
);
|
|
2986
|
+
console.info(RAG_LOG, "step 5 | vector search results", {
|
|
2987
|
+
rowsReturned: rows.length,
|
|
2988
|
+
chunkIds: rows.map((r) => r.id)
|
|
2989
|
+
});
|
|
2728
2990
|
let totalLen = 0;
|
|
2729
2991
|
for (const r of rows) {
|
|
2730
2992
|
const text = (r.content ?? "").trim();
|
|
@@ -2732,13 +2994,24 @@ function createChatHandlers(config) {
|
|
|
2732
2994
|
contextParts.push(text);
|
|
2733
2995
|
totalLen += text.length;
|
|
2734
2996
|
}
|
|
2735
|
-
|
|
2997
|
+
console.info(RAG_LOG, "step 5a | vector context selected", {
|
|
2998
|
+
chunksUsed: contextParts.length,
|
|
2999
|
+
totalChars: totalLen
|
|
3000
|
+
});
|
|
3001
|
+
} catch (vecErr) {
|
|
3002
|
+
console.warn(RAG_LOG, "step 5 | vector search failed; falling back to keyword", {
|
|
3003
|
+
err: vecErr instanceof Error ? vecErr.message : String(vecErr)
|
|
3004
|
+
});
|
|
2736
3005
|
}
|
|
2737
3006
|
}
|
|
2738
3007
|
if (contextParts.length === 0) {
|
|
2739
3008
|
const terms = getQueryTerms(message);
|
|
3009
|
+
console.info(RAG_LOG, "step 6 | keyword fallback", {
|
|
3010
|
+
reason: !(queryEmbedding && queryEmbedding.length > 0) ? "no embedding" : "vector search returned nothing",
|
|
3011
|
+
searchTerms: terms
|
|
3012
|
+
});
|
|
2740
3013
|
if (terms.length > 0) {
|
|
2741
|
-
const conditions = terms.map((t) => ({ content: (0,
|
|
3014
|
+
const conditions = kbDocumentScope?.length ? terms.map((t) => ({ content: (0, import_typeorm5.ILike)(`%${t}%`), documentId: (0, import_typeorm5.In)(kbDocumentScope) })) : terms.map((t) => ({ content: (0, import_typeorm5.ILike)(`%${t}%`) }));
|
|
2742
3015
|
const chunks = await chunkRepo().find({
|
|
2743
3016
|
where: conditions,
|
|
2744
3017
|
take: KB_CHUNK_LIMIT,
|
|
@@ -2753,19 +3026,66 @@ function createChatHandlers(config) {
|
|
|
2753
3026
|
contextParts.push(text);
|
|
2754
3027
|
totalLen += text.length;
|
|
2755
3028
|
}
|
|
3029
|
+
console.info(RAG_LOG, "step 6a | keyword results", {
|
|
3030
|
+
chunksFound: chunks.length,
|
|
3031
|
+
chunksUsed: contextParts.length,
|
|
3032
|
+
totalChars: totalLen
|
|
3033
|
+
});
|
|
2756
3034
|
}
|
|
2757
3035
|
}
|
|
2758
|
-
const
|
|
2759
|
-
const
|
|
3036
|
+
const historyRaw = (conv.messages ?? []).sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()).map((m) => ({ role: m.role, content: m.content }));
|
|
3037
|
+
const history = historyBeforeCurrentUser(historyRaw, message);
|
|
3038
|
+
let content;
|
|
3039
|
+
const ragContext = contextParts.length > 0 ? contextParts.join("\n\n") : void 0;
|
|
3040
|
+
console.info(RAG_LOG, "step 7 | final context", {
|
|
3041
|
+
method: contextParts.length > 0 ? "rag" : "none",
|
|
3042
|
+
contextChunks: contextParts.length,
|
|
3043
|
+
contextChars: ragContext?.length ?? 0,
|
|
3044
|
+
contextPreview: ragContext ? ragContext.slice(0, 200) + (ragContext.length > 200 ? "\u2026" : "") : "(no context)"
|
|
3045
|
+
});
|
|
3046
|
+
if (agentRow && llm.chatAgent) {
|
|
3047
|
+
const fromAgent = llmAgentToChatAgentOptions(agentRow);
|
|
3048
|
+
const systemPrompt = mergeGuardrailsIntoSystemPrompt(
|
|
3049
|
+
fromAgent.systemPrompt,
|
|
3050
|
+
parsedValidation.guardrailsForPrompt
|
|
3051
|
+
);
|
|
3052
|
+
const res = await llm.chatAgent({
|
|
3053
|
+
...fromAgent,
|
|
3054
|
+
systemPrompt: systemPrompt || void 0,
|
|
3055
|
+
context: ragContext,
|
|
3056
|
+
history,
|
|
3057
|
+
userPrompt: message
|
|
3058
|
+
});
|
|
3059
|
+
content = res.content;
|
|
3060
|
+
} else {
|
|
3061
|
+
const ragSystem = contextParts.length > 0 ? `Use the following context about the company and its products to answer. If the answer is not in the context, say so.
|
|
2760
3062
|
|
|
2761
3063
|
Context:
|
|
2762
|
-
${contextParts.join("\n\n")}` : "
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
3064
|
+
${contextParts.join("\n\n")}` : "";
|
|
3065
|
+
const defaultSystem = "You are a helpful assistant for the company. If you do not have specific information, say so.";
|
|
3066
|
+
let systemContent;
|
|
3067
|
+
if (agentRow) {
|
|
3068
|
+
const base = agentRow.systemInstruction?.trim() || "";
|
|
3069
|
+
systemContent = mergeGuardrailsIntoSystemPrompt(
|
|
3070
|
+
[base, ragSystem].filter(Boolean).join("\n\n") || defaultSystem,
|
|
3071
|
+
parsedValidation.guardrailsForPrompt
|
|
3072
|
+
);
|
|
3073
|
+
} else {
|
|
3074
|
+
systemContent = ragSystem || defaultSystem;
|
|
3075
|
+
}
|
|
3076
|
+
const messages = [
|
|
3077
|
+
{ role: "system", content: systemContent },
|
|
3078
|
+
...history,
|
|
3079
|
+
{ role: "user", content: message }
|
|
3080
|
+
];
|
|
3081
|
+
const chatOpts = agentRow ? {
|
|
3082
|
+
model: agentRow.model ?? void 0,
|
|
3083
|
+
temperature: agentRow.temperature ?? void 0,
|
|
3084
|
+
max_tokens: agentRow.maxTokens ?? void 0
|
|
3085
|
+
} : {};
|
|
3086
|
+
const res = await llm.chat(messages, chatOpts);
|
|
3087
|
+
content = res.content;
|
|
3088
|
+
}
|
|
2769
3089
|
await msgRepoInst.save(msgRepoInst.create({ conversationId, role: "assistant", content }));
|
|
2770
3090
|
return json({ content });
|
|
2771
3091
|
} catch (err) {
|
|
@@ -2776,6 +3096,314 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
|
|
|
2776
3096
|
};
|
|
2777
3097
|
}
|
|
2778
3098
|
|
|
3099
|
+
// src/api/llm-agent-knowledge-handlers.ts
|
|
3100
|
+
var import_typeorm6 = require("typeorm");
|
|
3101
|
+
var INGEST_CHUNK_CHARS = 900;
|
|
3102
|
+
var MAX_CHUNKS_PER_UPLOAD = 400;
|
|
3103
|
+
var EMBED_CONCURRENCY = 5;
|
|
3104
|
+
var MAX_PDF_BYTES = 25 * 1024 * 1024;
|
|
3105
|
+
var TEXT_FILE_TYPES = /* @__PURE__ */ new Set(["text/plain", "text/markdown", "application/json"]);
|
|
3106
|
+
var KB_LOG = "[llm-agent-knowledge]";
|
|
3107
|
+
function llmEmbedDebug() {
|
|
3108
|
+
const v = process.env.LLM_EMBED_DEBUG?.toLowerCase();
|
|
3109
|
+
return v === "1" || v === "true" || v === "yes";
|
|
3110
|
+
}
|
|
3111
|
+
function isPdfUpload(mime, fileName) {
|
|
3112
|
+
if (mime === "application/pdf") return true;
|
|
3113
|
+
const n = fileName.toLowerCase();
|
|
3114
|
+
return n.endsWith(".pdf");
|
|
3115
|
+
}
|
|
3116
|
+
async function extractTextFromPdf(buffer) {
|
|
3117
|
+
const pdfParse = await import("pdf-parse");
|
|
3118
|
+
const data = await pdfParse(buffer);
|
|
3119
|
+
return (data?.text ?? "").trim();
|
|
3120
|
+
}
|
|
3121
|
+
async function writeEmbeddingsConcurrent(dataSource, embed, chunks, concurrency) {
|
|
3122
|
+
let next = 0;
|
|
3123
|
+
let written = 0;
|
|
3124
|
+
let failed = 0;
|
|
3125
|
+
let skippedEmpty = 0;
|
|
3126
|
+
async function worker() {
|
|
3127
|
+
for (; ; ) {
|
|
3128
|
+
const i = next++;
|
|
3129
|
+
if (i >= chunks.length) return;
|
|
3130
|
+
const c = chunks[i];
|
|
3131
|
+
try {
|
|
3132
|
+
const emb = await embed(c.content);
|
|
3133
|
+
if (emb?.length) {
|
|
3134
|
+
const vectorStr = "[" + emb.join(",") + "]";
|
|
3135
|
+
await dataSource.query(
|
|
3136
|
+
`UPDATE knowledge_base_chunks SET embedding = $1::vector WHERE id = $2`,
|
|
3137
|
+
[vectorStr, c.id]
|
|
3138
|
+
);
|
|
3139
|
+
written++;
|
|
3140
|
+
} else {
|
|
3141
|
+
skippedEmpty++;
|
|
3142
|
+
if (llmEmbedDebug()) {
|
|
3143
|
+
console.warn(`${KB_LOG} embed() returned empty vector`, { chunkId: c.id });
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
} catch (err) {
|
|
3147
|
+
failed++;
|
|
3148
|
+
console.error(`${KB_LOG} embedding DB update failed`, {
|
|
3149
|
+
chunkId: c.id,
|
|
3150
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
const n = Math.max(1, Math.min(concurrency, chunks.length));
|
|
3156
|
+
await Promise.all(Array.from({ length: n }, () => worker()));
|
|
3157
|
+
const summary = {
|
|
3158
|
+
chunkCount: chunks.length,
|
|
3159
|
+
written,
|
|
3160
|
+
failed,
|
|
3161
|
+
skippedEmpty
|
|
3162
|
+
};
|
|
3163
|
+
if (skippedEmpty > 0 && written === 0) {
|
|
3164
|
+
summary.hint = "embed() returned empty vectors \u2014 see [LLM embed] logs (often HTTP 404 on /v1/embeddings, or wrong response shape).";
|
|
3165
|
+
}
|
|
3166
|
+
console.error(`${KB_LOG} embedding pass finished`, summary);
|
|
3167
|
+
return { written, failed };
|
|
3168
|
+
}
|
|
3169
|
+
function splitIntoChunks(text, maxLen) {
|
|
3170
|
+
const t = text.trim();
|
|
3171
|
+
if (!t) return [];
|
|
3172
|
+
const chunks = [];
|
|
3173
|
+
for (let i = 0; i < t.length; i += maxLen) {
|
|
3174
|
+
chunks.push(t.slice(i, i + maxLen));
|
|
3175
|
+
}
|
|
3176
|
+
return chunks;
|
|
3177
|
+
}
|
|
3178
|
+
async function findAgentBySlug(dataSource, llmAgents, slug) {
|
|
3179
|
+
const repo = dataSource.getRepository(llmAgents);
|
|
3180
|
+
return repo.findOne({
|
|
3181
|
+
where: { slug, deleted: false }
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
function createLlmAgentKnowledgeHandlers(config) {
|
|
3185
|
+
const { dataSource, entityMap, getCms, json, requireAuth, requireEntityPermission } = config;
|
|
3186
|
+
const kbDoc = entityMap.knowledge_base_documents;
|
|
3187
|
+
const kbChunk = entityMap.knowledge_base_chunks;
|
|
3188
|
+
const llmAgents = entityMap.llm_agents;
|
|
3189
|
+
const junction = entityMap.llm_agent_knowledge_documents;
|
|
3190
|
+
if (!kbDoc || !kbChunk || !llmAgents || !junction) {
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
async function gate(req, action) {
|
|
3194
|
+
const a = await requireAuth(req);
|
|
3195
|
+
if (a) return a;
|
|
3196
|
+
if (requireEntityPermission) {
|
|
3197
|
+
const pe = await requireEntityPermission(req, "llm_agents", action);
|
|
3198
|
+
if (pe) return pe;
|
|
3199
|
+
}
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
return {
|
|
3203
|
+
async list(req, slug) {
|
|
3204
|
+
const denied = await gate(req, "read");
|
|
3205
|
+
if (denied) return denied;
|
|
3206
|
+
try {
|
|
3207
|
+
const agent = await findAgentBySlug(dataSource, llmAgents, slug);
|
|
3208
|
+
if (!agent) return json({ error: "Agent not found" }, { status: 404 });
|
|
3209
|
+
const linkRepo = dataSource.getRepository(junction);
|
|
3210
|
+
const links = await linkRepo.find({ where: { agentId: agent.id } });
|
|
3211
|
+
const docIds = [...new Set(links.map((l) => l.documentId))];
|
|
3212
|
+
if (docIds.length === 0) return json({ documents: [] });
|
|
3213
|
+
const docRepo = dataSource.getRepository(kbDoc);
|
|
3214
|
+
const docs = await docRepo.find({ where: { id: (0, import_typeorm6.In)(docIds) } });
|
|
3215
|
+
const byId = new Map(docs.map((d) => [d.id, d]));
|
|
3216
|
+
const documents = docIds.map((id) => {
|
|
3217
|
+
const d = byId.get(id);
|
|
3218
|
+
return d ? { id: d.id, name: d.name } : null;
|
|
3219
|
+
}).filter(Boolean);
|
|
3220
|
+
return json({ documents });
|
|
3221
|
+
} catch (err) {
|
|
3222
|
+
const msg = err instanceof Error ? err.message : "Failed to list knowledge";
|
|
3223
|
+
return json({ error: msg }, { status: 500 });
|
|
3224
|
+
}
|
|
3225
|
+
},
|
|
3226
|
+
async post(req, slug) {
|
|
3227
|
+
const denied = await gate(req, "update");
|
|
3228
|
+
if (denied) return denied;
|
|
3229
|
+
try {
|
|
3230
|
+
const agent = await findAgentBySlug(dataSource, llmAgents, slug);
|
|
3231
|
+
if (!agent) return json({ error: "Agent not found" }, { status: 404 });
|
|
3232
|
+
let name = "";
|
|
3233
|
+
let text = "";
|
|
3234
|
+
let sourceUrl = null;
|
|
3235
|
+
let existingDocumentId = null;
|
|
3236
|
+
const ct = req.headers.get("content-type") || "";
|
|
3237
|
+
if (ct.includes("application/json")) {
|
|
3238
|
+
const body = await req.json();
|
|
3239
|
+
existingDocumentId = typeof body?.documentId === "number" && Number.isFinite(body.documentId) ? body.documentId : null;
|
|
3240
|
+
name = (body?.name ?? "").trim();
|
|
3241
|
+
text = (body?.text ?? "").trim();
|
|
3242
|
+
sourceUrl = typeof body?.sourceUrl === "string" && body.sourceUrl.trim() ? body.sourceUrl.trim() : null;
|
|
3243
|
+
} else if (ct.includes("multipart/form-data")) {
|
|
3244
|
+
const form = await req.formData();
|
|
3245
|
+
name = form.get("name")?.trim() ?? "";
|
|
3246
|
+
text = form.get("text")?.trim() ?? "";
|
|
3247
|
+
const file = form.get("file");
|
|
3248
|
+
if (file && typeof file !== "string" && "arrayBuffer" in file) {
|
|
3249
|
+
const f = file;
|
|
3250
|
+
const mime = (f.type || "").split(";")[0].trim().toLowerCase();
|
|
3251
|
+
const buf = Buffer.from(await f.arrayBuffer());
|
|
3252
|
+
if (TEXT_FILE_TYPES.has(mime)) {
|
|
3253
|
+
const decoded = buf.toString("utf8");
|
|
3254
|
+
if (!text) text = decoded;
|
|
3255
|
+
} else if (isPdfUpload(mime, f.name || "")) {
|
|
3256
|
+
if (buf.length > MAX_PDF_BYTES) {
|
|
3257
|
+
return json(
|
|
3258
|
+
{ error: `PDF too large (max ${Math.floor(MAX_PDF_BYTES / (1024 * 1024))}MB)` },
|
|
3259
|
+
{ status: 413 }
|
|
3260
|
+
);
|
|
3261
|
+
}
|
|
3262
|
+
try {
|
|
3263
|
+
const extracted = await extractTextFromPdf(buf);
|
|
3264
|
+
if (!text) text = extracted;
|
|
3265
|
+
} catch {
|
|
3266
|
+
return json(
|
|
3267
|
+
{ error: "Could not read PDF text (file may be encrypted, corrupt, or image-only)" },
|
|
3268
|
+
{ status: 422 }
|
|
3269
|
+
);
|
|
3270
|
+
}
|
|
3271
|
+
} else {
|
|
3272
|
+
return json(
|
|
3273
|
+
{
|
|
3274
|
+
error: "Unsupported file type; use text/plain, text/markdown, application/json, or application/pdf"
|
|
3275
|
+
},
|
|
3276
|
+
{ status: 415 }
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
if (!name && f.name) name = f.name.replace(/\.[^/.]+$/, "") || f.name;
|
|
3280
|
+
}
|
|
3281
|
+
} else {
|
|
3282
|
+
return json({ error: "Use application/json or multipart/form-data" }, { status: 400 });
|
|
3283
|
+
}
|
|
3284
|
+
const linkRepo = dataSource.getRepository(junction);
|
|
3285
|
+
if (existingDocumentId != null) {
|
|
3286
|
+
const docRepo2 = dataSource.getRepository(kbDoc);
|
|
3287
|
+
const existing = await docRepo2.findOne({ where: { id: existingDocumentId } });
|
|
3288
|
+
if (!existing) return json({ error: "documentId not found" }, { status: 404 });
|
|
3289
|
+
const dup2 = await linkRepo.findOne({
|
|
3290
|
+
where: { agentId: agent.id, documentId: existingDocumentId }
|
|
3291
|
+
});
|
|
3292
|
+
if (!dup2) {
|
|
3293
|
+
await linkRepo.save(linkRepo.create({ agentId: agent.id, documentId: existingDocumentId }));
|
|
3294
|
+
}
|
|
3295
|
+
return json({ documentId: existingDocumentId, linked: true, created: false });
|
|
3296
|
+
}
|
|
3297
|
+
if (!text) return json({ error: "text or file with text content is required" }, { status: 400 });
|
|
3298
|
+
if (!name) name = "Untitled";
|
|
3299
|
+
const parts = splitIntoChunks(text, INGEST_CHUNK_CHARS);
|
|
3300
|
+
if (parts.length === 0) {
|
|
3301
|
+
return json({ error: "text or file with text content is required" }, { status: 400 });
|
|
3302
|
+
}
|
|
3303
|
+
if (parts.length > MAX_CHUNKS_PER_UPLOAD) {
|
|
3304
|
+
return json(
|
|
3305
|
+
{
|
|
3306
|
+
error: `Document is too large for one upload (${parts.length} chunks; max ${MAX_CHUNKS_PER_UPLOAD}). Split into smaller files.`
|
|
3307
|
+
},
|
|
3308
|
+
{ status: 413 }
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
3311
|
+
const docRepo = dataSource.getRepository(kbDoc);
|
|
3312
|
+
const chunkRepo = dataSource.getRepository(kbChunk);
|
|
3313
|
+
const now = /* @__PURE__ */ new Date();
|
|
3314
|
+
const doc = await docRepo.save(
|
|
3315
|
+
docRepo.create({ name, content: text, sourceUrl, createdAt: now, updatedAt: now })
|
|
3316
|
+
);
|
|
3317
|
+
const docId = doc.id;
|
|
3318
|
+
const chunkRows = parts.map(
|
|
3319
|
+
(content, i) => chunkRepo.create({ documentId: docId, content, chunkIndex: i, createdAt: now })
|
|
3320
|
+
);
|
|
3321
|
+
const savedList = await chunkRepo.save(chunkRows);
|
|
3322
|
+
const savedChunks = savedList.map(
|
|
3323
|
+
(row, i) => ({
|
|
3324
|
+
id: row.id,
|
|
3325
|
+
content: parts[i]
|
|
3326
|
+
})
|
|
3327
|
+
);
|
|
3328
|
+
const dup = await linkRepo.findOne({ where: { agentId: agent.id, documentId: docId } });
|
|
3329
|
+
if (!dup) {
|
|
3330
|
+
await linkRepo.save(linkRepo.create({ agentId: agent.id, documentId: docId }));
|
|
3331
|
+
}
|
|
3332
|
+
let embeddingsWritten = 0;
|
|
3333
|
+
let embeddingsFailed = 0;
|
|
3334
|
+
try {
|
|
3335
|
+
const cms = await getCms();
|
|
3336
|
+
const llm = cms.getPlugin("llm");
|
|
3337
|
+
if (llm?.embed && savedChunks.length > 0) {
|
|
3338
|
+
console.info(`${KB_LOG} starting embedding pass`, { slug, chunkCount: savedChunks.length });
|
|
3339
|
+
const embedBound = (text2) => llm.embed(text2);
|
|
3340
|
+
const { written, failed } = await writeEmbeddingsConcurrent(
|
|
3341
|
+
dataSource,
|
|
3342
|
+
embedBound,
|
|
3343
|
+
savedChunks,
|
|
3344
|
+
EMBED_CONCURRENCY
|
|
3345
|
+
);
|
|
3346
|
+
embeddingsWritten = written;
|
|
3347
|
+
embeddingsFailed = failed;
|
|
3348
|
+
} else {
|
|
3349
|
+
console.error(`${KB_LOG} embeddings skipped`, {
|
|
3350
|
+
slug,
|
|
3351
|
+
hasLlmPlugin: !!llm,
|
|
3352
|
+
hasEmbed: typeof llm?.embed === "function",
|
|
3353
|
+
chunkCount: savedChunks.length,
|
|
3354
|
+
hint: !llm || typeof llm.embed !== "function" ? "LLM plugin missing or no embed(); check LLM_GATEWAY_URL + LLM_API_KEY so llm plugin initializes." : void 0
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
} catch (embErr) {
|
|
3358
|
+
const detail = embErr instanceof Error ? embErr.message : String(embErr);
|
|
3359
|
+
console.error(`${KB_LOG} embedding step threw before/during batch`, { slug, detail, embErr });
|
|
3360
|
+
return json(
|
|
3361
|
+
{
|
|
3362
|
+
documentId: docId,
|
|
3363
|
+
chunkCount: savedChunks.length,
|
|
3364
|
+
created: true,
|
|
3365
|
+
linked: true,
|
|
3366
|
+
embeddingsWritten: 0,
|
|
3367
|
+
embeddingsFailed: savedChunks.length,
|
|
3368
|
+
warning: "Document saved and linked; embedding step failed.",
|
|
3369
|
+
detail
|
|
3370
|
+
},
|
|
3371
|
+
{ status: 201 }
|
|
3372
|
+
);
|
|
3373
|
+
}
|
|
3374
|
+
return json({
|
|
3375
|
+
documentId: docId,
|
|
3376
|
+
chunkCount: savedChunks.length,
|
|
3377
|
+
created: true,
|
|
3378
|
+
linked: true,
|
|
3379
|
+
embeddingsWritten,
|
|
3380
|
+
embeddingsFailed
|
|
3381
|
+
});
|
|
3382
|
+
} catch (err) {
|
|
3383
|
+
const msg = err instanceof Error ? err.message : "Failed to ingest knowledge";
|
|
3384
|
+
const name = err instanceof Error ? err.name : "";
|
|
3385
|
+
return json({ error: msg, errorName: name || void 0 }, { status: 500 });
|
|
3386
|
+
}
|
|
3387
|
+
},
|
|
3388
|
+
async unlink(req, slug, documentIdStr) {
|
|
3389
|
+
const denied = await gate(req, "update");
|
|
3390
|
+
if (denied) return denied;
|
|
3391
|
+
const documentId = parseInt(documentIdStr, 10);
|
|
3392
|
+
if (!Number.isFinite(documentId)) return json({ error: "Invalid document id" }, { status: 400 });
|
|
3393
|
+
try {
|
|
3394
|
+
const agent = await findAgentBySlug(dataSource, llmAgents, slug);
|
|
3395
|
+
if (!agent) return json({ error: "Agent not found" }, { status: 404 });
|
|
3396
|
+
const linkRepo = dataSource.getRepository(junction);
|
|
3397
|
+
await linkRepo.delete({ agentId: agent.id, documentId });
|
|
3398
|
+
return json({ ok: true });
|
|
3399
|
+
} catch (err) {
|
|
3400
|
+
const msg = err instanceof Error ? err.message : "Failed to unlink";
|
|
3401
|
+
return json({ error: msg }, { status: 500 });
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
|
|
2779
3407
|
// src/message-templates/sms-defaults.ts
|
|
2780
3408
|
var SMS_MESSAGE_TEMPLATE_DEFAULTS = [
|
|
2781
3409
|
{
|
|
@@ -3084,6 +3712,26 @@ function createAdminRolesHandlers(config) {
|
|
|
3084
3712
|
}
|
|
3085
3713
|
|
|
3086
3714
|
// src/api/cms-api-handler.ts
|
|
3715
|
+
var KNOWLEDGE_SUFFIX = "knowledge";
|
|
3716
|
+
function matchLlmAgentKnowledgeRoute(path) {
|
|
3717
|
+
const p = path[0] === "api" ? path.slice(1) : path;
|
|
3718
|
+
if (p[0] !== "llm_agents" || p.length < 2) return null;
|
|
3719
|
+
const seg1 = p[1];
|
|
3720
|
+
if (!seg1) return null;
|
|
3721
|
+
if (p[2] === KNOWLEDGE_SUFFIX) {
|
|
3722
|
+
return {
|
|
3723
|
+
slug: seg1,
|
|
3724
|
+
documentId: p.length >= 4 ? p[3] : void 0
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
if (seg1.endsWith(KNOWLEDGE_SUFFIX) && seg1.length > KNOWLEDGE_SUFFIX.length) {
|
|
3728
|
+
const slug = seg1.slice(0, -KNOWLEDGE_SUFFIX.length);
|
|
3729
|
+
if (!slug) return null;
|
|
3730
|
+
if (p.length === 2) return { slug, documentId: void 0 };
|
|
3731
|
+
if (p.length === 3) return { slug, documentId: p[2] };
|
|
3732
|
+
}
|
|
3733
|
+
return null;
|
|
3734
|
+
}
|
|
3087
3735
|
var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
3088
3736
|
"users",
|
|
3089
3737
|
"password_reset_tokens",
|
|
@@ -3096,7 +3744,8 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
3096
3744
|
"cart_items",
|
|
3097
3745
|
"wishlists",
|
|
3098
3746
|
"wishlist_items",
|
|
3099
|
-
"message_templates"
|
|
3747
|
+
"message_templates",
|
|
3748
|
+
"llm_agent_knowledge_documents"
|
|
3100
3749
|
]);
|
|
3101
3750
|
function createCmsApiHandler(config) {
|
|
3102
3751
|
const {
|
|
@@ -3120,6 +3769,7 @@ function createCmsApiHandler(config) {
|
|
|
3120
3769
|
userProfile,
|
|
3121
3770
|
settings: settingsConfig,
|
|
3122
3771
|
chat: chatConfig,
|
|
3772
|
+
llmAgentKnowledge: llmAgentKnowledgeConfig,
|
|
3123
3773
|
requireEntityPermission: userRequireEntityPermission,
|
|
3124
3774
|
getSessionUser
|
|
3125
3775
|
} = config;
|
|
@@ -3227,6 +3877,17 @@ function createCmsApiHandler(config) {
|
|
|
3227
3877
|
requireEntityPermission: requireEntityPermissionEffective
|
|
3228
3878
|
});
|
|
3229
3879
|
const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
|
|
3880
|
+
const llmAgentKnowledgeMerged = llmAgentKnowledgeConfig ?? (chatConfig ? {
|
|
3881
|
+
dataSource: chatConfig.dataSource,
|
|
3882
|
+
entityMap: chatConfig.entityMap,
|
|
3883
|
+
getCms: chatConfig.getCms,
|
|
3884
|
+
json: chatConfig.json,
|
|
3885
|
+
requireAuth: chatConfig.requireAuth
|
|
3886
|
+
} : void 0);
|
|
3887
|
+
const llmAgentKnowledgeHandlers = llmAgentKnowledgeMerged ? createLlmAgentKnowledgeHandlers({
|
|
3888
|
+
...llmAgentKnowledgeMerged,
|
|
3889
|
+
requireEntityPermission: requireEntityPermissionEffective
|
|
3890
|
+
}) : null;
|
|
3230
3891
|
function resolveResource(segment) {
|
|
3231
3892
|
const model = pathToModel(segment);
|
|
3232
3893
|
return crudResources.includes(model) ? model : segment;
|
|
@@ -3338,7 +3999,32 @@ function createCmsApiHandler(config) {
|
|
|
3338
3999
|
if (method === "GET") return smsMessageTemplateHandlers.GET(req);
|
|
3339
4000
|
if (method === "PUT") return smsMessageTemplateHandlers.PUT(req);
|
|
3340
4001
|
}
|
|
4002
|
+
{
|
|
4003
|
+
const kbMatch = matchLlmAgentKnowledgeRoute(path);
|
|
4004
|
+
if (kbMatch) {
|
|
4005
|
+
if (!llmAgentKnowledgeHandlers) {
|
|
4006
|
+
return config.json(
|
|
4007
|
+
{
|
|
4008
|
+
error: "LLM agent knowledge is not available",
|
|
4009
|
+
hint: "With chat enabled, routes usually work automatically. Otherwise pass llmAgentKnowledge. If this persists, ensure entityMap includes knowledge_base_documents, knowledge_base_chunks, llm_agent_knowledge_documents, and run migrations."
|
|
4010
|
+
},
|
|
4011
|
+
{ status: 503 }
|
|
4012
|
+
);
|
|
4013
|
+
}
|
|
4014
|
+
const { slug, documentId } = kbMatch;
|
|
4015
|
+
if (method === "DELETE" && documentId != null && documentId !== "") {
|
|
4016
|
+
return llmAgentKnowledgeHandlers.unlink(req, slug, documentId);
|
|
4017
|
+
}
|
|
4018
|
+
if (method === "GET" && (documentId == null || documentId === "")) {
|
|
4019
|
+
return llmAgentKnowledgeHandlers.list(req, slug);
|
|
4020
|
+
}
|
|
4021
|
+
if (method === "POST" && (documentId == null || documentId === "")) {
|
|
4022
|
+
return llmAgentKnowledgeHandlers.post(req, slug);
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
3341
4026
|
if (path[0] === "chat" && chatHandlers) {
|
|
4027
|
+
if (path.length === 2 && path[1] === "config" && method === "GET") return chatHandlers.publicConfig(req);
|
|
3342
4028
|
if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
|
|
3343
4029
|
if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
|
|
3344
4030
|
if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
|
|
@@ -3407,7 +4093,7 @@ function createCmsApiHandler(config) {
|
|
|
3407
4093
|
}
|
|
3408
4094
|
|
|
3409
4095
|
// src/api/storefront-handlers.ts
|
|
3410
|
-
var
|
|
4096
|
+
var import_typeorm8 = require("typeorm");
|
|
3411
4097
|
|
|
3412
4098
|
// src/lib/is-valid-signup-email.ts
|
|
3413
4099
|
var MAX_EMAIL = 254;
|
|
@@ -3667,7 +4353,7 @@ async function queueSms(cms, payload) {
|
|
|
3667
4353
|
|
|
3668
4354
|
// src/lib/otp-challenge.ts
|
|
3669
4355
|
var import_crypto = require("crypto");
|
|
3670
|
-
var
|
|
4356
|
+
var import_typeorm7 = require("typeorm");
|
|
3671
4357
|
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
3672
4358
|
var MAX_SENDS_PER_HOUR = 5;
|
|
3673
4359
|
var MAX_VERIFY_ATTEMPTS = 8;
|
|
@@ -3701,7 +4387,7 @@ function normalizePhoneE164(raw, defaultCountryCode) {
|
|
|
3701
4387
|
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
3702
4388
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3703
4389
|
return repo.count({
|
|
3704
|
-
where: { purpose, identifier, createdAt: (0,
|
|
4390
|
+
where: { purpose, identifier, createdAt: (0, import_typeorm7.MoreThan)(since) }
|
|
3705
4391
|
});
|
|
3706
4392
|
}
|
|
3707
4393
|
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
@@ -3715,7 +4401,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
|
|
|
3715
4401
|
await repo.delete({
|
|
3716
4402
|
purpose,
|
|
3717
4403
|
identifier,
|
|
3718
|
-
consumedAt: (0,
|
|
4404
|
+
consumedAt: (0, import_typeorm7.IsNull)()
|
|
3719
4405
|
});
|
|
3720
4406
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
3721
4407
|
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
@@ -3736,7 +4422,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
|
3736
4422
|
const { purpose, identifier, code, pepper } = input;
|
|
3737
4423
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3738
4424
|
const row = await repo.findOne({
|
|
3739
|
-
where: { purpose, identifier, consumedAt: (0,
|
|
4425
|
+
where: { purpose, identifier, consumedAt: (0, import_typeorm7.IsNull)() },
|
|
3740
4426
|
order: { id: "DESC" }
|
|
3741
4427
|
});
|
|
3742
4428
|
if (!row) {
|
|
@@ -3970,7 +4656,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3970
4656
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
3971
4657
|
if (!u) return null;
|
|
3972
4658
|
const unclaimed = await contactRepo().findOne({
|
|
3973
|
-
where: { email: u.email, userId: (0,
|
|
4659
|
+
where: { email: u.email, userId: (0, import_typeorm8.IsNull)(), deleted: false }
|
|
3974
4660
|
});
|
|
3975
4661
|
if (unclaimed) {
|
|
3976
4662
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -5011,7 +5697,7 @@ function createStorefrontApiHandler(config) {
|
|
|
5011
5697
|
const previewByOrder = {};
|
|
5012
5698
|
if (orderIds.length) {
|
|
5013
5699
|
const oItems = await orderItemRepo().find({
|
|
5014
|
-
where: { orderId: (0,
|
|
5700
|
+
where: { orderId: (0, import_typeorm8.In)(orderIds) },
|
|
5015
5701
|
relations: ["product"],
|
|
5016
5702
|
order: { id: "ASC" }
|
|
5017
5703
|
});
|
|
@@ -5148,6 +5834,7 @@ function createStorefrontApiHandler(config) {
|
|
|
5148
5834
|
createForgotPasswordHandler,
|
|
5149
5835
|
createFormBySlugHandler,
|
|
5150
5836
|
createInviteAcceptHandler,
|
|
5837
|
+
createLlmAgentKnowledgeHandlers,
|
|
5151
5838
|
createMediaZipExtractHandler,
|
|
5152
5839
|
createSetPasswordHandler,
|
|
5153
5840
|
createSettingsApiHandlers,
|
|
@@ -5157,6 +5844,10 @@ function createStorefrontApiHandler(config) {
|
|
|
5157
5844
|
createUserAvatarHandler,
|
|
5158
5845
|
createUserProfileHandler,
|
|
5159
5846
|
createUsersApiHandlers,
|
|
5160
|
-
getPublicSettingsGroup
|
|
5847
|
+
getPublicSettingsGroup,
|
|
5848
|
+
mergeGuardrailsIntoSystemPrompt,
|
|
5849
|
+
parseLlmAgentValidationRules,
|
|
5850
|
+
validateUserMessageAgainstAgentRules,
|
|
5851
|
+
validateUserMessageAgainstStructuredRules
|
|
5161
5852
|
});
|
|
5162
5853
|
//# sourceMappingURL=api.cjs.map
|