@infuro/cms-core 1.0.7 → 1.0.8

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.cjs CHANGED
@@ -1271,6 +1271,129 @@ function createSettingsApiHandlers(config) {
1271
1271
  }
1272
1272
  };
1273
1273
  }
1274
+ var KB_CHUNK_LIMIT = 10;
1275
+ var KB_CONTEXT_MAX_CHARS = 4e3;
1276
+ function getQueryTerms(message) {
1277
+ return message.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 2).slice(0, 6);
1278
+ }
1279
+ function createChatHandlers(config) {
1280
+ const { dataSource, entityMap, json, getCms } = config;
1281
+ const contactRepo = () => dataSource.getRepository(entityMap.contacts);
1282
+ const convRepo = () => dataSource.getRepository(entityMap.chat_conversations);
1283
+ const msgRepo = () => dataSource.getRepository(entityMap.chat_messages);
1284
+ const chunkRepo = () => dataSource.getRepository(entityMap.knowledge_base_chunks);
1285
+ return {
1286
+ async identify(req) {
1287
+ try {
1288
+ const body = await req.json();
1289
+ const name = body?.name?.trim();
1290
+ const email = body?.email?.trim();
1291
+ if (!name || !email) return json({ error: "name and email required" }, { status: 400 });
1292
+ const repo = contactRepo();
1293
+ let contact = await repo.findOne({ where: { email, deleted: false } });
1294
+ if (!contact) {
1295
+ const created = repo.create({ name, email, phone: body.phone?.trim() || null });
1296
+ contact = await repo.save(created);
1297
+ }
1298
+ const convRepoInst = convRepo();
1299
+ const conv = await convRepoInst.save(convRepoInst.create({ contactId: contact.id }));
1300
+ return json({
1301
+ contactId: contact.id,
1302
+ conversationId: conv.id
1303
+ });
1304
+ } catch (err) {
1305
+ const message = err instanceof Error ? err.message : "Failed to identify";
1306
+ return json({ error: "Failed to identify", detail: message }, { status: 500 });
1307
+ }
1308
+ },
1309
+ async getMessages(req, conversationId) {
1310
+ try {
1311
+ const conv = await convRepo().findOne({
1312
+ where: { id: parseInt(conversationId, 10) },
1313
+ relations: ["messages"]
1314
+ });
1315
+ if (!conv) return json({ error: "Conversation not found" }, { status: 404 });
1316
+ const messages = (conv.messages ?? []).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()).map((m) => ({ role: m.role, content: m.content }));
1317
+ return json({ messages });
1318
+ } catch {
1319
+ return json({ error: "Failed to fetch messages" }, { status: 500 });
1320
+ }
1321
+ },
1322
+ async postMessage(req) {
1323
+ try {
1324
+ const body = await req.json();
1325
+ const conversationId = body?.conversationId;
1326
+ const message = body?.message?.trim();
1327
+ if (!conversationId || !message) return json({ error: "conversationId and message required" }, { status: 400 });
1328
+ const conv = await convRepo().findOne({
1329
+ where: { id: conversationId },
1330
+ relations: ["messages"]
1331
+ });
1332
+ if (!conv) return json({ error: "Conversation not found" }, { status: 404 });
1333
+ const msgRepoInst = msgRepo();
1334
+ await msgRepoInst.save(msgRepoInst.create({ conversationId, role: "user", content: message }));
1335
+ const cms = await getCms();
1336
+ const llm = cms.getPlugin("llm");
1337
+ if (!llm?.chat) return json({ error: "LLM not configured" }, { status: 503 });
1338
+ let contextParts = [];
1339
+ const queryEmbedding = llm.embed ? await llm.embed(message) : null;
1340
+ if (queryEmbedding && queryEmbedding.length > 0) {
1341
+ const vectorStr = "[" + queryEmbedding.join(",") + "]";
1342
+ try {
1343
+ const rows = await dataSource.query(
1344
+ `SELECT id, content FROM knowledge_base_chunks WHERE embedding IS NOT NULL ORDER BY embedding <=> $1::vector LIMIT $2`,
1345
+ [vectorStr, KB_CHUNK_LIMIT]
1346
+ );
1347
+ let totalLen = 0;
1348
+ for (const r of rows) {
1349
+ const text = (r.content ?? "").trim();
1350
+ if (!text || totalLen + text.length > KB_CONTEXT_MAX_CHARS) continue;
1351
+ contextParts.push(text);
1352
+ totalLen += text.length;
1353
+ }
1354
+ } catch {
1355
+ }
1356
+ }
1357
+ if (contextParts.length === 0) {
1358
+ const terms = getQueryTerms(message);
1359
+ if (terms.length > 0) {
1360
+ const conditions = terms.map((t) => ({ content: (0, import_typeorm2.ILike)(`%${t}%`) }));
1361
+ const chunks = await chunkRepo().find({
1362
+ where: conditions,
1363
+ take: KB_CHUNK_LIMIT,
1364
+ order: { id: "ASC" }
1365
+ });
1366
+ const seen = /* @__PURE__ */ new Set();
1367
+ let totalLen = 0;
1368
+ for (const c of chunks) {
1369
+ const text = c.content.trim();
1370
+ if (seen.has(text) || totalLen + text.length > KB_CONTEXT_MAX_CHARS) continue;
1371
+ seen.add(text);
1372
+ contextParts.push(text);
1373
+ totalLen += text.length;
1374
+ }
1375
+ }
1376
+ }
1377
+ 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 }));
1378
+ 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.
1379
+
1380
+ Context:
1381
+ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If you do not have specific information, say so.";
1382
+ const messages = [
1383
+ { role: "system", content: systemContent },
1384
+ ...history,
1385
+ { role: "user", content: message }
1386
+ ];
1387
+ const { content } = await llm.chat(messages);
1388
+ await msgRepoInst.save(msgRepoInst.create({ conversationId, role: "assistant", content }));
1389
+ return json({ content });
1390
+ } catch (err) {
1391
+ const msg = err instanceof Error ? err.message : "";
1392
+ return json({ error: msg || "Failed to send message" }, { status: 500 });
1393
+ }
1394
+ }
1395
+ };
1396
+ }
1274
1397
 
