{
  "id": "mEWNdUEQoGDdVJxX",
  "name": "RAG Telegram Bot with Google Drive & Local LLM (Ollama)",
  "tags": [],
  "nodes": [
    {
      "id": "eb336315-3849-4708-b53d-403bbb396080",
      "name": "\u2699\ufe0f Setup Instructions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1520,
        1088
      ],
      "parameters": {
        "color": "#8A9532",
        "width": 640,
        "height": 468,
        "content": "## Setup Checklist\n\nStep 1 \u2014 Credentials\nAdd your credentials in n8n Settings \u2192 Credentials:\n\u2705 Telegram API \u2014 your bot token from @BotFather\n\u2705 PostgreSQL \u2014 your Supabase (or any Postgres) connection\n\u2705 Google Drive OAuth2 \u2014 connect your Google account\n\nStep 2 \u2014 Google Drive Trigger\nOpen the `Google Drive Trigger` node and select the folder you want to monitor for new documents.\n\nStep 3 \u2014 PIN Code\nIn the `Register New User` node, change `'1234'` in the SQL to your desired access PIN. All new users will need this PIN to access the bot.\n\nStep 4 \u2014 Ollama\nEnsure Ollama is running and accessible. If using Docker, it must be at `http://host.docker.internal:11434`.\nPull required models:\nollama pull nomic-embed-text\nollama pull qwen2.5:7b\n\nStep 5 \u2014 Activate\nActivate the workflow. Send `/start` to your Telegram bot!"
      },
      "typeVersion": 1
    },
    {
      "id": "c25280fe-8551-457b-bc87-59519454bef9",
      "name": "\ud83d\udccc Change Default PIN",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        128,
        -128
      ],
      "parameters": {
        "color": "#6E6E6E",
        "width": 280,
        "height": 100,
        "content": "\u26a0\ufe0f **Change the PIN** from `1234` to your own secret code in the SQL query below."
      },
      "typeVersion": 1
    },
    {
      "id": "79a090e8-bb02-4c60-a028-14ddc1a4b99a",
      "name": "\ud83d\udccc Set Your Folder",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -576,
        1216
      ],
      "parameters": {
        "color": 2,
        "width": 280,
        "height": 80,
        "content": "\u26a0\ufe0f **Select your Google Drive folder** to watch in this trigger node."
      },
      "typeVersion": 1
    },
    {
      "id": "14aa1ca3-4cf3-473b-8b90-d063e7a2d2b3",
      "name": "Telegram Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -640,
        160
      ],
      "parameters": {
        "path": "rag-telegram-bot",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "150b2bf2-3ead-4fbd-928d-761027d6e774",
      "name": "Get User",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -416,
        160
      ],
      "parameters": {
        "query": "SELECT chat_id, username, pin_code, is_verified, pin_attempts FROM telegram_users WHERE chat_id = '={{ $('Telegram Webhook').first().json.body.message.chat.id }}'",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.5,
      "alwaysOutputData": true
    },
    {
      "id": "6263d211-5595-4bce-9ca0-24bbc5b24b2f",
      "name": "User State Router",
      "type": "n8n-nodes-base.code",
      "position": [
        -192,
        160
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst tgData = $('Telegram Webhook').first().json.body;\nif (!tgData || !tgData.message) {\n  return [{json: {state: 'ignore', chatId: '', text: '', username: ''}}];\n}\nconst chatId = String(tgData.message.chat.id);\nconst text = (tgData.message.text || '').trim();\nconst username = (tgData.message.from && tgData.message.from.username) ? tgData.message.from.username : '';\nif (items.length === 0 || !items[0].json.chat_id) {\n  const r = {state: 'new', chatId: chatId, text: text, username: username};\n  return [{json: r}];\n}\nconst user = items[0].json;\nif (user.is_verified) {\n  const r = {state: 'verified', chatId: chatId, text: text, username: username};\n  return [{json: r}];\n}\nconst r = {state: 'unverified', chatId: chatId, text: text, username: username, pin_code: String(user.pin_code || ''), pin_attempts: Number(user.pin_attempts || 0)};\nreturn [{json: r}];"
      },
      "typeVersion": 2
    },
    {
      "id": "62807390-906b-46f5-9e05-eac9b01422aa",
      "name": "Is New User?",
      "type": "n8n-nodes-base.if",
      "position": [
        32,
        160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "1",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.state }}",
              "rightValue": "new"
            },
            {
              "id": "2",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.state }}",
              "rightValue": "ignore"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "da7ffc6f-b24b-433d-b585-319d373c7f48",
      "name": "Register New User",
      "type": "n8n-nodes-base.postgres",
      "position": [
        256,
        0
      ],
      "parameters": {
        "query": "INSERT INTO telegram_users (chat_id, username, pin_code, is_verified, pin_attempts, created_at, updated_at) VALUES ('={{ $json.chatId }}', '={{ $json.username }}', 'YOUR_PIN_CODE', false, 0, NOW(), NOW()) ON CONFLICT (chat_id) DO UPDATE SET updated_at = NOW() RETURNING chat_id",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.5
    },
    {
      "id": "848711d5-f901-4b33-9c33-ad8169d3d32a",
      "name": "Send PIN Request",
      "type": "n8n-nodes-base.telegram",
      "maxTries": 3,
      "position": [
        480,
        -32
      ],
      "parameters": {
        "text": "Welcome! To access document search, please enter the PIN code:",
        "chatId": "={{ $('User State Router').first().json.chatId }}",
        "additionalFields": {}
      },
      "retryOnFail": true,
      "typeVersion": 1.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "92e2cb25-a441-4121-b464-ebb45332ef31",
      "name": "Is Verified?",
      "type": "n8n-nodes-base.if",
      "position": [
        256,
        160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-verified",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.state }}",
              "rightValue": "verified"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "c70f2339-20ad-456f-afd9-c7b5e8ff8c8f",
      "name": "Has Text?",
      "type": "n8n-nodes-base.if",
      "position": [
        480,
        160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-text",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.text }}"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "3b7a1e3a-316f-4aea-8d32-70a3a4b753f6",
      "name": "No Text Message",
      "type": "n8n-nodes-base.telegram",
      "maxTries": 3,
      "position": [
        704,
        -32
      ],
      "parameters": {
        "text": "Please send a text message to search the documents.",
        "chatId": "={{ $('User State Router').first().json.chatId }}",
        "additionalFields": {}
      },
      "retryOnFail": true,
      "typeVersion": 1.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "d95fdcb4-0904-4f1c-af33-3c6cef4dc628",
      "name": "Embed Query",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        704,
        160
      ],
      "parameters": {
        "url": "http://host.docker.internal:11434/api/embed",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({model: 'nomic-embed-text', input: $json.text}) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "51d25cb0-a0a9-44c6-9103-9fafeab9e30f",
      "name": "Format Query Embedding",
      "type": "n8n-nodes-base.code",
      "position": [
        928,
        160
      ],
      "parameters": {
        "jsCode": "const item = $input.first();\nconst embedding = item.json.embeddings[0];\nconst vectorStr = '[' + embedding.join(',') + ']';\nconst userState = $('User State Router').first().json;\nreturn [{json: {vectorStr: vectorStr, queryVec: embedding, chatId: userState.chatId, text: userState.text, username: userState.username}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "19a8f67a-7f69-4aca-9386-2cb1adab5d38",
      "name": "Search Documents",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1136,
        160
      ],
      "parameters": {
        "query": "SELECT chunk_id, chunk_text, file_name, embedding::text AS embedding_text FROM document_chunks",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.5,
      "alwaysOutputData": false
    },
    {
      "id": "af541a86-b3a3-4316-b577-93ee0bcfc100",
      "name": "Build Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        1360,
        160
      ],
      "parameters": {
        "jsCode": "const chunks = $input.all();\nconst userState = $('User State Router').first().json;\nconst queryVec = $('Format Query Embedding').first().json.queryVec;\nconst question = userState.text;\nconst chatId = userState.chatId;\n\nfunction cosineSim(a, b) {\n  let dot = 0, na = 0, nb = 0;\n  for (let i = 0; i < a.length; i++) {\n    dot += a[i] * b[i];\n    na += a[i] * a[i];\n    nb += b[i] * b[i];\n  }\n  return dot / (Math.sqrt(na) * Math.sqrt(nb));\n}\n\nconst scored = [];\nfor (const c of chunks) {\n  const embText = c.json.embedding_text || '';\n  const cleaned = embText.startsWith('=') ? embText.slice(1) : embText;\n  try {\n    const vec = JSON.parse(cleaned);\n    const sim = cosineSim(queryVec, vec);\n    scored.push({chunk_text: c.json.chunk_text || '', file_name: c.json.file_name || '', similarity: sim});\n  } catch(e) {}\n}\nscored.sort((a, b) => b.similarity - a.similarity);\nconst top5 = scored.slice(0, 5);\n\nlet context = '';\nfor (const c of top5) {\n  let t = c.chunk_text;\n  if (t.startsWith('=')) t = t.slice(1);\n  if (t.trim()) context += t + '\\n\\n';\n}\nif (!context.trim()) context = 'No relevant documents found.';\n\nconst messages = [\n  {role: 'system', content: 'You are a helpful assistant. Answer questions based on the provided context only. If the answer is not in the context, say you do not have that information.'},\n  {role: 'user', content: 'Context:\\n' + context + '\\n\\nQuestion: ' + question}\n];\nconst body = JSON.stringify({model: 'qwen2.5:7b', messages: messages, stream: false});\nreturn [{json: {messages: messages, body: body, chatId: chatId, question: question, matchCount: top5.length}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f185f20c-519f-4847-8884-d5afd1624505",
      "name": "Ask LLM",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1584,
        160
      ],
      "parameters": {
        "url": "http://host.docker.internal:11434/api/chat",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.body }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "32589a75-ef55-4a5a-adcf-3f602d5018a7",
      "name": "Send Answer",
      "type": "n8n-nodes-base.telegram",
      "maxTries": 3,
      "position": [
        1808,
        160
      ],
      "parameters": {
        "text": "={{ $json.message.content }}",
        "chatId": "={{ $('Build Prompt').first().json.chatId }}",
        "additionalFields": {}
      },
      "retryOnFail": true,
      "typeVersion": 1.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "4afd3214-3beb-4b91-add3-607de1fa65ea",
      "name": "Log Query",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2016,
        160
      ],
      "parameters": {
        "query": "INSERT INTO query_log (chat_id, username, query, answer, match_count) VALUES ('{{ $('Build Prompt').first().json.chatId }}', '{{ $('User State Router').first().json.username }}', '{{ $('User State Router').first().json.text.replace(/'/g, \"''\") }}', '{{ $('Ask LLM').first().json.message.content.replace(/'/g, \"''\") }}', {{ $('Build Prompt').first().json.matchCount }})",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.5
    },
    {
      "id": "f3cd3724-a613-4319-bc18-9db07356fd8f",
      "name": "PIN Correct?",
      "type": "n8n-nodes-base.if",
      "position": [
        480,
        368
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-pin",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.text }}",
              "rightValue": "={{ $json.pin_code }}"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "f4bf6313-20c0-459a-bbdb-c8f0c465cc60",
      "name": "Mark User Verified",
      "type": "n8n-nodes-base.postgres",
      "position": [
        704,
        368
      ],
      "parameters": {
        "query": "UPDATE telegram_users SET is_verified = true, verified_at = NOW(), updated_at = NOW() WHERE chat_id = '={{ $json.chatId }}' RETURNING chat_id",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.5
    },
    {
      "id": "056671c9-8994-41c2-bc59-7fb2882660d5",
      "name": "Send Welcome",
      "type": "n8n-nodes-base.telegram",
      "maxTries": 3,
      "position": [
        928,
        368
      ],
      "parameters": {
        "text": "You are now verified! Send me any question to search the documents.",
        "chatId": "={{ $('User State Router').first().json.chatId }}",
        "additionalFields": {}
      },
      "retryOnFail": true,
      "typeVersion": 1.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "54c70d97-b841-4f9e-9cbf-137ebe24fb93",
      "name": "Wrong PIN Message",
      "type": "n8n-nodes-base.telegram",
      "maxTries": 3,
      "position": [
        704,
        560
      ],
      "parameters": {
        "text": "Incorrect PIN. Please try again.",
        "chatId": "={{ $('User State Router').first().json.chatId }}",
        "additionalFields": {}
      },
      "retryOnFail": true,
      "typeVersion": 1.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "ce78c2f7-286b-4277-8486-4548e15eb464",
      "name": "Google Drive Trigger",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        -576,
        1040
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 1
    },
    {
      "id": "cc19368e-0b43-4894-8776-a383e2073b37",
      "name": "Download File",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        -384,
        1040
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.id }}"
        },
        "options": {},
        "operation": "download"
      },
      "typeVersion": 3
    },
    {
      "id": "99c24f92-3ca5-4746-95c1-29c774ff11f2",
      "name": "Extract Text",
      "type": "n8n-nodes-base.code",
      "position": [
        -160,
        1040
      ],
      "parameters": {
        "jsCode": "const fileData = $input.first();\nconst triggerData = $('Google Drive Trigger').first().json;\nconst fileName = triggerData.name || 'unknown';\nconst fileId = triggerData.id || '';\n\nlet text = '';\ntry {\n  const binaryKeys = Object.keys(fileData.binary || {});\n  if (binaryKeys.length > 0) {\n    const buf = await $helpers.getBinaryDataBuffer(binaryKeys[0]);\n    text = buf.toString('utf-8');\n  }\n} catch(e) {\n  text = '';\n}\n\nconst r = {text: text, fileName: fileName, fileId: fileId, driveFileId: fileId};\nreturn [{json: r}];"
      },
      "typeVersion": 2
    },
    {
      "id": "edbaac02-ec81-4dd9-9c3f-0dac7831e708",
      "name": "Split Into Chunks",
      "type": "n8n-nodes-base.code",
      "position": [
        48,
        1040
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const text = $json.text;\nconst fileName = $json.fileName;\nconst fileId = $json.fileId;\nconst driveFileId = $json.driveFileId;\nconst chunkSize = 500;\nconst overlap = 50;\n\nif (!text || text.length === 0) return [];\n\nconst chunks = [];\nlet i = 0;\nlet chunkIndex = 0;\n\nwhile (i < text.length) {\n  const end = Math.min(i + chunkSize, text.length);\n  const chunkText = text.slice(i, end).trim();\n  if (chunkText.length > 10) {\n    const c = {chunk_id: fileId + '_' + chunkIndex, file_id: fileId, file_name: fileName, drive_file_id: driveFileId, chunk_index: chunkIndex, chunk_text: chunkText};\n    chunks.push({json: c});\n    chunkIndex++;\n  }\n  i += chunkSize - overlap;\n}\n\nreturn chunks;"
      },
      "typeVersion": 2
    },
    {
      "id": "6c6a50af-6d0b-4fa6-aefd-397170f547af",
      "name": "Loop Chunks",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        272,
        1040
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "682f33a6-1c5b-43eb-8d7f-d5a5eb5d30bd",
      "name": "Embed Chunk",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        480,
        1024
      ],
      "parameters": {
        "url": "http://host.docker.internal:11434/api/embed",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({model: 'nomic-embed-text', input: $json.chunk_text}) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "d53b1be9-a611-4907-ba25-678ca33e2646",
      "name": "Format Chunk Embedding",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        1024
      ],
      "parameters": {
        "jsCode": "const item = $input.first();\nconst embedding = item.json.embeddings[0];\nconst vectorStr = '[' + embedding.join(',') + ']';\nconst chunk = $('Loop Chunks').first().json;\nreturn [{json: {chunk_id: chunk.chunk_id, file_id: chunk.file_id, file_name: chunk.file_name, chunk_index: chunk.chunk_index, chunk_text: chunk.chunk_text, drive_file_id: chunk.drive_file_id, vectorStr: vectorStr}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ee7f7eae-4c0e-4b5b-bd5d-04d1dc5dd51d",
      "name": "Store Chunk",
      "type": "n8n-nodes-base.postgres",
      "position": [
        928,
        1024
      ],
      "parameters": {
        "query": "INSERT INTO document_chunks (chunk_id, file_id, file_name, chunk_index, chunk_text, embedding) VALUES ('={{ $json.chunk_id }}', '={{ $json.file_id }}', '={{ $json.file_name }}', {{ $json.chunk_index }}, '={{ $json.chunk_text.replace(\"'\", \"''\") }}', '={{ $json.vectorStr }}'::vector) ON CONFLICT (chunk_id) DO UPDATE SET chunk_text = EXCLUDED.chunk_text, embedding = EXCLUDED.embedding",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.5
    },
    {
      "id": "c544da09-f14c-4e79-93ad-bd1bcdb9bbbe",
      "name": "Log Ingestion",
      "type": "n8n-nodes-base.postgres",
      "position": [
        544,
        1296
      ],
      "parameters": {
        "query": "INSERT INTO ingestion_log (file_id, file_name, drive_file_id, status) VALUES ($1, $2, $3, 'completed') ON CONFLICT (file_id) DO UPDATE SET status = 'completed', ingested_at = NOW()",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.5
    },
    {
      "id": "a2114b9e-ad3f-45aa-ba94-101b15aba09e",
      "name": "\ud83d\udccb Template Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1520,
        -208
      ],
      "parameters": {
        "color": "#6D6B37",
        "width": 640,
        "height": 1268,
        "content": "## RAG Telegram Bot with Local LLM Ollama\n\nWho is this for?\nTeams or individuals who want a private, PIN-protected Telegram chatbot that answers questions from their own documents  powered entirely by a local LLM stack with no external AI API costs.\n\nWhat You Need\nTelegram Bot (via @BotFather)\nPostgreSQL with pgvector (e.g. Supabase free tier)\nOllama running locally with `nomic-embed-text` + `qwen2.5:7b`\nGoogle Drive account\n\nHow It Works\n\nDocument Ingestion (bottom flow)\n1. New file added to your Google Drive folder\n2. File downloaded & text extracted\n3. Text split into 500-char chunks (50-char overlap)\n4. Each chunk embedded via Ollama `nomic-embed-text`\n5. Embeddings stored in PostgreSQL with pgvector\n\nTelegram Bot (top flow)\n1. User sends a message to the bot\n2. New users are registered and asked for a PIN\n3. Unverified users must enter the correct PIN\n4. Verified users ask questions in plain text\n5. Question is embedded \u2192 top-5 document chunks retrieved via cosine similarity\n6. `qwen2.5:7b` generates a context-aware answer\n7. Answer sent back to the user via Telegram\n8. Query logged to database for audit\n\nRequired PostgreSQL Tables\n\nRun these before activating:\nsql\nCREATE EXTENSION IF NOT EXISTS vector;\n\nCREATE TABLE telegram_users (\n  chat_id TEXT PRIMARY KEY,\n  username TEXT, pin_code TEXT,\n  is_verified BOOLEAN DEFAULT false,\n  pin_attempts INT DEFAULT 0,\n  verified_at TIMESTAMP,\n  created_at TIMESTAMP DEFAULT NOW(),\n  updated_at TIMESTAMP DEFAULT NOW()\n);\n\nCREATE TABLE document_chunks (\n  chunk_id TEXT PRIMARY KEY,\n  file_id TEXT, file_name TEXT,\n  chunk_index INT,\n  chunk_text TEXT,\n  embedding vector(768)\n);\n\nCREATE TABLE query_log (\n  id SERIAL PRIMARY KEY,\n  chat_id TEXT, username TEXT,\n  query TEXT, answer TEXT,\n  match_count INT,\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\nCREATE TABLE ingestion_log (\n  file_id TEXT PRIMARY KEY,\n  file_name TEXT, drive_file_id TEXT,\n  status TEXT,\n  ingested_at TIMESTAMP DEFAULT NOW()\n);"
      },
      "typeVersion": 1
    },
    {
      "id": "0202ce0f-f466-4513-988e-2644011a36c8",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -704,
        -144
      ],
      "parameters": {
        "color": "#6E6E6E",
        "width": 2912,
        "height": 944,
        "content": "## Telegram RAG Bot Flow"
      },
      "typeVersion": 1
    },
    {
      "id": "387269d8-e5e3-4944-9c3b-4ecbc17e5d92",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -704,
        928
      ],
      "parameters": {
        "color": "#6E6E6E",
        "width": 1856,
        "height": 608,
        "content": "## Document Ingestion Flow"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "1135a0b9-356d-41e6-8f6a-7413a37e09ac",
  "connections": {
    "Ask LLM": {
      "main": [
        [
          {
            "node": "Send Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get User": {
      "main": [
        [
          {
            "node": "User State Router",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Text?": {
      "main": [
        [
          {
            "node": "Embed Query",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Text Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Embed Chunk": {
      "main": [
        [
          {
            "node": "Format Chunk Embedding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Embed Query": {
      "main": [
        [
          {
            "node": "Format Query Embedding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Chunks": {
      "main": [
        [
          {
            "node": "Embed Chunk",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Ingestion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Answer": {
      "main": [
        [
          {
            "node": "Log Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Chunk": {
      "main": [
        [
          {
            "node": "Loop Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Prompt": {
      "main": [
        [
          {
            "node": "Ask LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Text": {
      "main": [
        [
          {
            "node": "Split Into Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is New User?": {
      "main": [
        [
          {
            "node": "Register New User",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Is Verified?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Verified?": {
      "main": [
        [
          {
            "node": "Has Text?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "PIN Correct?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PIN Correct?": {
      "main": [
        [
          {
            "node": "Mark User Verified",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wrong PIN Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File": {
      "main": [
        [
          {
            "node": "Extract Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Documents": {
      "main": [
        [
          {
            "node": "Build Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Webhook": {
      "main": [
        [
          {
            "node": "Get User",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Register New User": {
      "main": [
        [
          {
            "node": "Send PIN Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Into Chunks": {
      "main": [
        [
          {
            "node": "Loop Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "User State Router": {
      "main": [
        [
          {
            "node": "Is New User?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark User Verified": {
      "main": [
        [
          {
            "node": "Send Welcome",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "Download File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Chunk Embedding": {
      "main": [
        [
          {
            "node": "Store Chunk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Query Embedding": {
      "main": [
        [
          {
            "node": "Search Documents",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}