@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/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
2
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
4
  var __esm = (fn, res) => function __init() {
4
5
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
@@ -7,6 +8,14 @@ var __export = (target, all) => {
7
8
  for (var name in all)
8
9
  __defProp(target, name, { get: all[name], enumerable: true });
9
10
  };
11
+ var __decorateClass = (decorators, target, key, kind) => {
12
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
13
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
14
+ if (decorator = decorators[i])
15
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
16
+ if (kind && result) __defProp(target, key, result);
17
+ return result;
18
+ };
10
19
 
11
20
  // src/plugins/erp/erp-queue.ts
12
21
  async function queueErp(cms, payload) {
@@ -1335,6 +1344,86 @@ async function assertCaptchaOk(getCms, body, req, json) {
1335
1344
  return json({ error: result.message }, { status: result.status });
1336
1345
  }
1337
1346
 
1347
+ // src/entities/llm-agent.entity.ts
1348
+ import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
1349
+ var LlmAgent = class {
1350
+ id;
1351
+ name;
1352
+ slug;
1353
+ systemInstruction;
1354
+ model;
1355
+ temperature;
1356
+ maxTokens;
1357
+ validationRules;
1358
+ enabled;
1359
+ createdAt;
1360
+ updatedAt;
1361
+ deletedAt;
1362
+ deleted;
1363
+ createdBy;
1364
+ updatedBy;
1365
+ deletedBy;
1366
+ };
1367
+ __decorateClass([
1368
+ PrimaryGeneratedColumn()
1369
+ ], LlmAgent.prototype, "id", 2);
1370
+ __decorateClass([
1371
+ Column("varchar")
1372
+ ], LlmAgent.prototype, "name", 2);
1373
+ __decorateClass([
1374
+ Column("varchar")
1375
+ ], LlmAgent.prototype, "slug", 2);
1376
+ __decorateClass([
1377
+ Column("text", { name: "system_instruction", default: "" })
1378
+ ], LlmAgent.prototype, "systemInstruction", 2);
1379
+ __decorateClass([
1380
+ Column("varchar", { nullable: true })
1381
+ ], LlmAgent.prototype, "model", 2);
1382
+ __decorateClass([
1383
+ Column("double precision", { name: "temperature", nullable: true })
1384
+ ], LlmAgent.prototype, "temperature", 2);
1385
+ __decorateClass([
1386
+ Column("int", { name: "max_tokens", nullable: true })
1387
+ ], LlmAgent.prototype, "maxTokens", 2);
1388
+ __decorateClass([
1389
+ Column("text", { name: "validation_rules", nullable: true })
1390
+ ], LlmAgent.prototype, "validationRules", 2);
1391
+ __decorateClass([
1392
+ Column("boolean", { default: true })
1393
+ ], LlmAgent.prototype, "enabled", 2);
1394
+ __decorateClass([
1395
+ Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
1396
+ ], LlmAgent.prototype, "createdAt", 2);
1397
+ __decorateClass([
1398
+ Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
1399
+ ], LlmAgent.prototype, "updatedAt", 2);
1400
+ __decorateClass([
1401
+ Column({ type: "timestamp", nullable: true })
1402
+ ], LlmAgent.prototype, "deletedAt", 2);
1403
+ __decorateClass([
1404
+ Column("boolean", { default: false })
1405
+ ], LlmAgent.prototype, "deleted", 2);
1406
+ __decorateClass([
1407
+ Column("int", { nullable: true })
1408
+ ], LlmAgent.prototype, "createdBy", 2);
1409
+ __decorateClass([
1410
+ Column("int", { nullable: true })
1411
+ ], LlmAgent.prototype, "updatedBy", 2);
1412
+ __decorateClass([
1413
+ Column("int", { nullable: true })
1414
+ ], LlmAgent.prototype, "deletedBy", 2);
1415
+ LlmAgent = __decorateClass([
1416
+ Entity("llm_agents")
1417
+ ], LlmAgent);
1418
+ function llmAgentToChatAgentOptions(agent) {
1419
+ return {
1420
+ systemPrompt: agent.systemInstruction?.trim() || void 0,
1421
+ model: agent.model?.trim() || void 0,
1422
+ temperature: agent.temperature ?? void 0,
1423
+ max_tokens: agent.maxTokens ?? void 0
1424
+ };
1425
+ }
1426
+
1338
1427
  // src/lib/media-folder-path.ts
1339
1428
  function sanitizeMediaFolderPath(input) {
1340
1429
  if (input == null) return "";
@@ -1524,6 +1613,82 @@ async function extractZipMediaIntoParentTree(opts) {
1524
1613
  }
1525
1614
 
1526
1615
  // src/api/cms-handlers.ts
1616
+ function historyBeforeCurrentUser(history, currentUserContent) {
1617
+ const last = history[history.length - 1];
1618
+ if (last?.role === "user" && last.content === currentUserContent) {
1619
+ return history.slice(0, -1);
1620
+ }
1621
+ return [...history];
1622
+ }
1623
+ function pickStructuredRules(rules) {
1624
+ return {
1625
+ maxUserChars: rules.maxUserChars,
1626
+ maxMessageLength: rules.maxMessageLength,
1627
+ minUserChars: rules.minUserChars,
1628
+ minMessageLength: rules.minMessageLength,
1629
+ blockedSubstrings: rules.blockedSubstrings
1630
+ };
1631
+ }
1632
+ function guardrailsTextFromJsonObject(rules) {
1633
+ const g = typeof rules.guardrails === "string" && rules.guardrails.trim() || typeof rules.outputRules === "string" && rules.outputRules.trim() || typeof rules.outputInstructions === "string" && rules.outputInstructions.trim() || "";
1634
+ return g || null;
1635
+ }
1636
+ function parseLlmAgentValidationRules(validationRulesText) {
1637
+ const raw = validationRulesText?.trim();
1638
+ if (!raw) return { structured: {}, guardrailsForPrompt: null };
1639
+ try {
1640
+ const parsed = JSON.parse(raw);
1641
+ if (typeof parsed === "string") {
1642
+ const s = parsed.trim();
1643
+ return { structured: {}, guardrailsForPrompt: s || null };
1644
+ }
1645
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1646
+ return { structured: {}, guardrailsForPrompt: raw };
1647
+ }
1648
+ const rules = parsed;
1649
+ return {
1650
+ structured: pickStructuredRules(rules),
1651
+ guardrailsForPrompt: guardrailsTextFromJsonObject(rules)
1652
+ };
1653
+ } catch {
1654
+ return { structured: {}, guardrailsForPrompt: raw };
1655
+ }
1656
+ }
1657
+ function validateUserMessageAgainstStructuredRules(message, structured) {
1658
+ const maxLen = structured.maxUserChars ?? structured.maxMessageLength;
1659
+ if (typeof maxLen === "number" && Number.isFinite(maxLen) && maxLen >= 0 && message.length > maxLen) {
1660
+ return { ok: false, error: `Message exceeds maximum length (${maxLen} characters)` };
1661
+ }
1662
+ const minLen = structured.minUserChars ?? structured.minMessageLength;
1663
+ if (typeof minLen === "number" && Number.isFinite(minLen) && minLen > 0 && message.length < minLen) {
1664
+ return { ok: false, error: `Message is shorter than minimum length (${minLen} characters)` };
1665
+ }
1666
+ const blocked = structured.blockedSubstrings;
1667
+ if (Array.isArray(blocked) && blocked.length > 0) {
1668
+ const lower = message.toLowerCase();
1669
+ for (const s of blocked) {
1670
+ if (typeof s !== "string" || !s.trim()) continue;
1671
+ if (lower.includes(s.toLowerCase())) {
1672
+ return { ok: false, error: "Message contains disallowed content" };
1673
+ }
1674
+ }
1675
+ }
1676
+ return { ok: true };
1677
+ }
1678
+ function validateUserMessageAgainstAgentRules(message, validationRulesText) {
1679
+ const { structured } = parseLlmAgentValidationRules(validationRulesText);
1680
+ return validateUserMessageAgainstStructuredRules(message, structured);
1681
+ }
1682
+ function mergeGuardrailsIntoSystemPrompt(baseSystem, guardrailsForPrompt) {
1683
+ const g = guardrailsForPrompt?.trim();
1684
+ const b = (baseSystem ?? "").trim();
1685
+ if (!g) return b;
1686
+ const block = `### Output guardrails (follow in every reply)
1687
+ ${g}`;
1688
+ return b ? `${b}
1689
+
1690
+ ${block}` : block;
1691
+ }
1527
1692
  function createDashboardStatsHandler(config) {
1528
1693
  const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
1529
1694
  return async function GET(req) {
@@ -2605,9 +2770,24 @@ function createSettingsApiHandlers(config) {
2605
2770
  }
2606
2771
  var KB_CHUNK_LIMIT = 10;
2607
2772
  var KB_CONTEXT_MAX_CHARS = 4e3;
2773
+ var RAG_LOG = "[rag-context]";
2608
2774
  function getQueryTerms(message) {
2609
2775
  return message.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 2).slice(0, 6);
2610
2776
  }
2777
+ function normalizeChatModeSetting(raw) {
2778
+ if (raw === "external" || raw === "llm") return raw;
2779
+ return "whatsapp";
2780
+ }
2781
+ async function loadLlmSettingsMap(dataSource, entityMap) {
2782
+ if (!entityMap.configs) return {};
2783
+ const repo = dataSource.getRepository(entityMap.configs);
2784
+ const rows = await repo.find({ where: { settings: "llm", deleted: false } });
2785
+ const out = {};
2786
+ for (const row of rows) {
2787
+ out[row.key] = row.value;
2788
+ }
2789
+ return out;
2790
+ }
2611
2791
  function createChatHandlers(config) {
2612
2792
  const { dataSource, entityMap, json, getCms } = config;
2613
2793
  const contactRepo = () => dataSource.getRepository(entityMap.contacts);
@@ -2615,6 +2795,26 @@ function createChatHandlers(config) {
2615
2795
  const msgRepo = () => dataSource.getRepository(entityMap.chat_messages);
2616
2796
  const chunkRepo = () => dataSource.getRepository(entityMap.knowledge_base_chunks);
2617
2797
  return {
2798
+ async publicConfig(_req) {
2799
+ try {
2800
+ const map = await loadLlmSettingsMap(dataSource, entityMap);
2801
+ const mode = normalizeChatModeSetting(map.chatMode);
2802
+ const body = {
2803
+ enabled: map.enabled !== "false",
2804
+ chatMode: mode,
2805
+ agentSlug: mode === "llm" ? (map.attachedAgentSlug ?? "").trim() : "",
2806
+ botName: map.botName ?? "",
2807
+ icon: map.icon ?? "",
2808
+ iconImageUrl: map.iconImageUrl ?? "",
2809
+ iconBackgroundColor: map.iconBackgroundColor ?? "#6366f1",
2810
+ headerColor: map.headerColor ?? "#6366f1",
2811
+ whatsappPhone: map.whatsappPhone ?? ""
2812
+ };
2813
+ return json(body);
2814
+ } catch {
2815
+ return json({ error: "Failed to load chat config" }, { status: 500 });
2816
+ }
2817
+ },
2618
2818
  async identify(req) {
2619
2819
  try {
2620
2820
  const body = await req.json();
@@ -2662,20 +2862,78 @@ function createChatHandlers(config) {
2662
2862
  relations: ["messages"]
2663
2863
  });
2664
2864
  if (!conv) return json({ error: "Conversation not found" }, { status: 404 });
2665
- const msgRepoInst = msgRepo();
2666
- await msgRepoInst.save(msgRepoInst.create({ conversationId, role: "user", content: message }));
2667
2865
  const cms = await getCms();
2668
2866
  const llm = cms.getPlugin("llm");
2669
2867
  if (!llm?.chat) return json({ error: "LLM not configured" }, { status: 503 });
2868
+ const llmSettings = await loadLlmSettingsMap(dataSource, entityMap);
2869
+ const supportMode = normalizeChatModeSetting(llmSettings.chatMode);
2870
+ let effectiveSlug = (body?.agentSlug ?? "").trim();
2871
+ if (!effectiveSlug && supportMode === "llm" && entityMap.llm_agents) {
2872
+ effectiveSlug = (llmSettings.attachedAgentSlug ?? "").trim();
2873
+ }
2874
+ let agentRow = null;
2875
+ if (effectiveSlug) {
2876
+ if (!entityMap.llm_agents) {
2877
+ return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
2878
+ }
2879
+ const agentRepo = dataSource.getRepository(
2880
+ entityMap.llm_agents
2881
+ );
2882
+ agentRow = await agentRepo.findOne({
2883
+ where: { slug: effectiveSlug, deleted: false, enabled: true }
2884
+ });
2885
+ if (!agentRow && (body?.agentSlug ?? "").trim()) {
2886
+ return json({ error: "Agent not found or disabled", agentSlug: effectiveSlug }, { status: 404 });
2887
+ }
2888
+ }
2889
+ console.info(RAG_LOG, "step 1 | resolve agent", {
2890
+ agentSlug: effectiveSlug || "(none)",
2891
+ agentFound: !!agentRow,
2892
+ agentId: agentRow?.id ?? null
2893
+ });
2894
+ const parsedValidation = agentRow ? parseLlmAgentValidationRules(agentRow.validationRules) : { structured: {}, guardrailsForPrompt: null };
2895
+ if (agentRow) {
2896
+ const v = validateUserMessageAgainstStructuredRules(message, parsedValidation.structured);
2897
+ if (!v.ok) return json({ error: v.error, reason: "validation_failed" }, { status: 400 });
2898
+ }
2899
+ let kbDocumentScope;
2900
+ const Junction = entityMap.llm_agent_knowledge_documents;
2901
+ if (agentRow && Junction) {
2902
+ const linkRepo = dataSource.getRepository(Junction);
2903
+ const links = await linkRepo.find({ where: { agentId: agentRow.id } });
2904
+ const ids = [...new Set(links.map((l) => l.documentId))];
2905
+ if (ids.length > 0) kbDocumentScope = ids;
2906
+ }
2907
+ console.info(RAG_LOG, "step 2 | knowledge scope", {
2908
+ scopedDocumentIds: kbDocumentScope ?? "(all documents)",
2909
+ documentCount: kbDocumentScope?.length ?? "all"
2910
+ });
2911
+ const msgRepoInst = msgRepo();
2912
+ await msgRepoInst.save(msgRepoInst.create({ conversationId, role: "user", content: message }));
2670
2913
  let contextParts = [];
2914
+ console.info(RAG_LOG, "step 3 | embed user query", {
2915
+ messageChars: message.length,
2916
+ embedAvailable: !!llm.embed
2917
+ });
2671
2918
  const queryEmbedding = llm.embed ? await llm.embed(message) : null;
2919
+ console.info(RAG_LOG, "step 4 | query embedding result", {
2920
+ dimensions: queryEmbedding?.length ?? 0,
2921
+ hasEmbedding: !!(queryEmbedding && queryEmbedding.length > 0)
2922
+ });
2672
2923
  if (queryEmbedding && queryEmbedding.length > 0) {
2673
2924
  const vectorStr = "[" + queryEmbedding.join(",") + "]";
2674
2925
  try {
2675
- const rows = await dataSource.query(
2926
+ const rows = kbDocumentScope?.length ? await dataSource.query(
2927
+ `SELECT id, content FROM knowledge_base_chunks WHERE embedding IS NOT NULL AND "documentId" = ANY($3::int[]) ORDER BY embedding <=> $1::vector LIMIT $2`,
2928
+ [vectorStr, KB_CHUNK_LIMIT, kbDocumentScope]
2929
+ ) : await dataSource.query(
2676
2930
  `SELECT id, content FROM knowledge_base_chunks WHERE embedding IS NOT NULL ORDER BY embedding <=> $1::vector LIMIT $2`,
2677
2931
  [vectorStr, KB_CHUNK_LIMIT]
2678
2932
  );
2933
+ console.info(RAG_LOG, "step 5 | vector search results", {
2934
+ rowsReturned: rows.length,
2935
+ chunkIds: rows.map((r) => r.id)
2936
+ });
2679
2937
  let totalLen = 0;
2680
2938
  for (const r of rows) {
2681
2939
  const text = (r.content ?? "").trim();
@@ -2683,13 +2941,24 @@ function createChatHandlers(config) {
2683
2941
  contextParts.push(text);
2684
2942
  totalLen += text.length;
2685
2943
  }
2686
- } catch {
2944
+ console.info(RAG_LOG, "step 5a | vector context selected", {
2945
+ chunksUsed: contextParts.length,
2946
+ totalChars: totalLen
2947
+ });
2948
+ } catch (vecErr) {
2949
+ console.warn(RAG_LOG, "step 5 | vector search failed; falling back to keyword", {
2950
+ err: vecErr instanceof Error ? vecErr.message : String(vecErr)
2951
+ });
2687
2952
  }
2688
2953
  }
2689
2954
  if (contextParts.length === 0) {
2690
2955
  const terms = getQueryTerms(message);
2956
+ console.info(RAG_LOG, "step 6 | keyword fallback", {
2957
+ reason: !(queryEmbedding && queryEmbedding.length > 0) ? "no embedding" : "vector search returned nothing",
2958
+ searchTerms: terms
2959
+ });
2691
2960
  if (terms.length > 0) {
2692
- const conditions = terms.map((t) => ({ content: ILike2(`%${t}%`) }));
2961
+ const conditions = kbDocumentScope?.length ? terms.map((t) => ({ content: ILike2(`%${t}%`), documentId: In(kbDocumentScope) })) : terms.map((t) => ({ content: ILike2(`%${t}%`) }));
2693
2962
  const chunks = await chunkRepo().find({
2694
2963
  where: conditions,
2695
2964
  take: KB_CHUNK_LIMIT,
@@ -2704,19 +2973,66 @@ function createChatHandlers(config) {
2704
2973
  contextParts.push(text);
2705
2974
  totalLen += text.length;
2706
2975
  }
2976
+ console.info(RAG_LOG, "step 6a | keyword results", {
2977
+ chunksFound: chunks.length,
2978
+ chunksUsed: contextParts.length,
2979
+ totalChars: totalLen
2980
+ });
2707
2981
  }
2708
2982
  }
2709
- const history = (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 }));
2710
- const systemContent = 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.
2983
+ 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 }));
2984
+ const history = historyBeforeCurrentUser(historyRaw, message);
2985
+ let content;
2986
+ const ragContext = contextParts.length > 0 ? contextParts.join("\n\n") : void 0;
2987
+ console.info(RAG_LOG, "step 7 | final context", {
2988
+ method: contextParts.length > 0 ? "rag" : "none",
2989
+ contextChunks: contextParts.length,
2990
+ contextChars: ragContext?.length ?? 0,
2991
+ contextPreview: ragContext ? ragContext.slice(0, 200) + (ragContext.length > 200 ? "\u2026" : "") : "(no context)"
2992
+ });
2993
+ if (agentRow && llm.chatAgent) {
2994
+ const fromAgent = llmAgentToChatAgentOptions(agentRow);
2995
+ const systemPrompt = mergeGuardrailsIntoSystemPrompt(
2996
+ fromAgent.systemPrompt,
2997
+ parsedValidation.guardrailsForPrompt
2998
+ );
2999
+ const res = await llm.chatAgent({
3000
+ ...fromAgent,
3001
+ systemPrompt: systemPrompt || void 0,
3002
+ context: ragContext,
3003
+ history,
3004
+ userPrompt: message
3005
+ });
3006
+ content = res.content;
3007
+ } else {
3008
+ 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.
2711
3009
 
2712
3010
  Context:
2713
- ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If you do not have specific information, say so.";
2714
- const messages = [
2715
- { role: "system", content: systemContent },
2716
- ...history,
2717
- { role: "user", content: message }
2718
- ];
2719
- const { content } = await llm.chat(messages);
3011
+ ${contextParts.join("\n\n")}` : "";
3012
+ const defaultSystem = "You are a helpful assistant for the company. If you do not have specific information, say so.";
3013
+ let systemContent;
3014
+ if (agentRow) {
3015
+ const base = agentRow.systemInstruction?.trim() || "";
3016
+ systemContent = mergeGuardrailsIntoSystemPrompt(
3017
+ [base, ragSystem].filter(Boolean).join("\n\n") || defaultSystem,
3018
+ parsedValidation.guardrailsForPrompt
3019
+ );
3020
+ } else {
3021
+ systemContent = ragSystem || defaultSystem;
3022
+ }
3023
+ const messages = [
3024
+ { role: "system", content: systemContent },
3025
+ ...history,
3026
+ { role: "user", content: message }
3027
+ ];
3028
+ const chatOpts = agentRow ? {
3029
+ model: agentRow.model ?? void 0,
3030
+ temperature: agentRow.temperature ?? void 0,
3031
+ max_tokens: agentRow.maxTokens ?? void 0
3032
+ } : {};
3033
+ const res = await llm.chat(messages, chatOpts);
3034
+ content = res.content;
3035
+ }
2720
3036
  await msgRepoInst.save(msgRepoInst.create({ conversationId, role: "assistant", content }));
2721
3037
  return json({ content });
2722
3038
  } catch (err) {
@@ -2727,6 +3043,314 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
2727
3043
  };
2728
3044
  }
2729
3045
 
3046
+ // src/api/llm-agent-knowledge-handlers.ts
3047
+ import { In as In2 } from "typeorm";
3048
+ var INGEST_CHUNK_CHARS = 900;
3049
+ var MAX_CHUNKS_PER_UPLOAD = 400;
3050
+ var EMBED_CONCURRENCY = 5;
3051
+ var MAX_PDF_BYTES = 25 * 1024 * 1024;
3052
+ var TEXT_FILE_TYPES = /* @__PURE__ */ new Set(["text/plain", "text/markdown", "application/json"]);
3053
+ var KB_LOG = "[llm-agent-knowledge]";
3054
+ function llmEmbedDebug() {
3055
+ const v = process.env.LLM_EMBED_DEBUG?.toLowerCase();
3056
+ return v === "1" || v === "true" || v === "yes";
3057
+ }
3058
+ function isPdfUpload(mime, fileName) {
3059
+ if (mime === "application/pdf") return true;
3060
+ const n = fileName.toLowerCase();
3061
+ return n.endsWith(".pdf");
3062
+ }
3063
+ async function extractTextFromPdf(buffer) {
3064
+ const pdfParse = await import("pdf-parse");
3065
+ const data = await pdfParse(buffer);
3066
+ return (data?.text ?? "").trim();
3067
+ }
3068
+ async function writeEmbeddingsConcurrent(dataSource, embed, chunks, concurrency) {
3069
+ let next = 0;
3070
+ let written = 0;
3071
+ let failed = 0;
3072
+ let skippedEmpty = 0;
3073
+ async function worker() {
3074
+ for (; ; ) {
3075
+ const i = next++;
3076
+ if (i >= chunks.length) return;
3077
+ const c = chunks[i];
3078
+ try {
3079
+ const emb = await embed(c.content);
3080
+ if (emb?.length) {
3081
+ const vectorStr = "[" + emb.join(",") + "]";
3082
+ await dataSource.query(
3083
+ `UPDATE knowledge_base_chunks SET embedding = $1::vector WHERE id = $2`,
3084
+ [vectorStr, c.id]
3085
+ );
3086
+ written++;
3087
+ } else {
3088
+ skippedEmpty++;
3089
+ if (llmEmbedDebug()) {
3090
+ console.warn(`${KB_LOG} embed() returned empty vector`, { chunkId: c.id });
3091
+ }
3092
+ }
3093
+ } catch (err) {
3094
+ failed++;
3095
+ console.error(`${KB_LOG} embedding DB update failed`, {
3096
+ chunkId: c.id,
3097
+ err: err instanceof Error ? err.message : String(err)
3098
+ });
3099
+ }
3100
+ }
3101
+ }
3102
+ const n = Math.max(1, Math.min(concurrency, chunks.length));
3103
+ await Promise.all(Array.from({ length: n }, () => worker()));
3104
+ const summary = {
3105
+ chunkCount: chunks.length,
3106
+ written,
3107
+ failed,
3108
+ skippedEmpty
3109
+ };
3110
+ if (skippedEmpty > 0 && written === 0) {
3111
+ summary.hint = "embed() returned empty vectors \u2014 see [LLM embed] logs (often HTTP 404 on /v1/embeddings, or wrong response shape).";
3112
+ }
3113
+ console.error(`${KB_LOG} embedding pass finished`, summary);
3114
+ return { written, failed };
3115
+ }
3116
+ function splitIntoChunks(text, maxLen) {
3117
+ const t = text.trim();
3118
+ if (!t) return [];
3119
+ const chunks = [];
3120
+ for (let i = 0; i < t.length; i += maxLen) {
3121
+ chunks.push(t.slice(i, i + maxLen));
3122
+ }
3123
+ return chunks;
3124
+ }
3125
+ async function findAgentBySlug(dataSource, llmAgents, slug) {
3126
+ const repo = dataSource.getRepository(llmAgents);
3127
+ return repo.findOne({
3128
+ where: { slug, deleted: false }
3129
+ });
3130
+ }
3131
+ function createLlmAgentKnowledgeHandlers(config) {
3132
+ const { dataSource, entityMap, getCms, json, requireAuth, requireEntityPermission } = config;
3133
+ const kbDoc = entityMap.knowledge_base_documents;
3134
+ const kbChunk = entityMap.knowledge_base_chunks;
3135
+ const llmAgents = entityMap.llm_agents;
3136
+ const junction = entityMap.llm_agent_knowledge_documents;
3137
+ if (!kbDoc || !kbChunk || !llmAgents || !junction) {
3138
+ return null;
3139
+ }
3140
+ async function gate(req, action) {
3141
+ const a = await requireAuth(req);
3142
+ if (a) return a;
3143
+ if (requireEntityPermission) {
3144
+ const pe = await requireEntityPermission(req, "llm_agents", action);
3145
+ if (pe) return pe;
3146
+ }
3147
+ return null;
3148
+ }
3149
+ return {
3150
+ async list(req, slug) {
3151
+ const denied = await gate(req, "read");
3152
+ if (denied) return denied;
3153
+ try {
3154
+ const agent = await findAgentBySlug(dataSource, llmAgents, slug);
3155
+ if (!agent) return json({ error: "Agent not found" }, { status: 404 });
3156
+ const linkRepo = dataSource.getRepository(junction);
3157
+ const links = await linkRepo.find({ where: { agentId: agent.id } });
3158
+ const docIds = [...new Set(links.map((l) => l.documentId))];
3159
+ if (docIds.length === 0) return json({ documents: [] });
3160
+ const docRepo = dataSource.getRepository(kbDoc);
3161
+ const docs = await docRepo.find({ where: { id: In2(docIds) } });
3162
+ const byId = new Map(docs.map((d) => [d.id, d]));
3163
+ const documents = docIds.map((id) => {
3164
+ const d = byId.get(id);
3165
+ return d ? { id: d.id, name: d.name } : null;
3166
+ }).filter(Boolean);
3167
+ return json({ documents });
3168
+ } catch (err) {
3169
+ const msg = err instanceof Error ? err.message : "Failed to list knowledge";
3170
+ return json({ error: msg }, { status: 500 });
3171
+ }
3172
+ },
3173
+ async post(req, slug) {
3174
+ const denied = await gate(req, "update");
3175
+ if (denied) return denied;
3176
+ try {
3177
+ const agent = await findAgentBySlug(dataSource, llmAgents, slug);
3178
+ if (!agent) return json({ error: "Agent not found" }, { status: 404 });
3179
+ let name = "";
3180
+ let text = "";
3181
+ let sourceUrl = null;
3182
+ let existingDocumentId = null;
3183
+ const ct = req.headers.get("content-type") || "";
3184
+ if (ct.includes("application/json")) {
3185
+ const body = await req.json();
3186
+ existingDocumentId = typeof body?.documentId === "number" && Number.isFinite(body.documentId) ? body.documentId : null;
3187
+ name = (body?.name ?? "").trim();
3188
+ text = (body?.text ?? "").trim();
3189
+ sourceUrl = typeof body?.sourceUrl === "string" && body.sourceUrl.trim() ? body.sourceUrl.trim() : null;
3190
+ } else if (ct.includes("multipart/form-data")) {
3191
+ const form = await req.formData();
3192
+ name = form.get("name")?.trim() ?? "";
3193
+ text = form.get("text")?.trim() ?? "";
3194
+ const file = form.get("file");
3195
+ if (file && typeof file !== "string" && "arrayBuffer" in file) {
3196
+ const f = file;
3197
+ const mime = (f.type || "").split(";")[0].trim().toLowerCase();
3198
+ const buf = Buffer.from(await f.arrayBuffer());
3199
+ if (TEXT_FILE_TYPES.has(mime)) {
3200
+ const decoded = buf.toString("utf8");
3201
+ if (!text) text = decoded;
3202
+ } else if (isPdfUpload(mime, f.name || "")) {
3203
+ if (buf.length > MAX_PDF_BYTES) {
3204
+ return json(
3205
+ { error: `PDF too large (max ${Math.floor(MAX_PDF_BYTES / (1024 * 1024))}MB)` },
3206
+ { status: 413 }
3207
+ );
3208
+ }
3209
+ try {
3210
+ const extracted = await extractTextFromPdf(buf);
3211
+ if (!text) text = extracted;
3212
+ } catch {
3213
+ return json(
3214
+ { error: "Could not read PDF text (file may be encrypted, corrupt, or image-only)" },
3215
+ { status: 422 }
3216
+ );
3217
+ }
3218
+ } else {
3219
+ return json(
3220
+ {
3221
+ error: "Unsupported file type; use text/plain, text/markdown, application/json, or application/pdf"
3222
+ },
3223
+ { status: 415 }
3224
+ );
3225
+ }
3226
+ if (!name && f.name) name = f.name.replace(/\.[^/.]+$/, "") || f.name;
3227
+ }
3228
+ } else {
3229
+ return json({ error: "Use application/json or multipart/form-data" }, { status: 400 });
3230
+ }
3231
+ const linkRepo = dataSource.getRepository(junction);
3232
+ if (existingDocumentId != null) {
3233
+ const docRepo2 = dataSource.getRepository(kbDoc);
3234
+ const existing = await docRepo2.findOne({ where: { id: existingDocumentId } });
3235
+ if (!existing) return json({ error: "documentId not found" }, { status: 404 });
3236
+ const dup2 = await linkRepo.findOne({
3237
+ where: { agentId: agent.id, documentId: existingDocumentId }
3238
+ });
3239
+ if (!dup2) {
3240
+ await linkRepo.save(linkRepo.create({ agentId: agent.id, documentId: existingDocumentId }));
3241
+ }
3242
+ return json({ documentId: existingDocumentId, linked: true, created: false });
3243
+ }
3244
+ if (!text) return json({ error: "text or file with text content is required" }, { status: 400 });
3245
+ if (!name) name = "Untitled";
3246
+ const parts = splitIntoChunks(text, INGEST_CHUNK_CHARS);
3247
+ if (parts.length === 0) {
3248
+ return json({ error: "text or file with text content is required" }, { status: 400 });
3249
+ }
3250
+ if (parts.length > MAX_CHUNKS_PER_UPLOAD) {
3251
+ return json(
3252
+ {
3253
+ error: `Document is too large for one upload (${parts.length} chunks; max ${MAX_CHUNKS_PER_UPLOAD}). Split into smaller files.`
3254
+ },
3255
+ { status: 413 }
3256
+ );
3257
+ }
3258
+ const docRepo = dataSource.getRepository(kbDoc);
3259
+ const chunkRepo = dataSource.getRepository(kbChunk);
3260
+ const now = /* @__PURE__ */ new Date();
3261
+ const doc = await docRepo.save(
3262
+ docRepo.create({ name, content: text, sourceUrl, createdAt: now, updatedAt: now })
3263
+ );
3264
+ const docId = doc.id;
3265
+ const chunkRows = parts.map(
3266
+ (content, i) => chunkRepo.create({ documentId: docId, content, chunkIndex: i, createdAt: now })
3267
+ );
3268
+ const savedList = await chunkRepo.save(chunkRows);
3269
+ const savedChunks = savedList.map(
3270
+ (row, i) => ({
3271
+ id: row.id,
3272
+ content: parts[i]
3273
+ })
3274
+ );
3275
+ const dup = await linkRepo.findOne({ where: { agentId: agent.id, documentId: docId } });
3276
+ if (!dup) {
3277
+ await linkRepo.save(linkRepo.create({ agentId: agent.id, documentId: docId }));
3278
+ }
3279
+ let embeddingsWritten = 0;
3280
+ let embeddingsFailed = 0;
3281
+ try {
3282
+ const cms = await getCms();
3283
+ const llm = cms.getPlugin("llm");
3284
+ if (llm?.embed && savedChunks.length > 0) {
3285
+ console.info(`${KB_LOG} starting embedding pass`, { slug, chunkCount: savedChunks.length });
3286
+ const embedBound = (text2) => llm.embed(text2);
3287
+ const { written, failed } = await writeEmbeddingsConcurrent(
3288
+ dataSource,
3289
+ embedBound,
3290
+ savedChunks,
3291
+ EMBED_CONCURRENCY
3292
+ );
3293
+ embeddingsWritten = written;
3294
+ embeddingsFailed = failed;
3295
+ } else {
3296
+ console.error(`${KB_LOG} embeddings skipped`, {
3297
+ slug,
3298
+ hasLlmPlugin: !!llm,
3299
+ hasEmbed: typeof llm?.embed === "function",
3300
+ chunkCount: savedChunks.length,
3301
+ 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
3302
+ });
3303
+ }
3304
+ } catch (embErr) {
3305
+ const detail = embErr instanceof Error ? embErr.message : String(embErr);
3306
+ console.error(`${KB_LOG} embedding step threw before/during batch`, { slug, detail, embErr });
3307
+ return json(
3308
+ {
3309
+ documentId: docId,
3310
+ chunkCount: savedChunks.length,
3311
+ created: true,
3312
+ linked: true,
3313
+ embeddingsWritten: 0,
3314
+ embeddingsFailed: savedChunks.length,
3315
+ warning: "Document saved and linked; embedding step failed.",
3316
+ detail
3317
+ },
3318
+ { status: 201 }
3319
+ );
3320
+ }
3321
+ return json({
3322
+ documentId: docId,
3323
+ chunkCount: savedChunks.length,
3324
+ created: true,
3325
+ linked: true,
3326
+ embeddingsWritten,
3327
+ embeddingsFailed
3328
+ });
3329
+ } catch (err) {
3330
+ const msg = err instanceof Error ? err.message : "Failed to ingest knowledge";
3331
+ const name = err instanceof Error ? err.name : "";
3332
+ return json({ error: msg, errorName: name || void 0 }, { status: 500 });
3333
+ }
3334
+ },
3335
+ async unlink(req, slug, documentIdStr) {
3336
+ const denied = await gate(req, "update");
3337
+ if (denied) return denied;
3338
+ const documentId = parseInt(documentIdStr, 10);
3339
+ if (!Number.isFinite(documentId)) return json({ error: "Invalid document id" }, { status: 400 });
3340
+ try {
3341
+ const agent = await findAgentBySlug(dataSource, llmAgents, slug);
3342
+ if (!agent) return json({ error: "Agent not found" }, { status: 404 });
3343
+ const linkRepo = dataSource.getRepository(junction);
3344
+ await linkRepo.delete({ agentId: agent.id, documentId });
3345
+ return json({ ok: true });
3346
+ } catch (err) {
3347
+ const msg = err instanceof Error ? err.message : "Failed to unlink";
3348
+ return json({ error: msg }, { status: 500 });
3349
+ }
3350
+ }
3351
+ };
3352
+ }
3353
+
2730
3354
  // src/message-templates/sms-defaults.ts
2731
3355
  var SMS_MESSAGE_TEMPLATE_DEFAULTS = [
2732
3356
  {
@@ -3035,6 +3659,26 @@ function createAdminRolesHandlers(config) {
3035
3659
  }
3036
3660
 
3037
3661
  // src/api/cms-api-handler.ts
3662
+ var KNOWLEDGE_SUFFIX = "knowledge";
3663
+ function matchLlmAgentKnowledgeRoute(path) {
3664
+ const p = path[0] === "api" ? path.slice(1) : path;
3665
+ if (p[0] !== "llm_agents" || p.length < 2) return null;
3666
+ const seg1 = p[1];
3667
+ if (!seg1) return null;
3668
+ if (p[2] === KNOWLEDGE_SUFFIX) {
3669
+ return {
3670
+ slug: seg1,
3671
+ documentId: p.length >= 4 ? p[3] : void 0
3672
+ };
3673
+ }
3674
+ if (seg1.endsWith(KNOWLEDGE_SUFFIX) && seg1.length > KNOWLEDGE_SUFFIX.length) {
3675
+ const slug = seg1.slice(0, -KNOWLEDGE_SUFFIX.length);
3676
+ if (!slug) return null;
3677
+ if (p.length === 2) return { slug, documentId: void 0 };
3678
+ if (p.length === 3) return { slug, documentId: p[2] };
3679
+ }
3680
+ return null;
3681
+ }
3038
3682
  var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
3039
3683
  "users",
3040
3684
  "password_reset_tokens",
@@ -3047,7 +3691,8 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
3047
3691
  "cart_items",
3048
3692
  "wishlists",
3049
3693
  "wishlist_items",
3050
- "message_templates"
3694
+ "message_templates",
3695
+ "llm_agent_knowledge_documents"
3051
3696
  ]);
3052
3697
  function createCmsApiHandler(config) {
3053
3698
  const {
@@ -3071,6 +3716,7 @@ function createCmsApiHandler(config) {
3071
3716
  userProfile,
3072
3717
  settings: settingsConfig,
3073
3718
  chat: chatConfig,
3719
+ llmAgentKnowledge: llmAgentKnowledgeConfig,
3074
3720
  requireEntityPermission: userRequireEntityPermission,
3075
3721
  getSessionUser
3076
3722
  } = config;
@@ -3178,6 +3824,17 @@ function createCmsApiHandler(config) {
3178
3824
  requireEntityPermission: requireEntityPermissionEffective
3179
3825
  });
3180
3826
  const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
3827
+ const llmAgentKnowledgeMerged = llmAgentKnowledgeConfig ?? (chatConfig ? {
3828
+ dataSource: chatConfig.dataSource,
3829
+ entityMap: chatConfig.entityMap,
3830
+ getCms: chatConfig.getCms,
3831
+ json: chatConfig.json,
3832
+ requireAuth: chatConfig.requireAuth
3833
+ } : void 0);
3834
+ const llmAgentKnowledgeHandlers = llmAgentKnowledgeMerged ? createLlmAgentKnowledgeHandlers({
3835
+ ...llmAgentKnowledgeMerged,
3836
+ requireEntityPermission: requireEntityPermissionEffective
3837
+ }) : null;
3181
3838
  function resolveResource(segment) {
3182
3839
  const model = pathToModel(segment);
3183
3840
  return crudResources.includes(model) ? model : segment;
@@ -3289,7 +3946,32 @@ function createCmsApiHandler(config) {
3289
3946
  if (method === "GET") return smsMessageTemplateHandlers.GET(req);
3290
3947
  if (method === "PUT") return smsMessageTemplateHandlers.PUT(req);
3291
3948
  }
3949
+ {
3950
+ const kbMatch = matchLlmAgentKnowledgeRoute(path);
3951
+ if (kbMatch) {
3952
+ if (!llmAgentKnowledgeHandlers) {
3953
+ return config.json(
3954
+ {
3955
+ error: "LLM agent knowledge is not available",
3956
+ 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."
3957
+ },
3958
+ { status: 503 }
3959
+ );
3960
+ }
3961
+ const { slug, documentId } = kbMatch;
3962
+ if (method === "DELETE" && documentId != null && documentId !== "") {
3963
+ return llmAgentKnowledgeHandlers.unlink(req, slug, documentId);
3964
+ }
3965
+ if (method === "GET" && (documentId == null || documentId === "")) {
3966
+ return llmAgentKnowledgeHandlers.list(req, slug);
3967
+ }
3968
+ if (method === "POST" && (documentId == null || documentId === "")) {
3969
+ return llmAgentKnowledgeHandlers.post(req, slug);
3970
+ }
3971
+ }
3972
+ }
3292
3973
  if (path[0] === "chat" && chatHandlers) {
3974
+ if (path.length === 2 && path[1] === "config" && method === "GET") return chatHandlers.publicConfig(req);
3293
3975
  if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
3294
3976
  if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
3295
3977
  if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
@@ -3358,7 +4040,7 @@ function createCmsApiHandler(config) {
3358
4040
  }
3359
4041
 
3360
4042
  // src/api/storefront-handlers.ts
3361
- import { In as In2, IsNull as IsNull4 } from "typeorm";
4043
+ import { In as In3, IsNull as IsNull4 } from "typeorm";
3362
4044
 
3363
4045
  // src/lib/is-valid-signup-email.ts
3364
4046
  var MAX_EMAIL = 254;
@@ -4962,7 +5644,7 @@ function createStorefrontApiHandler(config) {
4962
5644
  const previewByOrder = {};
4963
5645
  if (orderIds.length) {
4964
5646
  const oItems = await orderItemRepo().find({
4965
- where: { orderId: In2(orderIds) },
5647
+ where: { orderId: In3(orderIds) },
4966
5648
  relations: ["product"],
4967
5649
  order: { id: "ASC" }
4968
5650
  });
@@ -5098,6 +5780,7 @@ export {
5098
5780
  createForgotPasswordHandler,
5099
5781
  createFormBySlugHandler,
5100
5782
  createInviteAcceptHandler,
5783
+ createLlmAgentKnowledgeHandlers,
5101
5784
  createMediaZipExtractHandler,
5102
5785
  createSetPasswordHandler,
5103
5786
  createSettingsApiHandlers,
@@ -5107,6 +5790,10 @@ export {
5107
5790
  createUserAvatarHandler,
5108
5791
  createUserProfileHandler,
5109
5792
  createUsersApiHandlers,
5110
- getPublicSettingsGroup
5793
+ getPublicSettingsGroup,
5794
+ mergeGuardrailsIntoSystemPrompt,
5795
+ parseLlmAgentValidationRules,
5796
+ validateUserMessageAgainstAgentRules,
5797
+ validateUserMessageAgainstStructuredRules
5111
5798
  };
5112
5799
  //# sourceMappingURL=api.js.map