1275
1398
  // src/api/cms-api-handler.ts
1276
1399
  var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set(["users", "password_reset_tokens", "user_groups", "permissions", "comments", "form_fields", "configs"]);
@@ -1293,7 +1416,8 @@ function createCmsApiHandler(config) {
1293
1416
  usersApi,
1294
1417
  userAvatar,
1295
1418
  userProfile,
1296
- settings: settingsConfig
1419
+ settings: settingsConfig,
1420
+ chat: chatConfig
1297
1421
  } = config;
1298
1422
  const analytics = analyticsConfig ?? (getCms ? {
1299
1423
  json: config.json,
@@ -1338,6 +1462,7 @@ function createCmsApiHandler(config) {
1338
1462
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
1339
1463
  const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
1340
1464
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
1465
+ const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
1341
1466
  function resolveResource(segment) {
1342
1467
  const model = pathToModel(segment);
1343
1468
  return crudResources.includes(model) ? model : segment;
@@ -1397,6 +1522,11 @@ function createCmsApiHandler(config) {
1397
1522
  if (method === "GET") return settingsHandlers.GET(req, path[1]);
1398
1523
  if (method === "PUT") return settingsHandlers.PUT(req, path[1]);
1399
1524
  }
1525
+ if (path[0] === "chat" && chatHandlers) {
1526
+ if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
1527
+ if (path.length === 4 && path[1] === "conversations" && path[3] === "messages" && method === "GET") return chatHandlers.getMessages(req, path[2]);
1528
+ if (path.length === 2 && path[1] === "messages" && method === "POST") return chatHandlers.postMessage(req);
1529
+ }
1400
1530
  if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
1401
1531
  const resource = resolveResource(path[0]);
1402
1532
  if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });