{
 "id": "RhR5Rag000000001",
 "name": "R5 N10 ตอบจากเอกสารบริษัท",
 "active": false,
 "nodes": [
  {
   "id": "r5n10",
   "name": "Webhook",
   "type": "n8n-nodes-base.webhook",
   "typeVersion": 2,
   "position": [
    0,
    300
   ],
   "parameters": {
    "httpMethod": "POST",
    "path": "r5-ask",
    "responseMode": "responseNode",
    "options": {}
   },
   "webhookId": "33610c6c-df9d-4663-a689-01d1fb5a70a2"
  },
  {
   "id": "r5n11",
   "name": "Answer from docs",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    220,
    300
   ],
   "parameters": {
    "jsCode": "// N10 / L9: answer only from the shop's documents. Retrieval = character bigram overlap (works for Thai\n// without a word segmenter); the LLM step is injected and must quote from the retrieved chunks only.\nfunction chunk(docs, size = 220) {\n  const out = [];\n  for (const d of docs) {\n    const paras = String(d.text).split(/\\n\\s*\\n/).map((p) => p.trim()).filter(Boolean);\n    let buf = \"\";\n    for (const p of paras) {\n      if ((buf + \"\\n\" + p).length > size && buf) { out.push({ doc: d.title, text: buf }); buf = p; }\n      else buf = buf ? buf + \"\\n\" + p : p;\n    }\n    if (buf) out.push({ doc: d.title, text: buf });\n  }\n  return out;\n}\nconst grams = (s) => { const t = String(s).toLowerCase().replace(/[\\s\\p{P}]+/gu, \"\"); const g = new Set(); for (let i = 0; i < t.length - 1; i++) g.add(t.slice(i, i + 2)); return g; };\nfunction retrieve(chunks, q, k = 2) {\n  const qg = grams(q);\n  if (!qg.size) return [];\n  return chunks.map((c) => { const cg = grams(c.text); let hit = 0; for (const g of qg) if (cg.has(g)) hit++; const tg = grams(c.doc); let th = 0; for (const g of tg) if (qg.has(g)) th++; return { ...c, score: hit / qg.size + (tg.size ? 0.2 * th / tg.size : 0) }; })\n    .sort((a, b) => b.score - a.score).slice(0, k);\n}\nconst HANDOFF = /(คุยกับแอดมิน|ติดต่อพนักงาน|ขอคุยกับคน|ร้องเรียน)/;\n// llm({question, context}) -> string. Returns {answer, sources, handoff}\nfunction answer(chunks, q, llm, opts = {}) {\n  const min = opts.minScore ?? 0.35;\n  if (HANDOFF.test(q)) return { answer: \"ส่งต่อให้แอดมินแล้ว จะตอบกลับในเวลาทำการ\", sources: [], handoff: true };\n  const top = retrieve(chunks, q, 2).filter((c) => c.score >= min);\n  if (!top.length) return { answer: \"ไม่พบคำตอบในเอกสารของร้าน ส่งต่อให้แอดมินแล้ว\", sources: [], handoff: true };\n  const ctx = top.map((c, i) => \"[\" + (i + 1) + \"] \" + c.text).join(\"\\n\");\n  let a = llm ? llm({ question: q, context: ctx }) : top[0].text;\n  // guard: every number in the answer must appear in the context (no invented prices/dates)\n  // compared as whole numbers, not substrings: \"15\" is not in a context that only says \"150\" or \"1500\"\n  const numsOf = (s) => (String(s).match(/\\d[\\d,.]*/g) || []).map((n) => n.replace(/[.,]+$/, \"\"));\n  const known = new Set(numsOf(ctx.replace(/^\\[\\d+\\] /gm, \"\")));\n  const nums = numsOf(String(a).replace(/\\[\\d+\\]/g, \"\")); // [1] [2] are source markers, not facts\n  if (nums.some((n) => !known.has(n))) a = top[0].text;\n  return { answer: a, sources: [...new Set(top.map((c) => c.doc))], handoff: false };\n}\n\n\nconst CH = chunk([{\"title\":\"นโยบายคืนสินค้า\",\"text\":\"คืนสินค้าได้ภายใน 7 วันหลังได้รับ สินค้าต้องอยู่ในสภาพเดิม\\n\\nค่าส่งคืนลูกค้าเป็นผู้จ่าย ยกเว้นสินค้าชำรุดจากร้าน\"},{\"title\":\"ค่าส่ง\",\"text\":\"ค่าส่งทั่วประเทศ 50 บาท ซื้อครบ 1,000 บาทส่งฟรี\\n\\nส่งของทุกวันจันทร์ถึงศุกร์ ตัดรอบบ่ายสองโมง\"},{\"title\":\"เวลาเปิด\",\"text\":\"ร้านเปิดทุกวัน 10:00 ถึง 20:00 หยุดวันสงกรานต์\"}], 160);\nconst r = answer(CH, String($input.first().json.body.question || \"\"), null);\nreturn [{ json: { code: 200, response: r } }];"
   }
  },
  {
   "id": "r5n12",
   "name": "Respond",
   "type": "n8n-nodes-base.respondToWebhook",
   "typeVersion": 1.1,
   "position": [
    440,
    300
   ],
   "parameters": {
    "respondWith": "json",
    "responseBody": "={{ JSON.stringify($json.response) }}",
    "options": {
     "responseCode": "={{ $json.code }}"
    }
   }
  }
 ],
 "connections": {
  "Webhook": {
   "main": [
    [
     {
      "node": "Answer from docs",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Answer from docs": {
   "main": [
    [
     {
      "node": "Respond",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "settings": {
  "executionOrder": "v1",
  "saveDataErrorExecution": "all"
 },
 "pinData": {}
}