strapi-plugin-faqchatbot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,903 @@
1
+ "use strict";
2
+ const OpenAI = require("openai");
3
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
4
+ const OpenAI__default = /* @__PURE__ */ _interopDefault(OpenAI);
5
+ const bootstrap = ({ strapi }) => {
6
+ const UID = "plugin::faqchatbot.faqqa";
7
+ const updateEmbedding = async (params, existingEntry) => {
8
+ const { data } = params;
9
+ const question = data.question ?? existingEntry?.question;
10
+ const answer = data.answer ?? existingEntry?.answer;
11
+ if (!question || !answer) return;
12
+ const textToEmbed = `Q: ${question}
13
+ A: ${answer}`;
14
+ const embedding = await strapi.plugin("faqchatbot").service("embed").generateEmbedding(textToEmbed);
15
+ if (embedding) {
16
+ data.embedding = embedding;
17
+ }
18
+ };
19
+ strapi.db.lifecycles.subscribe({
20
+ models: [UID],
21
+ async beforeCreate(event) {
22
+ await updateEmbedding(event.params);
23
+ },
24
+ async beforeUpdate(event) {
25
+ const { where } = event.params;
26
+ const existingEntry = await strapi.db.query(UID).findOne({ where });
27
+ await updateEmbedding(event.params, existingEntry);
28
+ }
29
+ });
30
+ };
31
+ const destroy = ({ strapi }) => {
32
+ };
33
+ const register = ({ strapi }) => {
34
+ };
35
+ const config$2 = {
36
+ default: {},
37
+ validator() {
38
+ }
39
+ };
40
+ const kind = "collectionType";
41
+ const collectionName = "chatbot_config_faqqas";
42
+ const info = {
43
+ singularName: "faqqa",
44
+ pluralName: "faqqas",
45
+ displayName: "Chatbot-FAQ"
46
+ };
47
+ const options = {
48
+ draftAndPublish: true
49
+ };
50
+ const attributes = {
51
+ question: {
52
+ type: "string",
53
+ required: true
54
+ },
55
+ answer: {
56
+ type: "richtext",
57
+ required: true
58
+ },
59
+ embedding: {
60
+ type: "json"
61
+ }
62
+ };
63
+ const schema = {
64
+ kind,
65
+ collectionName,
66
+ info,
67
+ options,
68
+ attributes
69
+ };
70
+ const faqqa = { schema };
71
+ const contentTypes = {
72
+ faqqa
73
+ };
74
+ const controller = ({ strapi }) => ({
75
+ index(ctx) {
76
+ ctx.body = strapi.plugin("faqchatbot").service("service").getWelcomeMessage();
77
+ }
78
+ });
79
+ const config$1 = ({ strapi }) => ({
80
+ async index(ctx) {
81
+ const settings = await strapi.plugin("faqchatbot").service("config").getConfig();
82
+ const contentTypes2 = Object.values(strapi.contentTypes).filter((ct) => ct.uid.startsWith("api::")).map((ct) => ({
83
+ uid: ct.uid,
84
+ displayName: ct.info.displayName,
85
+ attributes: Object.keys(ct.attributes).map((attr) => ({
86
+ name: attr
87
+ }))
88
+ }));
89
+ ctx.body = {
90
+ settings,
91
+ contentTypes: contentTypes2
92
+ };
93
+ },
94
+ async update(ctx) {
95
+ const settings = ctx.request.body;
96
+ const data = await strapi.plugin("faqchatbot").service("config").setConfig(settings);
97
+ ctx.body = data;
98
+ }
99
+ });
100
+ const openai$1 = new OpenAI__default.default({
101
+ apiKey: process.env.OPENAI_API_KEY
102
+ });
103
+ async function getContactLink(strapi) {
104
+ const pluginStore = strapi.store({
105
+ environment: null,
106
+ type: "plugin",
107
+ name: "faqchatbot"
108
+ });
109
+ const settings = await pluginStore.get({ key: "settings" });
110
+ return settings?.contactLink || null;
111
+ }
112
+ async function getInstructions(strapi) {
113
+ const pluginStore = strapi.store({
114
+ environment: null,
115
+ type: "plugin",
116
+ name: "faqchatbot"
117
+ });
118
+ const settings = await pluginStore.get({ key: "settings" });
119
+ return {
120
+ system: settings?.systemInstructions || "",
121
+ response: settings?.responseInstructions || ""
122
+ };
123
+ }
124
+ async function getActiveCollections(strapi) {
125
+ try {
126
+ const pluginStore = strapi.store({
127
+ environment: null,
128
+ type: "plugin",
129
+ name: "faqchatbot"
130
+ });
131
+ const settings = await pluginStore.get({ key: "collections" });
132
+ if (!settings) return [];
133
+ const activeList = [];
134
+ for (const item of settings) {
135
+ const ignored = ["faqitem", "item"];
136
+ const name = item.name.toLowerCase();
137
+ const hasEnabledFields = item.fields?.some((f) => f.enabled);
138
+ if (!hasEnabledFields || ignored.includes(name)) {
139
+ continue;
140
+ }
141
+ const uid = `api::${item.name}.${item.name}`;
142
+ const contentType = strapi.contentTypes[uid];
143
+ if (!contentType) {
144
+ console.warn(` [WARNING] Content type not found for UID: ${uid}`);
145
+ continue;
146
+ }
147
+ const fields = Object.keys(contentType.attributes).filter((key) => {
148
+ const attr = contentType.attributes[key];
149
+ return [
150
+ "string",
151
+ "text",
152
+ "email",
153
+ "uid",
154
+ "richtext",
155
+ "enumeration",
156
+ "integer",
157
+ "biginteger",
158
+ "decimal",
159
+ "float",
160
+ "date",
161
+ "datetime",
162
+ "time",
163
+ "relation"
164
+ ].includes(attr.type);
165
+ });
166
+ activeList.push({ name: item.name, fields });
167
+ }
168
+ return activeList;
169
+ } catch (err) {
170
+ console.error(" [ERROR] Error loading active collections:", err);
171
+ return [];
172
+ }
173
+ }
174
+ async function rephraseQuestion(history, question) {
175
+ if (!history || !Array.isArray(history) || history.length === 0) {
176
+ return question;
177
+ }
178
+ try {
179
+ const response = await openai$1.chat.completions.create({
180
+ model: "gpt-4o-mini",
181
+ temperature: 0,
182
+ messages: [
183
+ {
184
+ role: "system",
185
+ content: `You are a Search Query Optimizer.
186
+ Your task is to determine if the user's new message is a **Follow-up** or a **New Topic** and if a follow-up just rewrite the question .
187
+ Do NOT return any explanations, only the optimized search string.
188
+
189
+ ### RULES
190
+ 1. **Dependency Check (The "Pronoun" Rule):**
191
+ - ONLY combine with history if the new question contains **Pronouns** ("it", "that", "they") or is **Grammatically Incomplete** ("How much?", "Where do I buy?", "Is it refundable?").
192
+
193
+ 2. **Independence Check (The "Specifics" Rule):**
194
+ - If the user asks a complete question containing a **New Specific Noun** or **Scenario** (e.g., "Group of 7 people", "Booking for pets"), treat it as a **Standalone Query**.
195
+ - **Do NOT** attach the previous topic to it.
196
+ - *Example:* History="Commuter Pass", Input="Can I book for a group of 7?" -> Output="Group booking for 7 people" (Correct).
197
+ - *Bad Output:* "Group booking for Commuter Pass" (Incorrect).
198
+
199
+ 3. **Output:**
200
+ - Return ONLY the optimized search string.`
201
+ },
202
+ ...history.slice(-4),
203
+ { role: "user", content: question }
204
+ ]
205
+ });
206
+ const rewritten = response.choices[0].message.content?.trim();
207
+ if (!rewritten) return question;
208
+ const lower = rewritten.toLowerCase();
209
+ if (lower.includes("unavailable") || lower.includes("sorry") || lower.includes("i am") || lower.includes("cannot") || rewritten.length > 120) {
210
+ return question;
211
+ }
212
+ return rewritten;
213
+ } catch (err) {
214
+ console.error("Error in rephraseQuestion:", err);
215
+ return question;
216
+ }
217
+ }
218
+ function sanitizeFilters(filters) {
219
+ if (!filters || typeof filters !== "object") return filters;
220
+ if (Array.isArray(filters)) {
221
+ return filters.map(sanitizeFilters);
222
+ }
223
+ const operators = [
224
+ "eq",
225
+ "ne",
226
+ "lt",
227
+ "gt",
228
+ "lte",
229
+ "gte",
230
+ "in",
231
+ "notIn",
232
+ "contains",
233
+ "notContains",
234
+ "containsi",
235
+ "notContainsi",
236
+ "null",
237
+ "notNull",
238
+ "between",
239
+ "startsWith",
240
+ "endsWith",
241
+ "or",
242
+ "and",
243
+ "not"
244
+ ];
245
+ const newFilters = {};
246
+ for (const key in filters) {
247
+ let newKey = key;
248
+ if (operators.includes(key) && !key.startsWith("$")) {
249
+ newKey = `$${key}`;
250
+ }
251
+ newFilters[newKey] = sanitizeFilters(filters[key]);
252
+ }
253
+ return newFilters;
254
+ }
255
+ function updateJsonContext(prevContext, question) {
256
+ const MAX_HISTORY = 10;
257
+ const ctx = { ...prevContext || {} };
258
+ ctx.history = Array.isArray(ctx.history) ? ctx.history : [];
259
+ ctx.history.push(question);
260
+ if (ctx.history.length > MAX_HISTORY) ctx.history.shift();
261
+ const words = question.toLowerCase().replace(/[^\w\s]/g, "").split(" ").filter((w) => w.length > 3);
262
+ ctx.keywords = [.../* @__PURE__ */ new Set([...ctx.keywords || [], ...words])];
263
+ ctx.lastQuestion = question;
264
+ return ctx;
265
+ }
266
+ async function searchRealtime(strapi, plan, activeCollections) {
267
+ if (!plan || !plan.collection) {
268
+ return null;
269
+ }
270
+ const sanitizedFilters = sanitizeFilters(plan.filters || {});
271
+ const config2 = activeCollections.find((c) => c.name === plan.collection);
272
+ if (!config2) {
273
+ return null;
274
+ }
275
+ const uid = `api::${plan.collection}.${plan.collection}`;
276
+ try {
277
+ if (plan.operation === "count") {
278
+ const count = await strapi.entityService.count(uid, {
279
+ filters: sanitizedFilters
280
+ });
281
+ return {
282
+ type: "count",
283
+ collection: plan.collection,
284
+ value: count
285
+ };
286
+ }
287
+ const result = await strapi.entityService.findMany(uid, {
288
+ filters: sanitizedFilters,
289
+ sort: plan.sort,
290
+ limit: 10
291
+ });
292
+ const cleaned = result.map((row) => {
293
+ const clean = {};
294
+ for (const f of config2.fields) clean[f] = row[f];
295
+ return clean;
296
+ });
297
+ return {
298
+ type: "list",
299
+ collection: plan.collection,
300
+ schema: config2.fields,
301
+ items: cleaned
302
+ };
303
+ } catch (err) {
304
+ console.error("Realtime search error:", err);
305
+ return null;
306
+ }
307
+ }
308
+ function cosineSimilarity(a, b) {
309
+ if (!a || !b || a.length !== b.length) return 0;
310
+ let dot = 0;
311
+ let normA = 0;
312
+ let normB = 0;
313
+ for (let i = 0; i < a.length; i++) {
314
+ dot += a[i] * b[i];
315
+ normA += a[i] * a[i];
316
+ normB += b[i] * b[i];
317
+ }
318
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
319
+ }
320
+ async function searchFAQ(question, strapi) {
321
+ const embedding = await openai$1.embeddings.create({
322
+ model: "text-embedding-3-small",
323
+ input: question
324
+ });
325
+ let queryVector = embedding.data[0].embedding;
326
+ if (!queryVector || !queryVector.length) {
327
+ return [];
328
+ }
329
+ const faqs = await strapi.db.connection("chatbot_config_faqqas").select("answer", "embedding").whereNotNull("embedding").whereNotNull("published_at");
330
+ if (!faqs.length) return [];
331
+ const scored = faqs.map((f) => {
332
+ let dbVector = f.embedding;
333
+ try {
334
+ if (typeof dbVector === "string") {
335
+ dbVector = JSON.parse(dbVector);
336
+ }
337
+ dbVector = Array.isArray(dbVector) ? dbVector.map((n) => Number(n)) : [];
338
+ if (!Array.isArray(dbVector) || dbVector.length !== queryVector.length) {
339
+ return { answer: f.answer, similarity: 0 };
340
+ }
341
+ return {
342
+ answer: f.answer,
343
+ similarity: cosineSimilarity(queryVector, dbVector)
344
+ };
345
+ } catch (err) {
346
+ return { answer: f.answer, similarity: 0 };
347
+ }
348
+ });
349
+ scored.sort((a, b) => b.similarity - a.similarity);
350
+ if (!scored.length || scored[0].similarity < 0.4) {
351
+ return [];
352
+ }
353
+ return scored.slice(0, 3).map((s) => s.answer);
354
+ }
355
+ async function simplePlanner(question, activeCollections, instructions) {
356
+ const response = await openai$1.chat.completions.create({
357
+ model: "gpt-4o-mini",
358
+ temperature: 0,
359
+ messages: [
360
+ {
361
+ role: "system",
362
+ content: `
363
+ ${instructions.system || ""}
364
+ You are a STRICT database query planner that converts user questions into Strapi query JSON.
365
+
366
+ --------------------------------
367
+ CORE TASK
368
+ --------------------------------
369
+ Return ONLY valid JSON. No text. No explanation.
370
+
371
+ --------------------------------
372
+ COLLECTION SELECTION
373
+ --------------------------------
374
+ - Choose the most relevant collection from the available list.
375
+ - Never invent collection names.
376
+
377
+ --------------------------------
378
+ FIELD RULES
379
+ --------------------------------
380
+ - Only use fields that exist in the selected collection schema.
381
+ - Never hallucinate fields.
382
+
383
+
384
+ --------------------------------
385
+ LOCATION NORMALIZATION (CRITICAL)
386
+ --------------------------------
387
+ The database stores locations in the format:
388
+ City Name (AIRPORT_CODE)
389
+
390
+ Before generating filters, you MUST normalize
391
+ all user-provided places into the nearest
392
+ major city or airport name.
393
+
394
+ RULES:
395
+
396
+ 1. SMALL TOWNS / VILLAGES
397
+ - Convert to nearest major airport city.
398
+ Example:
399
+ "Kalveerampalayam" → "Coimbatore"
400
+ "Kollam" → "Trivandrum"
401
+ "Alappuzha" → "Kochi"
402
+
403
+ 2. OLD OR LOCAL NAMES
404
+ - Convert to modern official city name.
405
+ Example:
406
+ "Madras" → "Chennai"
407
+ "Cochin" → "Kochi"
408
+ "Bombay" → "Mumbai"
409
+
410
+ 3. SUBURBS / DISTRICTS
411
+ - Convert to main metro city.
412
+ Example:
413
+ "Brooklyn" → "New York"
414
+ "Noida" → "Delhi"
415
+
416
+ 4. AIRPORT CODES
417
+ - If user provides code (COK, MAA, JFK),
418
+ search using containsi for that code.
419
+
420
+ Example:
421
+ User: "flight from COK"
422
+ Filter:
423
+ { "origin": { "containsi": "COK" } }
424
+
425
+ 5. ALWAYS MATCH DATABASE STRINGS
426
+ - Use containsi
427
+ - Never use raw spelling if DB format differs
428
+ - Prefer airport code if available
429
+
430
+ --------------------------------
431
+ TEXT FILTER RULES (VERY IMPORTANT)
432
+ --------------------------------
433
+ - For city names, titles, destinations, names → ALWAYS use "containsi"
434
+ - NEVER use "eq" for text
435
+ - NEVER use "in" for text arrays
436
+ - For multiple text values use "$or" with containsi
437
+
438
+ Example:
439
+ User: "flight to paris or amsterdam"
440
+ Filters:
441
+ {
442
+ "$or": [
443
+ { "destination": { "containsi": "paris" } },
444
+ { "destination": { "containsi": "amsterdam" } }
445
+ ]
446
+ }
447
+
448
+ --------------------------------
449
+ NUMBER FILTER RULES
450
+ --------------------------------
451
+ - For price, fare, amount → use lt, lte, gt, gte, between
452
+ - "under" → lte
453
+ - "above" → gte
454
+ - "between" → between
455
+
456
+ --------------------------------
457
+ OPERATION RULES
458
+ --------------------------------
459
+ - "how many", "count" → operation = "count"
460
+ - otherwise → operation = "list"
461
+
462
+ --------------------------------
463
+ SORT RULES
464
+ --------------------------------
465
+ - "cheapest", "lowest" → sort ["fare:asc"]
466
+ - "highest", "expensive" → sort ["fare:desc"]
467
+ - Only add sort if user implies ranking
468
+
469
+ --------------------------------
470
+ INTENT CLASSIFICATION (CRITICAL)
471
+ --------------------------------
472
+ First decide intent:
473
+
474
+ INTENT = "realtime"
475
+ - User asks about availability, price, list, count, search, show items
476
+ - Mentions data stored in collections
477
+
478
+ INTENT = "faq"
479
+ - User asks "who is", "what is", "explain", "details about"
480
+ - General knowledge
481
+ - No clear database entity
482
+
483
+ If no clear database match → ALWAYS choose "faq"
484
+ NEVER force a collection.
485
+
486
+ OUTPUT FORMAT
487
+
488
+ Return ONLY JSON.
489
+
490
+ If no database match exists, return:
491
+
492
+ {
493
+ "collection": null
494
+ }
495
+
496
+ Otherwise return:
497
+
498
+ {
499
+ "collection": "name",
500
+ "operation": "list" | "count",
501
+ "filters": {},
502
+ "sort": []
503
+ }
504
+
505
+
506
+ --------------------------------
507
+ AVAILABLE COLLECTIONS
508
+ --------------------------------
509
+ ${JSON.stringify(activeCollections, null, 2)}
510
+ `
511
+ },
512
+ {
513
+ role: "user",
514
+ content: question
515
+ }
516
+ ]
517
+ });
518
+ try {
519
+ const raw = response.choices[0].message.content || "{}";
520
+ const cleaned = raw.replace(/```json/g, "").replace(/```/g, "").trim();
521
+ const plan = JSON.parse(cleaned);
522
+ return plan;
523
+ } catch (err) {
524
+ return null;
525
+ }
526
+ }
527
+ async function realtimeInterpreterAI(question, realtimeData) {
528
+ if (!realtimeData) return null;
529
+ const response = await openai$1.chat.completions.create({
530
+ model: "gpt-4o-mini",
531
+ temperature: 0.2,
532
+ messages: [
533
+ {
534
+ role: "system",
535
+ content: `
536
+ You are a realtime data interpreter.
537
+
538
+ Convert database JSON into a SHORT natural language summary.
539
+
540
+ Rules:
541
+ - Do NOT output JSON
542
+ - Do NOT hallucinate
543
+ - If count → say number
544
+ - If list → summarize important fields only
545
+ - Max 3–4 lines
546
+ `
547
+ },
548
+ {
549
+ role: "user",
550
+ content: `
551
+ QUESTION: ${question}
552
+
553
+ REALTIME DATA:
554
+ ${JSON.stringify(realtimeData)}
555
+ `
556
+ }
557
+ ]
558
+ });
559
+ const text = response.choices[0].message.content;
560
+ return text;
561
+ }
562
+ async function finalAggregator(ctx, question, faq, realtimeMeta, realtimeText, contactLink, instructions) {
563
+ ctx.set("Content-Type", "text/event-stream");
564
+ ctx.set("Cache-Control", "no-cache");
565
+ ctx.set("Connection", "keep-alive");
566
+ ctx.status = 200;
567
+ ctx.res.flushHeaders?.();
568
+ const stream = await openai$1.chat.completions.create({
569
+ model: "gpt-4o-mini",
570
+ temperature: 0.3,
571
+ stream: true,
572
+ messages: [
573
+ {
574
+ role: "system",
575
+ content: `
576
+
577
+ ${instructions.response || ""}
578
+ You are an intelligent AI Assistant for a website chatbot.
579
+
580
+ INPUTS:
581
+ - FAQ semantic answers
582
+ - REALTIME_META (structured database info)
583
+ - REALTIME_TEXT (human summary)
584
+ - User question
585
+
586
+ --------------------------------
587
+ RESPONSE LENGTH RULE
588
+ --------------------------------
589
+ Default → SHORT & PRECISE (2–3 lines max)
590
+
591
+ If the user's question contains:
592
+ "explain", "details", "more", "elaborate", "why", "how"
593
+ → Provide LONGER detailed answer.
594
+
595
+ If FAQ answer is long:
596
+ → Summarize unless user asked for detail.
597
+
598
+ --------------------------------
599
+ CORE RULE
600
+ --------------------------------
601
+ REALTIME_META decides logic.
602
+ REALTIME_TEXT decides wording.
603
+
604
+ --------------------------------
605
+ CONTACT INTENT RULE
606
+ --------------------------------
607
+ If user asks about contacting support, customer service, help, or similar:
608
+
609
+ AND contactLink is provided:
610
+ Return ONLY this link in a short sentence.
611
+
612
+ Example:
613
+ "You can contact us here: https://example.com/contact"
614
+
615
+ --------------------------------
616
+ ANSWER LOGIC
617
+ --------------------------------
618
+
619
+ CASE 1 — REALTIME_META.type = "count"
620
+ Return ONE sentence with the number.
621
+
622
+ CASE 2 — REALTIME_META.type = "list"
623
+ Use REALTIME_TEXT as main answer.
624
+
625
+ CASE 3 — REALTIME_META = null
626
+ Use FAQ.
627
+
628
+ CASE 4 — BOTH EXIST
629
+ Use REALTIME_TEXT as main + FAQ as support.
630
+
631
+ CASE 5 — NOTHING
632
+ Say information unavailable.
633
+
634
+ Never show JSON.
635
+ Never hallucinate.
636
+ Max 5 lines.
637
+ `
638
+ },
639
+ {
640
+ role: "user",
641
+ content: `
642
+ QUESTION: ${question}
643
+
644
+ CONTACT_LINK:
645
+ ${contactLink || "NOT_AVAILABLE"}
646
+
647
+ FAQ:
648
+ ${JSON.stringify(faq)}
649
+
650
+ REALTIME_META:
651
+ ${JSON.stringify(realtimeMeta)}
652
+
653
+ REALTIME_TEXT:
654
+ ${realtimeText}
655
+ `
656
+ }
657
+ ]
658
+ });
659
+ for await (const chunk of stream) {
660
+ const token = chunk.choices?.[0]?.delta?.content;
661
+ if (token) {
662
+ ctx.res.write(`data: ${token}
663
+
664
+ `);
665
+ }
666
+ }
667
+ if (realtimeMeta && realtimeMeta.type === "list") {
668
+ const cardsPayload = {
669
+ title: realtimeMeta.collection,
670
+ schema: realtimeMeta.schema,
671
+ items: realtimeMeta.items
672
+ };
673
+ ctx.res.write(`event: cards
674
+ `);
675
+ ctx.res.write(`data: ${JSON.stringify(cardsPayload)}
676
+
677
+ `);
678
+ }
679
+ ctx.res.write("data: [DONE]\n\n");
680
+ ctx.res.end();
681
+ }
682
+ async function validateOpenAIKey(key) {
683
+ try {
684
+ const temp = new OpenAI__default.default({ apiKey: key });
685
+ await temp.models.list();
686
+ return true;
687
+ } catch {
688
+ return false;
689
+ }
690
+ }
691
+ const ask = ({ strapi }) => ({
692
+ async validateKey(ctx) {
693
+ const { key } = ctx.request.body;
694
+ const isValid = await validateOpenAIKey(key);
695
+ ctx.body = { valid: isValid };
696
+ },
697
+ async ask(ctx) {
698
+ const { question, history = [] } = ctx.request.body;
699
+ const instructions = await getInstructions(strapi);
700
+ let jsonContext = ctx.request.body.context || {};
701
+ jsonContext = updateJsonContext(jsonContext, question);
702
+ ctx.set("X-User-Context", JSON.stringify(jsonContext));
703
+ try {
704
+ const activeCollections = await getActiveCollections(strapi);
705
+ if (!activeCollections || activeCollections.length === 0) {
706
+ }
707
+ const rewritten = await rephraseQuestion(history, question);
708
+ const contactLink = await getContactLink(strapi);
709
+ const faqResults = await searchFAQ(rewritten, strapi);
710
+ const plan = await simplePlanner(rewritten, activeCollections, instructions);
711
+ let realtimeResults = null;
712
+ let realtimeAIText = null;
713
+ if (plan && plan.collection) {
714
+ realtimeResults = await searchRealtime(strapi, plan, activeCollections);
715
+ realtimeAIText = await realtimeInterpreterAI(rewritten, realtimeResults);
716
+ } else {
717
+ }
718
+ await finalAggregator(
719
+ ctx,
720
+ rewritten,
721
+ faqResults,
722
+ realtimeResults,
723
+ realtimeAIText,
724
+ contactLink,
725
+ instructions
726
+ );
727
+ return;
728
+ } catch (err) {
729
+ console.error("[ERROR]", err);
730
+ ctx.body = { type: "text", content: "Error occurred." };
731
+ }
732
+ }
733
+ });
734
+ const suggestQuestions = ({ strapi }) => ({
735
+ async getSuggested(ctx) {
736
+ const pluginStore = strapi.store({
737
+ environment: null,
738
+ type: "plugin",
739
+ name: "faqchatbot"
740
+ });
741
+ const settings = await pluginStore.get({ key: "settings" });
742
+ ctx.body = {
743
+ suggestedQuestions: settings?.suggestedQuestions || []
744
+ };
745
+ }
746
+ });
747
+ const cardMapping = ({ strapi }) => ({
748
+ async index(ctx) {
749
+ const pluginStore = strapi.store({
750
+ environment: null,
751
+ type: "plugin",
752
+ name: "faqchatbot"
753
+ });
754
+ const settings = await pluginStore.get({ key: "settings" });
755
+ ctx.body = {
756
+ cardStyles: settings?.cardStyles || {}
757
+ };
758
+ }
759
+ });
760
+ const controllers = {
761
+ controller,
762
+ config: config$1,
763
+ ask,
764
+ suggestQuestions,
765
+ cardMapping
766
+ };
767
+ const middlewares = {};
768
+ const policies = {};
769
+ const admin = {
770
+ type: "admin",
771
+ routes: [
772
+ {
773
+ method: "GET",
774
+ path: "/collections",
775
+ handler: "config.index",
776
+ config: {
777
+ auth: false
778
+ }
779
+ },
780
+ {
781
+ method: "POST",
782
+ path: "/collections",
783
+ handler: "config.update",
784
+ config: {
785
+ auth: false
786
+ }
787
+ }
788
+ ]
789
+ };
790
+ const contentApi = () => ({
791
+ type: "content-api",
792
+ routes: [
793
+ {
794
+ method: "GET",
795
+ path: "/",
796
+ handler: "controller.index",
797
+ config: {
798
+ auth: false
799
+ }
800
+ },
801
+ {
802
+ method: "POST",
803
+ path: "/ask",
804
+ handler: "ask.ask",
805
+ config: {
806
+ auth: false
807
+ }
808
+ },
809
+ {
810
+ method: "GET",
811
+ path: "/suggested-questions",
812
+ handler: "suggestQuestions.getSuggested",
813
+ config: {
814
+ auth: false
815
+ }
816
+ },
817
+ {
818
+ method: "GET",
819
+ path: "/card-mapping",
820
+ handler: "cardMapping.index",
821
+ config: { auth: false }
822
+ },
823
+ {
824
+ method: "POST",
825
+ path: "/validate-key",
826
+ handler: "ask.validateKey",
827
+ config: {
828
+ auth: false
829
+ }
830
+ }
831
+ ]
832
+ });
833
+ const routes = {
834
+ admin,
835
+ "content-api": contentApi
836
+ };
837
+ const openai = new OpenAI__default.default({
838
+ apiKey: process.env.OPENAI_API_KEY
839
+ });
840
+ const embed = ({ strapi }) => ({
841
+ async generateEmbedding(text) {
842
+ try {
843
+ if (!process.env.OPENAI_API_KEY) {
844
+ return null;
845
+ }
846
+ const response = await openai.embeddings.create({
847
+ model: "text-embedding-3-small",
848
+ input: text
849
+ });
850
+ return response.data[0].embedding;
851
+ } catch (error) {
852
+ strapi.log.error("Error generating embedding via OpenAI:");
853
+ console.error(error);
854
+ return null;
855
+ }
856
+ }
857
+ });
858
+ const config = ({ strapi }) => ({
859
+ async getConfig() {
860
+ const pluginStore = strapi.store({
861
+ environment: null,
862
+ type: "plugin",
863
+ name: "faqchatbot"
864
+ });
865
+ const settings = await pluginStore.get({ key: "settings" });
866
+ return settings && typeof settings === "object" ? settings : {};
867
+ },
868
+ async setConfig(newSettings) {
869
+ const pluginStore = strapi.store({
870
+ environment: null,
871
+ type: "plugin",
872
+ name: "faqchatbot"
873
+ });
874
+ const existingRaw = await pluginStore.get({ key: "settings" });
875
+ const existingSettings = existingRaw && typeof existingRaw === "object" ? existingRaw : {};
876
+ const mergedSettings = {
877
+ ...existingSettings,
878
+ ...newSettings
879
+ };
880
+ await pluginStore.set({
881
+ key: "settings",
882
+ value: mergedSettings
883
+ });
884
+ return mergedSettings;
885
+ }
886
+ });
887
+ const services = {
888
+ config,
889
+ embed
890
+ };
891
+ const index = {
892
+ register,
893
+ bootstrap,
894
+ destroy,
895
+ config: config$2,
896
+ controllers,
897
+ routes,
898
+ services,
899
+ contentTypes,
900
+ policies,
901
+ middlewares
902
+ };
903
+ module.exports = index;