Workflow: AI Document Assistant via Telegram + Supabase
Integrations: aggregate, code, extractFromFile, noOp, openWeatherMapTool, set, splitOut, stickyNote, stopAndError, switch, telegram, telegramTrigger

{"id": "LL0TBxEbXoK2zhqp", "meta": {"instanceId": "workflow-0245cfaf", "versionId": "1.0.0", "createdAt": "2025-09-29T07:07:57.467320", "updatedAt": "2025-09-29T07:07:57.467334", "owner": "n8n-user", "license": "MIT", "category": "automation", "status": "active", "priority": "high", "environment": "production"}, "name": "AI Document Assistant via Telegram + Supabase", "tags": ["automation", "n8n", "production-ready", "excellent", "optimized"], "nodes": [{"id": "0213dfab-a1b2-42c9-9ab1-8a0f1de4c4c0", "name": "Google Gemini Chat Model", "type": "n8n-nodes-base.noOp", "position": [480, 40], "parameters": {"options": {}, "modelName": "models/gemini-2.5-flash-preview-04-17"}, "credentials": {"googlePalmApi": {"id": "QuysglXiB421WI90", "name": "Google Gemini(PaLM) Api account"}}, "typeVersion": 1, "notes": "This lmChatGoogleGemini node performs automated tasks as part of the workflow."}, {"id": "9c166f83-8ea4-4dc7-8ea2-92ec186c9f32", "name": "OpenWeatherMap", "type": "n8n-nodes-base.openWeatherMapTool", "position": [740, -100], "parameters": {"cityName": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('City', ``, 'string') }}"}, "credentials": {"openWeatherMapApi": {"id": "MCzSGdWHBJE7l1aN", "name": "OpenWeatherMap account"}}, "typeVersion": 1, "notes": "This openWeatherMapTool node performs automated tasks as part of the workflow."}, {"id": "aa0abeff-b5e9-497b-9d9c-8f79721a5c11", "name": "AI Agent", "type": "n8n-nodes-base.noOp", "position": [480, -320], "parameters": {"text": "={{ $json.message.text }}", "options": {"systemMessage": "=4. If the user sends you a message starting with / sign, it means this is a Telegram bot command. For example, all users send /start command as their first message. Try to figure out what these commands mean and reply accodringly.\nUser can only send pdf files and text messages and let them know that this type is not supported if it was not a PDF file or text.\nAt first let them know that they can ask questions about sent PDF files you can use your own capabilities as well. \nGenerate a detailed, well-structured response ,\nFormat the response strictly using Telegram's supported HTML syntax. Use tags like <b>, <i>, <u>, 
, <span class=\"tg-spoiler\">, <code>, <pre> (with optional <code class=\"language-...\"> inside), <a href=\"...\">, and <blockquote> where appropriate.\n\nStructure the content logically using paragraphs and distinct sections. **Be mindful that this text might need to be split into multiple messages due to character limits (Telegram's limit is around 4096 characters per message). Try to make sections or paragraphs relatively self-contained where possible to facilitate splitting.**\n\n**Ensure all <, >, and & symbols within the *text content* (i.e., not part of an HTML tag or entity) are replaced with the corresponding HTML entities: < with &lt;, > with &gt;, and & with &amp;.**\n\nMaintain proper nesting of HTML tags according to Telegram's rules. While the final splitting will be handled by a script, aim for a structure that is easy to break into logical parts without leaving tags improperly open mid-message."}, "promptType": "define", "hasOutputParser": true}, "typeVersion": 1.9, "notes": "This agent node performs automated tasks as part of the workflow."}, {"id": "72b85aff-4fe7-4705-a07c-463f381cb806", "name": "Telegram Trigger", "type": "n8n-nodes-base.telegramTrigger", "position": [-20, 100], "webhookId": "d4f286b2-8094-40e3-aeb2-813eb1895ecf", "parameters": {"updates": ["message"], "additionalFields": {}}, "credentials": {"telegramApi": {"id": "jOxapcl3g1n1HrCE", "name": "Telegram account"}}, "typeVersion": 1.2, "notes": "This telegramTrigger node performs automated tasks as part of the workflow."}, {"id": "ea716dba-2856-40a8-ad73-86132f52dda8", "name": "Telegram", "type": "n8n-nodes-base.telegram", "onError": "continueErrorOutput", "position": [1540, -320], "webhookId": "137d8d2f-a941-4803-8646-8932525360c3", "parameters": {"text": "={{ $json.text }}", "chatId": "={{ $json.chatId }}", "additionalFields": {"parse_mode": "HTML", "appendAttribution": false}}, "credentials": {"telegramApi": {"id": "jOxapcl3g1n1HrCE", "name": "Telegram account"}}, "typeVersion": 1.2, "alwaysOutputData": true, "notes": "This telegram node performs automated tasks as part of the workflow."}, {"id": "59a22620-0d26-4e19-940a-5c07efccbdfa", "name": "Think", "type": "n8n-nodes-base.noOp", "position": [640, -100], "parameters": {}, "typeVersion": 1, "notes": "This toolThink node performs automated tasks as part of the workflow."}, {"id": "7bb66887-c9c6-4057-bbc0-306d1e20ea12", "name": "Embeddings Google Gemini", "type": "n8n-nodes-base.noOp", "position": [840, 340], "parameters": {"modelName": "models/text-embedding-004"}, "credentials": {"googlePalmApi": {"id": "QuysglXiB421WI90", "name": "Google Gemini(PaLM) Api account"}}, "typeVersion": 1, "notes": "This embeddingsGoogleGemini node performs automated tasks as part of the workflow."}, {"id": "668db8fd-3d5f-433a-8ccb-4bea237107ce", "name": "Default Data Loader", "type": "n8n-nodes-base.noOp", "position": [1220, 460], "parameters": {"options": {}}, "typeVersion": 1, "notes": "This documentDefaultDataLoader node performs automated tasks as part of the workflow."}, {"id": "d1495354-bfc0-4ef1-9102-dc3577580d5b", "name": "Recursive Character Text Splitter", "type": "n8n-nodes-base.noOp", "position": [1440, 620], "parameters": {"options": {}}, "typeVersion": 1, "notes": "This textSplitterRecursiveCharacterTextSplitter node performs automated tasks as part of the workflow."}, {"id": "6a8e5ce6-4c52-4ca0-962f-045ea42dac7c", "name": "Extract from File", "type": "n8n-nodes-base.extractFromFile", "position": [1020, 480], "parameters": {"options": {}, "operation": "pdf"}, "typeVersion": 1, "alwaysOutputData": true, "notes": "This extractFromFile node performs automated tasks as part of the workflow."}, {"id": "3f4a9da9-0364-4861-a6f3-33b1d5c501e0", "name": "Answer questions with a vector store", "type": "n8n-nodes-base.noOp", "position": [860, -60], "parameters": {"description": "Use this data if the user's question appears to reference an uploaded file, document content, or specific information that might be stored in prior user documents. If not relevant, ignore this source."}, "typeVersion": 1.1, "notes": "This toolVectorStore node performs automated tasks as part of the workflow."}, {"id": "933a93c7-9401-4bac-9b9c-395866b46d61", "name": "Supabase Vector Store", "type": "n8n-nodes-base.noOp", "position": [760, 80], "parameters": {"options": {"queryName": "match_documents"}, "tableName": {"__rl": true, "mode": "list", "value": "user_knowledge_base", "cachedResultName": "user_knowledge_base"}}, "credentials": {"supabaseApi": {"id": "jq6dt73fwyUImYqH", "name": "Supabase account"}}, "typeVersion": 1.1, "notes": "This vectorStoreSupabase node performs automated tasks as part of the workflow."}, {"id": "5f37c202-a1ca-4ee0-9de0-267349adffbd", "name": "Sticky Note10", "type": "n8n-nodes-base.stickyNote", "position": [360, 200], "parameters": {"color": 5, "width": 1625, "height": 779, "content": "\u2705 Scenario 2 \u2013 Document Upload and Embedding\n\nFlow for downloading a document sent via Telegram, extracting its text, generating embeddings, and inserting them into Supabase Vector Store."}, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow."}, {"id": "6e9c1070-90bc-4ab7-a8a0-62461bede708", "name": "Sticky Note11", "type": "n8n-nodes-base.stickyNote", "position": [360, -420], "parameters": {"color": 5, "width": 1625, "height": 599, "content": "\u2705 Scenario 1 \u2013 Chatbot Interaction\n\nFlow for handling user messages sent to the bot. Includes accessing weather data, answering questions based on user-uploaded documents, and running code using a code execution tool."}, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow."}, {"id": "3b211a14-6813-459f-8d23-b40fc0eb4bd6", "name": "Telegram - Embedding Complete", "type": "n8n-nodes-base.telegram", "position": [1760, 320], "webhookId": "4eaead72-f9a7-49a3-95ca-b3bc8f6b9a95", "parameters": {"text": "=\u2705 Document saved!\nFeel free to start asking questions about it.", "chatId": "={{ $('Command Router').item.json.message.chat.id }}", "additionalFields": {"appendAttribution": false}}, "credentials": {"telegramApi": {"id": "jOxapcl3g1n1HrCE", "name": "Telegram account"}}, "typeVersion": 1.2, "notes": "This telegram node performs automated tasks as part of the workflow."}, {"id": "05703266-aaed-491d-87a6-ed7f96a9c49a", "name": "Supabase - Save Embeddings", "type": "n8n-nodes-base.noOp", "position": [1200, 320], "parameters": {"mode": "insert", "options": {}, "tableName": {"__rl": true, "mode": "list", "value": "user_knowledge_base", "cachedResultName": "user_knowledge_base"}}, "credentials": {"supabaseApi": {"id": "jq6dt73fwyUImYqH", "name": "Supabase account"}}, "typeVersion": 1.1, "alwaysOutputData": false, "notes": "This vectorStoreSupabase node performs automated tasks as part of the workflow."}, {"id": "3b7db0e6-b551-4698-921a-306e837ceffc", "name": "Command Router", "type": "n8n-nodes-base.switch", "position": [160, 100], "parameters": {"rules": {"values": [{"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "loose"}, "combinator": "and", "conditions": [{"id": "895b32db-777d-4d8e-b1d3-596cc9863d09", "operator": {"type": "boolean", "operation": "exists", "singleValue": true}, "leftValue": "={{ $json.message.document }}", "rightValue": "={{ $json.message.document }}"}]}, "renameOutput": true}, {"outputKey": "YOUR_CREDENTIAL_HERE", "conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "loose"}, "combinator": "and", "conditions": [{"id": "26c12573-8e00-4832-8410-73d2d739c455", "operator": {"type": "boolean", "operation": "exists", "singleValue": true}, "leftValue": "={{ $json.message.text }}", "rightValue": ""}]}, "renameOutput": true}]}, "options": {"fallbackOutput": "extra"}, "looseTypeValidation": true}, "typeVersion": 3.2, "notes": "This switch node performs automated tasks as part of the workflow."}, {"id": "fa06fc6c-3661-4065-81fc-09f93d6a4a25", "name": "Telegram - Download file", "type": "n8n-nodes-base.telegram", "position": [600, 540], "webhookId": "11b8f884-34bc-401c-8978-b28507d96e40", "parameters": {"fileId": "={{ $('Telegram Trigger').item.json.message.document.file_id }}", "resource": "file"}, "credentials": {"telegramApi": {"id": "jOxapcl3g1n1HrCE", "name": "Telegram account"}}, "typeVersion": 1.2, "notes": "This telegram node performs automated tasks as part of the workflow."}, {"id": "756a36aa-187d-48ca-894c-f8c9a79a4794", "name": "Aggregate", "type": "n8n-nodes-base.aggregate", "notes": "This is used to flag the end of progress\u2014no real aggregation.", "position": [1580, 320], "parameters": {"options": {}, "fieldsToAggregate": {"fieldToAggregate": [{}]}}, "notesInFlow": true, "typeVersion": 1}, {"id": "3b49f357-5d21-4710-bd32-3218d23b1bd9", "name": "Fallback- No formatting", "type": "n8n-nodes-base.telegram", "notes": "This is used if, even after HTML formatting,g Telegram wasn't able to process the text, so we send it without formatting.", "position": [1740, -260], "webhookId": "dd2182fe-0b11-4d96-9838-30d60bf8c229", "parameters": {"text": "={{ $('Manual Mapping').item.json.text }}", "chatId": "={{ $('Manual Mapping').item.json.chatId }}", "additionalFields": {"appendAttribution": false}}, "credentials": {"telegramApi": {"id": "jOxapcl3g1n1HrCE", "name": "Telegram account"}}, "notesInFlow": true, "typeVersion": 1.2}, {"id": "eafdbacb-17e5-4de6-a4e9-b986140353e5", "name": "Split Out", "type": "n8n-nodes-base.splitOut", "position": [1120, -320], "parameters": {"options": {}, "fieldToSplitOut": "output"}, "typeVersion": 1, "notes": "This splitOut node performs automated tasks as part of the workflow."}, {"id": "538be3ed-4bd6-4295-ac11-e4d46b943f5a", "name": "Simple Memory", "type": "n8n-nodes-base.noOp", "position": [540, -100], "parameters": {"sessionKey": "YOUR_CREDENTIAL_HERE", "sessionIdType": "customKey"}, "typeVersion": 1.3, "notes": "This memoryBufferWindow node performs automated tasks as part of the workflow."}, {"id": "0afca77d-0e08-4f04-a6d3-b107c1dd54f9", "name": "Handle formatting and split", "type": "n8n-nodes-base.code", "notes": "This is used to prevent Markdown issues in Telegram while sending messages.", "position": [900, -320], "parameters": {"language": "python", "pythonCode": "import re\nimport html\n\ngemini_output_text = _('AI Agent').first().json.output;\n# Regex to match any HTML tag <...>\nHTML_TAG_PATTERN = re.compile(r'(<[^>]*?>)', re.IGNORECASE)\n\n# List of UNSUPPORTED Telegram HTML tag names\nUNSUPPORTED_TAG_NAMES = [\n    'p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol',\n    'table', 'thead', 'tbody', 'tr', 'td', 'th', 'div', 'br', 'font',\n    'span', # Span is unsupported *unless* it has the specific class\n    'a'     # A is unsupported *unless* it has the href attribute\n    # Add more unsupported tags if you encounter them\n]\n\n# Regex to match unsupported opening or closing tags based on the names list\n# This pattern is simplified and might misinterpret complex attributes\nUNSUPPORTED_TAG_PATTERN = re.compile(r'<\\/?(' + '|'.join(UNSUPPORTED_TAG_NAMES) + r')\\b[^>]*?>', re.IGNORECASE)\n\n# Regex to match a span tag *without* the class=\"tg-spoiler\" attribute\n# This tries to capture the tag and its content to remove both\nUNSUPPORTED_SPAN_FULL_PATTERN = re.compile(r'<span(?! class=\"tg-spoiler\"\\b)[^>]*?>.*?<\\/span>', re.IGNORECASE | re.DOTALL) # DOTALL allows . to match newlines\n\n# Regex to match an a tag *without* an href attribute\n# This tries to capture the tag and its content to remove both\nUNSUPPORTED_A_FULL_PATTERN = re.compile(r'<a(?![^>]*href=)[^>]*?>.*?<\\/a>', re.IGNORECASE | re.DOTALL)\n\n\n# --- Cleaning Function (Regex Only) ---\n\ndef unescape_common_html_entities(text):\n    \"\"\"\n    Unescapes a limited set of common HTML entities in text.\n    Does NOT use html.unescape for maximum compatibility with \"no external library\" rule.\n    \"\"\"\n    # Order matters: &amp; must be replaced first!\n    text = text.replace('&amp;', '&')\n    text = text.replace('&lt;', '<')\n    text = text.replace('&gt;', '>')\n    text = text.replace('&quot;', '\"')\n    text = text.replace('&apos;', \"'\")\n    # Add more common entities here if needed, e.g., text = text.replace('&nbsp;', ' ')\n    return text\n\n\ndef clean_html_regex_only(html_string):\n    \"\"\"\n    Cleans HTML string using regex: removes unsupported tags and escapes text content.\n    Handles &apos; and other basic entities.\n    WARNING: This is a regex-based approach and is NOT as robust as using an HTML parser.\n    It may fail on complex or malformed HTML.\n\n    Args:\n        html_string (str): The input HTML string.\n\n    Returns:\n        str: The cleaned HTML string.\n    \"\"\"\n    # 1. Remove unsupported tags and their content where specific attributes are missing\n    # Process specific full patterns first\n    cleaned_text = UNSUPPORTED_SPAN_FULL_PATTERN.sub('', html_string)\n    cleaned_text = UNSUPPORTED_A_FULL_PATTERN.sub('', cleaned_text)\n\n    # 2. Remove remaining unsupported opening/closing tags, leaving content behind\n    cleaned_text = UNSUPPORTED_TAG_PATTERN.sub('', cleaned_text)\n\n    # 3. Split the remaining string into tags and text segments\n    # This pattern captures the tags themselves so we can differentiate them from text\n    parts = HTML_TAG_PATTERN.split(cleaned_text)\n\n    cleaned_parts = []\n    for part in parts:\n        if not part:\n            continue\n\n        if HTML_TAG_PATTERN.fullmatch(part):\n            # If the part is a tag (matches the full tag pattern)\n            # We assume at this point it's a supported tag due to previous removal steps.\n            # Keep the tag as is.\n            cleaned_parts.append(part)\n        else:\n            # If the part is text content\n            # 1. Unescape common HTML entities (like &apos;) that might be in the text\n            unescaped_text = unescape_common_html_entities(part)\n\n            # 2. Escape the literal characters <, >, & that are *in* the text content\n            # This ensures only the characters themselves are escaped, not entities.\n            # Need to escape & first to avoid issues with '&amp;' if it resulted from unescaping or was original.\n            re_escaped_text = unescaped_text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')\n\n            cleaned_parts.append(re_escaped_text)\n\n    # Join the processed parts back into a single string\n    return \"\".join(cleaned_parts)\n\n# --- Splitting Logic ---\nSPLIT_PATTERN_REGEX_ONLY = re.compile(r'(<\\/blockquote>|<\\/pre>|\\n\\n|\\s{2,}|(?<=[.!?])\\s+|<[a-z]+[^>]*?>|<\\/[a-z]+>)', flags=re.IGNORECASE)\n\n\ndef split_telegram_message_regex_only(text, max_length=4096):\n    \"\"\"\n    Splits text into multiple messages based on character count and basic patterns.\n    Operates on text already cleaned by clean_html_regex_only.\n    Does NOT guarantee HTML tag integrity across splits due to lack of parsing.\n\n    Args:\n        text (str): The input text (preferably cleaned by clean_html_regex_only).\n        max_length (int): The maximum length for each message part.\n\n    Returns:\n        list: A list of strings, where each string is a message part.\n    \"\"\"\n    if len(text) <= max_length:\n        return [text]\n\n    messages = []\n    current_chunk = \"\"\n\n    # Split by the defined pattern\n    parts = SPLIT_PATTERN_REGEX_ONLY.split(text)\n\n    for part in parts:\n        # Handle parts that are None (can happen with split) or just short whitespace\n        if part is None or (not part.strip() and len(part) < 2 and part != '\\n\\n'):\n             if part is not None and len(part) > 0: # Keep meaningful whitespace splits like \\n\\n\n                  if len(current_chunk) + len(part) <= max_length:\n                       current_chunk += part\n                  else:\n                       # Split happens within meaningful whitespace, finalize chunk\n                       if current_chunk.strip(): # Only add if chunk has content\n                            messages.append(current_chunk.strip())\n                       current_chunk = part # Start new chunk with the whitespace\n             continue # Skip to next part\n\n\n        # Check if adding the current part exceeds the max length\n        if len(current_chunk) + len(part) > max_length:\n            # If the current chunk is empty or only whitespace after stripping,\n            # it means the 'part' itself is too long to fit in a new chunk.\n            if not current_chunk.strip():\n                # Handle very long individual parts (e.g., a huge code block line, a very long word, a single huge tag)\n                # Hard split the long part. WARNING: This can break tags, words, or escape sequences.\n                while len(part) > max_length:\n                    messages.append(part[:max_length])\n                    part = part[max_length:]\n                if part.strip():\n                    current_chunk = part # Remaining part starts a new chunk\n                else:\n                     current_chunk = \"\" # If remainder is just whitespace, clear\n            else:\n                # The current part makes the chunk too long, finalize the current chunk\n                messages.append(current_chunk.strip())\n                # Start a new chunk with the current part\n                current_chunk = part # Keep original part for the new chunk\n\n        else:\n            # Add the current part to the chunk\n            current_chunk += part\n\n    # Add the last chunk\n    if current_chunk.strip(): # Only add if the final chunk has content\n        messages.append(current_chunk.strip())\n\n    # Clean up any empty messages that might have been created\n    messages = [msg for msg in messages if msg.strip()]\n\n    return messages\n  \ncleaned_html_regex = clean_html_regex_only(gemini_output_text)\nmessage_parts_regex = split_telegram_message_regex_only(cleaned_html_regex)\n\nreturn dict({'output': message_parts_regex })"}, "typeVersion": 2}, {"id": "dbea9e13-6ad4-4eb3-8da1-9db9e2116283", "name": "Sticky Note", "type": "n8n-nodes-base.stickyNote", "position": [2000, -420], "parameters": {"width": 1960, "height": 3520, "content": "# \ud83e\udd16 Telegram AI Assistant for Your Documents (n8n + Supabase + Gemini)\n\nThis project transforms a standard **Telegram bot** into your dedicated AI assistant \u2013 designed to understand and answer questions based on **your own documents**. It seamlessly integrates the power of **Google Gemini** for advanced language capabilities and **Supabase's vector database** for efficient, intelligent document retrieval. Built entirely within the no-code platform **n8n**, it allows you to deploy a sophisticated document chatbot without writing a single line of code.\n\nSimply upload any PDF document to the bot, and instantly gain the ability to chat with it, querying its contents as if it were a knowledgeable expert on your uploaded files.\n\n---\n## \ud83d\udcf9 Watch the Bot in Action\n\n[![\ud835\udde8\ud835\uddfb\ud835\uddf9\ud835\uddf2\ud835\uddee\ud835\ude00\ud835\uddf5\ud835\uddf6\ud835\uddfb\ud835\uddf4 \ud835\uddd4\ud835\udddc \ud835\uddfc\ud835\uddfb \ud835\udde0\ud835\ude06 \ud835\uddd5\ud835\uddfc\ud835\uddfc\ud835\uddf8\ud835\ude00\ud835\uddf5\ud835\uddf2\ud835\uddf9\ud835\uddf3: \ud835\uddd9\ud835\uddf9\ud835\uddfc\ud835\ude04 \ud835\udde3\ud835\uddff\ud835\uddfc\ud835\uddf4\ud835\uddff\ud835\uddee\ud835\uddfa\ud835\uddfa\ud835\uddf6\ud835\uddfb\ud835\uddf4 \ud835\udde3\ud835\uddfc\ud835\ude04\ud835\uddf2\ud835\uddff\ud835\ude00 \ud835\uddee \ud835\udde1\ud835\uddf2\ud835\ude05\ud835\ude01-\ud835\udddf\ud835\uddf2\ud835\ude03\ud835\uddf2\ud835\uddf9 \ud835\udde7\ud835\uddf2\ud835\uddf9\ud835\uddf2\ud835\uddf4\ud835\uddff\ud835\uddee\ud835\uddfa \ud835\uddd5\ud835\uddfc\ud835\ude01\ud83e\udd16]({{ $env.WEBHOOK_URL }}]({{ $env.WEBHOOK_URL }}\n\n**\u25b6\ufe0f Click the image above to watch a live demo on YouTube.** \n\nThis video provides a live demonstration of the bot's core features and how it interacts. See a quick walkthrough of its capabilities and user flow.\n\n---\n\n## \u2728 Ignite Your Workflow: Use Cases\n\nThis project empowers two core interactions:\n\n### 1. Conversational AI Interface (User Inquiry \u2192 Telegram Bot \u2192 Intelligent Answers)\n- Users pose questions directly to the Telegram bot.\n- The bot generates relevant, informative answers using the cutting-edge capabilities of the Google Gemini LLM.\n- Leveraging a powerful vector search mechanism, it can pull specific, contextual information from previously uploaded documents to provide highly relevant and informed responses.\n- (Optional) Augment answers with real-time data, like current **weather information**.\n\n### 2. Effortless Document Integration (User Upload PDF \u2192 Processing \u2192 Searchable Knowledge)\n- Users upload a PDF document directly to the bot.\n- The workflow automatically parses the document content, converts it into numerical representations called embeddings using Gemini's embedding models.\n- These embeddings, alongside the document's text content, are then securely stored in a dedicated **Supabase vector table**, creating a searchable knowledge base.\n- Immediately after successful processing, the document becomes part of the bot's memory, enabling users to ask questions about its contents via the standard chat interface.\n\n---\n## \ud83e\udde0 Core Intelligence Features\n\n- \u2705 **Pure No-Code**: Developed and managed entirely within the intuitive [n8n]({{ $env.WEBHOOK_URL }} automation platform.\n- \ud83d\udcc4 **Seamless PDF Integration**: Easily upload and process PDF documents to expand the bot's knowledge.\n- \ud83e\udde0 **Powered by Google Gemini**: Utilizes Gemini for both generating document embeddings and formulating intelligent conversational responses.\n- \ud83d\uddc2 **Vector Database Memory (Supabase)**: Employs **Supabase as a robust vector database** for storing and efficiently searching document embeddings, providing the bot with long-term memory about your content.\n- **\u26a1\ufe0f Rapid & Private Retrieval**: The vector search allows for swift identification and retrieval of the most relevant document snippets based on the user's query. This approach enhances response speed and significantly improves data privacy, as **the original document content remains securely stored in your Supabase instance, and only the user's query and the retrieved relevant chunks are sent to the LLM for generating a response.**\n- \ud83e\uddf9 **Intelligent HTML Post-processing**: Cleans the LLM's responses by removing HTML tags not supported by Telegram while preserving essential formatting and correctly escaping special characters in the text content.\n- \ud83d\udce4 **Adaptive Message Chunking**: Splits lengthy AI-generated answers into multiple messages that adhere to Telegram's 4096-character limit, ensuring the full response is delivered cleanly.\n- \ud83c\udf26\ufe0f **Dynamic Weather Data**: (Optional) Integrates with OpenWeatherMap to provide current weather information upon request.\n- **\ud83d\udcdd Note on Usage**: This workflow is designed primarily for **personal, single-user** scenarios. It processes each message independently and **does not include multi-user session management**, making it unsuitable for public deployment where different users require separate conversational contexts. For a session-based Telegram bot implemented in Python, you may refer to this project, which is a multi-model telegram bot: [{{ $env.WEBHOOK_URL }}]({{ $env.WEBHOOK_URL }}\n---\n\n## \ud83d\udee0 Getting Started: Setup\n\n### 1. Deploy the Workflow in n8n\n\n- Click the \"Use this workflow\" button on the n8n template page.\n- This will open the workflow directly in your n8n instance, ready for configuration.\n\n\n### 2. Connect Your Services: Configure Credentials\n\nCreate API credentials for the following services within your n8n instance:\n\n| Service\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 | Purpose\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 |\n|------------------|------------------------------------|\n| Telegram API\u00a0 \u00a0 \u00a0| Receiving user messages & sending replies |\n| Google Gemini\u00a0 \u00a0 | Generating embeddings & LLM responses |\n| Supabase\u00a0 \u00a0 \u00a0 \u00a0 \u00a0| Storing & searching document vectors\u00a0|\n| OpenWeatherMap\u00a0 \u00a0| (Optional) Fetching weather data\u00a0 \u00a0 |\n\n### 3. Prepare Your Supabase Knowledge Base\n\nSet up a vector-enabled table in your Supabase project to store your document embeddings. Execute the following SQL commands in your Supabase SQL Editor:\n\n``` sql\n-- Enable the pgvector extension to work with embedding vectors\ncreate extension vector;\n\n-- Create a table to store your documents and their embeddings\ncreate table user_knowledge_base (\n\u00a0 id bigserial primary key,\n\u00a0 content text, -- Stores the text chunk from the document\n\u00a0 metadata jsonb, -- Stores document information (e.g., filename, page number)\n\u00a0 embedding vector(768) -- Stores the vector representation (embedding) generated by Gemini. Adjust dimension if using a different model.\n);\n\n-- Create a function to perform vector similarity search against your documents\ncreate function match_documents (\n\u00a0 query_embedding vector(768),\n\u00a0 match_count int default null,\n\u00a0 filter jsonb DEFAULT '{}'\n) returns table (\n\u00a0 id bigint,\n\u00a0 content text,\n\u00a0 metadata jsonb,\n\u00a0 similarity float\n)\nlanguage plpgsql\nas $$\n#variable_conflict use_column\nbegin\n\u00a0 return query\n\u00a0 select\n\u00a0 \u00a0 id,\n\u00a0 \u00a0 content,\n\u00a0 \u00a0 metadata,\n\u00a0 \u00a0 -- Calculate cosine similarity: 1 - cosine distance (using the '<=>' operator provided by pgvector)\n\u00a0 \u00a0 1 - (user_knowledge_base.embedding <=> query_embedding) as similarity\n\u00a0 from user_knowledge_base\n\u00a0 where metadata @> filter -- Optional: filter results based on metadata\n\u00a0 order by user_knowledge_base.embedding <=> query_embedding -- Order by similarity (closest first)\n\u00a0 limit match_count; -- Limit the number of results\nend;\n$$;\n````\n\nThis sets up the necessary table and a function to perform vector similarity searches, allowing you to find document chunks most similar to a user's query.\n-----\n\n## \ud83d\udcda Integrated Technologies\n\nThis project brings together powerful tools:\n\n  - [n8n]({{ $env.WEBHOOK_URL }} \u2013 The central hub for workflow automation and integration.\n  - [Telegram Bot API]({{ $env.API_BASE_URL }} \u2013 The communication layer for user interaction.\n  - [Supabase]({{ $env.WEBHOOK_URL }} + [pgvector Extension]({{ $env.WEBHOOK_URL }} \u2013 Provides a scalable database with powerful vector search capabilities.\n  - [Google Gemini API]({{ $env.WEBHOOK_URL }} \u2013 The intelligence engine for embeddings and text generation.\n  - [OpenWeatherMap API]({{ $env.API_BASE_URL }} \u2013 (Optional) For adding real-time weather features.\n\n-----\n"}, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow."}, {"id": "965ba2bd-747d-4718-a76e-9f7d685dcea4", "name": "Manual Mapping", "type": "n8n-nodes-base.set", "position": [1320, -320], "parameters": {"options": {}, "assignments": {"assignments": [{"id": "cdeb5bf1-c91c-44ae-bebd-ab3f4ba2561a", "name": "text", "type": "string", "value": "={{ $json.output }}"}, {"id": "7cd7d120-96fa-4539-b343-25bc9b75abb4", "name": "chatId", "type": "number", "value": "={{ $('Command Router').item.json.message.from.id }}"}]}}, "typeVersion": 3.4, "notes": "This set node performs automated tasks as part of the workflow."}, {"id": "c6a315f1-6f0b-4127-b377-b7b12975929f", "name": "Unsupported message", "type": "n8n-nodes-base.telegram", "position": [500, 760], "webhookId": "52f3456a-06ef-4799-b245-0293213dcc4b", "parameters": {"text": "Unsupported command or file. \ud83d\ude13 Please upload a valid PDF document or ask your question regarding your files.", "chatId": "={{ $('Command Router').item.json.message.chat.id }}", "additionalFields": {"appendAttribution": false}}, "credentials": {"telegramApi": {"id": "jOxapcl3g1n1HrCE", "name": "Telegram account"}}, "typeVersion": 1.2, "notes": "This telegram node performs automated tasks as part of the workflow."}, {"id": "375bd185-3836-4f25-8708-d6dcd79b2675", "name": "Send processing document message", "type": "n8n-nodes-base.telegram", "position": [920, 720], "webhookId": "32ade357-f14b-4d10-91f2-02c8aa6e198e", "parameters": {"text": "=<b>Processing document...</b>\n<b>Please wait...\u23f3</b>", "chatId": "={{ $('Command Router').item.json.message.chat.id }}", "additionalFields": {"parse_mode": "HTML", "appendAttribution": false}}, "credentials": {"telegramApi": {"id": "jOxapcl3g1n1HrCE", "name": "Telegram account"}}, "typeVersion": 1.2, "notes": "This telegram node performs automated tasks as part of the workflow."}, {"id": "d01f8b15-e495-46cf-bfdf-20b4399c23d7", "name": "Send embedding Started message", "type": "n8n-nodes-base.telegram", "position": [1220, 660], "webhookId": "32ade357-f14b-4d10-91f2-02c8aa6e198e", "parameters": {"text": "=<b>Document processed \u2705 </b> \n<b>Num of pages:</b> {{ $json.numpages }} \n<b>Creator:</b> {{ $json.info.Creator }}\n<b>Title:</b> {{ $json.info.Title }} \n<b>Version:</b> {{ $json.version }}", "chatId": "={{ $('Command Router').item.json.message.chat.id }}", "additionalFields": {"parse_mode": "HTML", "appendAttribution": false}}, "credentials": {"telegramApi": {"id": "jOxapcl3g1n1HrCE", "name": "Telegram account"}}, "typeVersion": 1.2, "notes": "This telegram node performs automated tasks as part of the workflow."}, {"id": "error-4a8a58c1", "name": "Error Handler", "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [1000, 400], "parameters": {"message": "Workflow execution error", "options": {}}}], "active": true, "pinData": {}, "settings": {"executionOrder": "v1", "saveManualExecutions": true, "callerPolicy": "workflowsFromSameOwner", "errorWorkflow": null, "timezone": "UTC", "executionTimeout": 3600, "maxExecutions": 1000, "retryOnFail": true, "retryCount": 3, "retryDelay": 1000}, "versionId": "749ec7d0-e135-478a-b02e-9241dbf4ab68", "connections": {"0213dfab-a1b2-42c9-9ab1-8a0f1de4c4c0": {"main": [[{"node": "error-handler-0213dfab-a1b2-42c9-9ab1-8a0f1de4c4c0-ce2f6a8a", "type": "main", "index": 0}]]}, "72b85aff-4fe7-4705-a07c-463f381cb806": {"main": [[{"node": "error-handler-72b85aff-4fe7-4705-a07c-463f381cb806-85e53507", "type": "main", "index": 0}]]}, "ea716dba-2856-40a8-ad73-86132f52dda8": {"main": [[{"node": "error-handler-ea716dba-2856-40a8-ad73-86132f52dda8-2834b68c", "type": "main", "index": 0}]]}, "7bb66887-c9c6-4057-bbc0-306d1e20ea12": {"main": [[{"node": "error-handler-7bb66887-c9c6-4057-bbc0-306d1e20ea12-297b6949", "type": "main", "index": 0}]]}, "6a8e5ce6-4c52-4ca0-962f-045ea42dac7c": {"main": [[{"node": "error-handler-6a8e5ce6-4c52-4ca0-962f-045ea42dac7c-467a65bb", "type": "main", "index": 0}]]}, "3b211a14-6813-459f-8d23-b40fc0eb4bd6": {"main": [[{"node": "error-handler-3b211a14-6813-459f-8d23-b40fc0eb4bd6-01e33af6", "type": "main", "index": 0}]]}, "fa06fc6c-3661-4065-81fc-09f93d6a4a25": {"main": [[{"node": "error-handler-fa06fc6c-3661-4065-81fc-09f93d6a4a25-28334ab8", "type": "main", "index": 0}]]}, "3b49f357-5d21-4710-bd32-3218d23b1bd9": {"main": [[{"node": "error-handler-3b49f357-5d21-4710-bd32-3218d23b1bd9-c20cacf7", "type": "main", "index": 0}]]}, "c6a315f1-6f0b-4127-b377-b7b12975929f": {"main": [[{"node": "error-handler-c6a315f1-6f0b-4127-b377-b7b12975929f-f6d45555", "type": "main", "index": 0}]]}, "375bd185-3836-4f25-8708-d6dcd79b2675": {"main": [[{"node": "error-handler-375bd185-3836-4f25-8708-d6dcd79b2675-ae8a9b89", "type": "main", "index": 0}]]}, "d01f8b15-e495-46cf-bfdf-20b4399c23d7": {"main": [[{"node": "error-handler-d01f8b15-e495-46cf-bfdf-20b4399c23d7-b193ebe3", "type": "main", "index": 0}]]}}, "description": "Automated workflow: AI Document Assistant via Telegram + Supabase. This workflow integrates 20 different services: stickyNote, textSplitterRecursiveCharacterTextSplitter, switch, toolThink, extractFromFile. It contains 39 nodes and follows best practices for error handling and security.", "notes": "Excellent quality workflow: AI Document Assistant via Telegram + Supabase. This workflow has been optimized for production use with comprehensive error handling, security, and documentation."}