{
  "name": "Local AI Agent (HTTP-based)",
  "nodes": [
    {
      "parameters": {
        "path": "telegram-ai-agent",
        "httpMethod": "POST",
        "responseMode": "onReceived",
        "options": {}
      },
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [
        -520,
        300
      ]
    },
    {
      "parameters": {
        "functionCode": "const body = $json.body || {};\nconst message = body.message || {};\nconst chat = message.chat || {};\nconst text = message.text || '';\nconst document = message.document;\n\nconst isWebSearch = text.startsWith('/search ');\nconst isDocument = !!document;\n\nreturn [{\n  chatId: chat.id,\n  originalText: text,\n  isWebSearch,\n  isDocument,\n  document,\n  query: isWebSearch ? text.replace('/search ', '') : text\n}];"
      },
      "name": "Parse Telegram",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -300,
        300
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "Thinking...",
        "additionalFields": {}
      },
      "name": "Telegram Typing",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1,
      "position": [
        -90,
        140
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.isDocument }}",
              "value2": true
            }
          ],
          "string": []
        }
      },
      "name": "If Document",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        -90,
        560
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.isWebSearch }}",
              "value2": true
            }
          ],
          "string": []
        }
      },
      "name": "If Web Search",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        90,
        420
      ]
    },
    {
      "parameters": {
        "requestMethod": "GET",
        "url": "={{ 'https://api.telegram.org/bot' + $env.TELEGRAM_TOKEN + '/getFile' }}",
        "jsonParameters": true,
        "options": {},
        "sendQuery": true,
        "queryParametersJson": "={{ JSON.stringify({ \"file_id\": $json.document.file_id }) }}"
      },
      "name": "Get File Path",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        200,
        700
      ]
    },
    {
      "parameters": {
        "requestMethod": "GET",
        "url": "={{ 'https://api.telegram.org/file/bot' + $env.TELEGRAM_TOKEN + '/' + $json.result.file_path }}",
        "jsonParameters": true,
        "options": {},
        "responseFormat": "string"
      },
      "name": "Download File",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        400,
        700
      ]
    },
    {
      "parameters": {
        "functionCode": "const text = $json.data || '';\n// Simple chunking by paragraphs or length\nconst chunkSize = 1000;\nconst chunks = [];\n\n// Basic split by newlines first\nconst paragraphs = text.split(/\\n\\s*\\n/);\n\nlet currentChunk = '';\nfor (const p of paragraphs) {\n  if ((currentChunk + p).length > chunkSize) {\n    if (currentChunk) chunks.push(currentChunk.trim());\n    currentChunk = p;\n  } else {\n    currentChunk += (currentChunk ? '\\n\\n' : '') + p;\n  }\n}\nif (currentChunk) chunks.push(currentChunk.trim());\n\n// If no paragraphs (single line), force split\nif (chunks.length === 0 && text.length > 0) {\n  for (let i = 0; i < text.length; i += chunkSize) {\n    chunks.push(text.slice(i, i + chunkSize));\n  }\n}\n\nreturn chunks.map((chunk, index) => ({\n  chunk,\n  chunkIndex: index,\n  totalChunks: chunks.length,\n  fileName: $node[\"Parse Telegram\"].json.document.file_name,\n  chatId: $node[\"Parse Telegram\"].json.chatId\n}));"
      },
      "name": "Split Text",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        600,
        700
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "={{ ($env.OLLAMA_BASE_URL || 'http://ollama:11434') + '/api/embeddings' }}",
        "jsonParameters": true,
        "options": {},
        "headerParameters": [
          {
            "name": "Authorization",
            "value": "={{ $env.OLLAMA_API_KEY ? ('Bearer ' + $env.OLLAMA_API_KEY) : '' }}"
          }
        ],
        "bodyParametersJson": "={{ JSON.stringify({\n  \"model\": $env.OLLAMA_MODEL || \"llama3.2:3b\",\n  \"prompt\": $json.chunk\n}) }}"
      },
      "name": "Embed Chunk",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        800,
        700
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "={{ 'http://chroma:8000/api/v1/collections/' + $('Merge Collection ID').first().json.collectionId + '/upsert' }}",
        "jsonParameters": true,
        "options": {},
        "bodyParametersJson": "={{ JSON.stringify({\n  \"ids\": [ $('Parse Telegram').first().json.document.file_id + '_' + $json.chunkIndex ],\n  \"embeddings\": [ $json.embedding ],\n  \"documents\": [ $json.chunk ],\n  \"metadatas\": [ { \"source\": \"file\", \"filename\": $json.fileName } ]\n}) }}"
      },
      "name": "Upsert Chunk",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        1000,
        700
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $('Parse Telegram').first().json.chatId }}",
        "text": "={{ 'I have read \"' + $('Parse Telegram').first().json.document.file_name + '\". Processed ' + ($('Split Text').item.json.chunkIndex + 1) + ' of ' + $('Split Text').item.json.totalChunks + ' chunks.' }}",
        "additionalFields": {}
      },
      "name": "Notify Progress",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1,
      "position": [
        1200,
        700
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "http://chroma:8000/api/v1/collections",
        "jsonParameters": true,
        "options": {},
        "bodyParametersJson": "{\"name\": \"ai_memory\", \"get_or_create\": true}"
      },
      "name": "Get Collection",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -200,
        420
      ]
    },
    {
      "parameters": {
        "functionCode": "const original = $node[\"Parse Telegram\"].json;\nconst collection = $json;\nreturn [{ ...original, collectionId: collection.id }];"
      },
      "name": "Merge Collection ID",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        200,
        420
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "={{ ($env.OLLAMA_BASE_URL || 'http://ollama:11434') + '/api/embeddings' }}",
        "jsonParameters": true,
        "options": {},
        "headerParameters": [
          {
            "name": "Authorization",
            "value": "={{ $env.OLLAMA_API_KEY ? ('Bearer ' + $env.OLLAMA_API_KEY) : '' }}"
          }
        ],
        "bodyParametersJson": "={{ JSON.stringify({\n  \"model\": $env.OLLAMA_MODEL || \"llama3.2:3b\",\n  \"prompt\": $json.query\n}) }}"
      },
      "name": "Generate Query Embedding",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        140,
        220
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "={{ 'http://chroma:8000/api/v1/collections/' + $node[\"Merge Collection ID\"].json.collectionId + '/query' }}",
        "jsonParameters": true,
        "options": {},
        "bodyParametersJson": "={{ JSON.stringify({ \"query_embeddings\": [ $json.embedding ], \"n_results\": 3 }) }}"
      },
      "name": "Chroma Query",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        340,
        220
      ]
    },
    {
      "parameters": {
        "functionCode": "const results = $json.documents || [];\nlet context = '';\nif (Array.isArray(results) && results.length > 0) {\n  const docs = results[0];\n  if (Array.isArray(docs)) {\n    context = docs.join('\\n');\n  }\n}\n\n// Access original data from Parse Telegram node\nconst originalData = $('Parse Telegram').first().json;\n\nreturn [{\n  chatId: originalData.chatId,\n  originalText: originalData.originalText,\n  query: originalData.query,\n  isWebSearch: originalData.isWebSearch,\n  memoryContext: context\n}];"
      },
      "name": "Build RAG Context",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        540,
        220
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "https://api.tavily.com/search",
        "jsonParameters": true,
        "options": {},
        "bodyParametersJson": "={{ JSON.stringify({\n  \"api_key\": $env.TAVILY_API_KEY,\n  \"query\": $json.query,\n  \"search_depth\": \"advanced\",\n  \"max_results\": 7,\n  \"include_answer\": true,\n  \"include_raw_content\": true\n}) }}"
      },
      "name": "Tavily Search",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        140,
        520
      ]
    },
    {
      "parameters": {
        "functionCode": "let webContext = '';\n\nif ($json.answer) {\n  webContext += `Tavily Direct Answer:\\n${$json.answer}\\n\\n`;\n}\n\nif (Array.isArray($json.results)) {\n  webContext += \"Search Results:\\n\";\n  webContext += $json.results.map((r, i) => `[${i+1}] Title: ${r.title}\\nURL: ${r.url}\\nContent: ${r.content}`).join('\\n\\n');\n}\n\n// Access original data from Parse Telegram node\nconst originalData = $('Parse Telegram').first().json;\n\nreturn [{\n  chatId: originalData.chatId,\n  originalText: originalData.originalText,\n  query: originalData.query,\n  memoryContext: '',\n  webContext\n}];"
      },
      "name": "Build Web Context",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        360,
        520
      ]
    },
    {
      "parameters": {
        "functionCode": "const system = '\u0422\u044b \u0440\u0443\u0441\u0441\u043a\u043e\u044f\u0437\u044b\u0447\u043d\u044b\u0439 \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442 \u0432 Telegram. \u041e\u0442\u0432\u0435\u0447\u0430\u0439 \u0441\u0442\u0440\u043e\u0433\u043e \u043f\u043e \\\"Web search context\\\". \u041d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439 \u0432\u043d\u0435\u0448\u043d\u0438\u0435 \u0437\u043d\u0430\u043d\u0438\u044f. \u0415\u0441\u043b\u0438 \u043e\u0442\u0432\u0435\u0442\u0430 \u043d\u0435\u0442 \u0432 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0435, \u043f\u0440\u044f\u043c\u043e \u0441\u043a\u0430\u0436\u0438 \u043e\u0431 \u044d\u0442\u043e\u043c. \u041f\u0440\u0438\u0432\u043e\u0434\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u043d\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 [1], [2]. \u0411\u0443\u0434\u044c \u043a\u0440\u0430\u0442\u043e\u043a.';\n\nconst parts = [];\nif ($json.memoryContext) {\n  parts.push('Memory context:\\n' + $json.memoryContext);\n}\nif ($json.webContext) {\n  parts.push('Web search context:\\n' + $json.webContext);\n}\nconst context = parts.join('\\n\\n');\n\nlet content = '';\nif (context) {\n  content = `<<CONTEXT_START>>\\n${context}\\n<<CONTEXT_END>>\\n\\n\u0412\u043e\u043f\u0440\u043e\u0441: ${$json.query}\\n\u041e\u0442\u0432\u0435\u0442:`;\n} else {\n  content = `\u0412\u043e\u043f\u0440\u043e\u0441: ${$json.query}\\n\u041e\u0442\u0432\u0435\u0442:`;\n}\n\nreturn [{\n  chatId: $json.chatId,\n  prompt: content,\n  system,\n  query: $json.query\n}];"
      },
      "name": "Build Prompt",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        580,
        340
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "={{ ($env.OLLAMA_BASE_URL || 'http://ollama:11434') + '/api/chat' }}",
        "jsonParameters": true,
        "options": {},
        "headerParameters": [
          {
            "name": "Authorization",
            "value": "={{ $env.OLLAMA_API_KEY ? ('Bearer ' + $env.OLLAMA_API_KEY) : '' }}"
          }
        ],
        "bodyParametersJson": "={{ JSON.stringify({\n  \"model\": ($env.OLLAMA_MODEL || \"llama3.2:3b\"),\n  \"keep_alive\": \"30m\",\n  \"stream\": false,\n  \"options\": { \"temperature\": 0 },\n  \"messages\": [\n    {\"role\": \"system\", \"content\": $json.system},\n    {\"role\": \"user\", \"content\": $json.prompt}\n  ]\n}) }}"
      },
      "name": "Ollama Chat",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        800,
        340
      ]
    },
    {
      "parameters": {
        "functionCode": "const resp = $json;\nlet llmAnswer = '';\nif (resp.message && resp.message.content) {\n  llmAnswer = resp.message.content;\n} else if (resp.error) {\n  llmAnswer = \"\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442 AI: \" + JSON.stringify(resp.error);\n} else {\n  llmAnswer = \"\u041d\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430 \u043e\u0442 AI. \u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442: \" + JSON.stringify(resp);\n}\n\nlet tavilyAnswer = '';\ntry {\n  const t = $('Tavily Search').first().json;\n  if (t && t.answer) tavilyAnswer = t.answer;\n} catch (e) {}\n\nconst finalAnswer = tavilyAnswer || llmAnswer;\n\nconst originalData = $('Parse Telegram').first().json;\nconst promptData = $('Build Prompt').first().json;\n\nreturn [{\n  chatId: originalData.chatId,\n  answer: finalAnswer,\n  question: promptData.query\n}];"
      },
      "name": "Extract Answer",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        1020,
        340
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "={{ ($env.OLLAMA_BASE_URL || 'http://ollama:11434') + '/api/embeddings' }}",
        "jsonParameters": true,
        "options": {},
        "headerParameters": [
          {
            "name": "Authorization",
            "value": "={{ $env.OLLAMA_API_KEY ? ('Bearer ' + $env.OLLAMA_API_KEY) : '' }}"
          }
        ],
        "bodyParametersJson": "={{ JSON.stringify({\n  \"model\": $env.OLLAMA_MODEL || \"llama3.2:3b\",\n  \"prompt\": ($json.question + \"\\n\\n\" + $json.answer).substring(0, 4000)\n}) }}"
      },
      "name": "Generate Answer Embedding",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        1240,
        220
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "={{ 'http://chroma:8000/api/v1/collections/' + $('Merge Collection ID').first().json.collectionId + '/upsert' }}",
        "jsonParameters": true,
        "options": {},
        "bodyParametersJson": "={{ JSON.stringify({\n  \"ids\": [ Date.now().toString() ],\n  \"embeddings\": [ $json.embedding ],\n  \"documents\": [ $('Extract Answer').first().json.question + \"\\n\\n\" + $('Extract Answer').first().json.answer ],\n  \"metadatas\": [ { \"source\": \"telegram\" } ]\n}) }}"
      },
      "name": "Chroma Upsert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        1440,
        220
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.answer }}",
        "additionalFields": {}
      },
      "name": "Telegram Send Answer",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1,
      "position": [
        1640,
        460
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Parse Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Telegram": {
      "main": [
        [
          {
            "node": "Telegram Typing",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Collection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Collection": {
      "main": [
        [
          {
            "node": "Merge Collection ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Collection ID": {
      "main": [
        [
          {
            "node": "If Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Document": {
      "main": [
        [
          {
            "node": "Get File Path",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If Web Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Web Search": {
      "main": [
        [
          {
            "node": "Tavily Search",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Query Embedding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Query Embedding": {
      "main": [
        [
          {
            "node": "Chroma Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chroma Query": {
      "main": [
        [
          {
            "node": "Build RAG Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tavily Search": {
      "main": [
        [
          {
            "node": "Build Web Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build RAG Context": {
      "main": [
        [
          {
            "node": "Build Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Web Context": {
      "main": [
        [
          {
            "node": "Build Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Prompt": {
      "main": [
        [
          {
            "node": "Ollama Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ollama Chat": {
      "main": [
        [
          {
            "node": "Extract Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Answer": {
      "main": [
        [
          {
            "node": "Generate Answer Embedding",
            "type": "main",
            "index": 0
          },
          {
            "node": "Telegram Send Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Answer Embedding": {
      "main": [
        [
          {
            "node": "Chroma Upsert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get File Path": {
      "main": [
        [
          {
            "node": "Download File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File": {
      "main": [
        [
          {
            "node": "Split Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Text": {
      "main": [
        [
          {
            "node": "Embed Chunk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Embed Chunk": {
      "main": [
        [
          {
            "node": "Upsert Chunk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Chunk": {
      "main": [
        [
          {
            "node": "Notify Progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}