From a8d634f422e77195be23bed5d4043485bdf082c9 Mon Sep 17 00:00:00 2001 From: bilal Date: Tue, 23 Jun 2026 20:20:56 +0300 Subject: [PATCH] Initial project setup for Telegram-to-Qdrant knowledge base. Add unified n8n workflow, userbot ingestion script, and detailed README for running n8n with a separate Dockerized Telethon collector. Co-authored-by: Cursor --- .env.example | 6 + .gitignore | 8 + DBBot Unified n8n Qdrant.json | 403 +++++++++++++++++++++++++++ Dockerfile | 12 + TG Fixed Native.json | 155 +++++++++++ docker-compose.yaml | 40 +++ redme.md | 199 +++++++++++++ requirements.txt | 2 + script.py | 164 +++++++++++ Работа с базой (3).json | 268 ++++++++++++++++++ Создание базы (2).json | 183 ++++++++++++ Создание базы (TG Text) (2).json | 178 ++++++++++++ Создание базы (TG Text) - Fixed.json | 150 ++++++++++ 13 files changed, 1768 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 DBBot Unified n8n Qdrant.json create mode 100644 Dockerfile create mode 100644 TG Fixed Native.json create mode 100644 docker-compose.yaml create mode 100644 redme.md create mode 100644 requirements.txt create mode 100644 script.py create mode 100644 Работа с базой (3).json create mode 100644 Создание базы (2).json create mode 100644 Создание базы (TG Text) (2).json create mode 100644 Создание базы (TG Text) - Fixed.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..358b546 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +TG_API_ID=12345678 +TG_API_HASH=your_telegram_api_hash +TG_GROUP_ID=-1001234567890 +N8N_WEBHOOK_URL=http://n8n:5678/webhook/tg-inbox +SESSION_FILE_PATH=/app/session/session.session +HISTORY_CHECK_FILE=/app/session/history_done.flag diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65e4103 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +__pycache__/ +*.pyc +.env +session_data/session.session +session_data/history_done.flag +n8n_data/ +qdrant_data/ diff --git a/DBBot Unified n8n Qdrant.json b/DBBot Unified n8n Qdrant.json new file mode 100644 index 0000000..61cfb4d --- /dev/null +++ b/DBBot Unified n8n Qdrant.json @@ -0,0 +1,403 @@ +{ + "name": "DBBot Unified (Ingest + RAG)", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "tg-inbox", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -640, + -120 + ], + "id": "31d39bff-4f9a-4645-94b2-b38d4ebf0bf0", + "name": "Webhook TG Inbox", + "webhookId": "dbbot-tg-inbox-webhook" + }, + { + "parameters": { + "jsCode": "const body = $input.item.json.body || {};\nconst meta = body.metadata || {};\nconst rawText = (body.text || '').trim();\n\nif (!rawText) {\n return { json: { skip: true, reason: 'empty_text' } };\n}\n\nconst authorLabel = meta.author_label || meta.sender_name || 'Unknown';\nconst authorId = meta.sender_id || meta.author?.id || '0';\nconst repliedTo = meta.replied_to_author_label ? ` (в ответ ${meta.replied_to_author_label})` : '';\n\nreturn {\n json: {\n skip: false,\n text: `Автор ${authorLabel} (ID: ${authorId})${repliedTo}: ${rawText}`,\n metadata: {\n ...meta,\n original_text: rawText,\n source: meta.source || 'telegram_group',\n processed_at: new Date().toISOString()\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -416, + -120 + ], + "id": "a9347d4b-fad0-4fb8-a23c-bcad6d2f3186", + "name": "Normalize Telegram Payload" + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.skip }}", + "value2": true + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + -192, + -120 + ], + "id": "de52e6d1-98be-4c24-bc03-d90d7fa80b93", + "name": "Skip Empty Text" + }, + { + "parameters": { + "mode": "insert", + "qdrantCollection": { + "__rl": true, + "value": "telegram_kb", + "mode": "list", + "cachedResultName": "telegram_kb" + }, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant", + "typeVersion": 1.3, + "position": [ + 288, + -120 + ], + "id": "2444f3f3-d57d-45fa-b80e-e40f73f0c70f", + "name": "Qdrant Insert" + }, + { + "parameters": { + "jsCode": "const WINDOW_MS = 20 * 60 * 1000;\nconst MAX_MESSAGES = 12;\nconst MIN_MESSAGES_TO_FLUSH = 3;\n\nfunction aggregateBuffer(buffer) {\n const lines = buffer.messages.map((m) => {\n const repliedTo = m.replyTo ? ` -> ${m.replyTo}` : '';\n return `[${m.date}] ${m.author}${repliedTo}: ${m.text}`;\n });\n\n return {\n skip: false,\n text: [\n `Контекст диалога (${buffer.messages.length} сообщений, участники: ${buffer.participants.join(', ')})`,\n ...lines,\n ].join('\\n'),\n metadata: {\n ...buffer.lastMeta,\n context_id: buffer.contextId,\n context_message_count: buffer.messages.length,\n context_start_date: new Date(buffer.startTs).toISOString(),\n context_end_date: new Date(buffer.lastTs).toISOString(),\n context_participants: buffer.participants,\n context_mode: 'thread_time_window_20m'\n }\n };\n}\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.contextBuffers = staticData.contextBuffers || {};\n\nconst meta = $json.metadata || {};\nconst rawText = (meta.original_text || '').trim();\nif (!rawText) {\n return { json: { skip: true, reason: 'empty_original_text' } };\n}\n\nconst nowTs = meta.date ? new Date(meta.date).getTime() : Date.now();\nconst safeTs = Number.isNaN(nowTs) ? Date.now() : nowTs;\nconst chatId = String(meta.chat_id || meta.group_id || 'unknown_chat');\nconst threadId = String(meta.thread_id || meta.reply_to_message_id || meta.message_id || 'single');\nconst contextId = `${chatId}:${threadId}`;\n\nlet buffer = staticData.contextBuffers[contextId];\n\nif (buffer && safeTs - buffer.lastTs > WINDOW_MS) {\n const staleBuffer = buffer;\n buffer = null;\n\n const author = meta.author_label || meta.sender_name || 'Unknown';\n staticData.contextBuffers[contextId] = {\n contextId,\n startTs: safeTs,\n lastTs: safeTs,\n participants: [author],\n lastMeta: meta,\n messages: [\n {\n date: meta.date || new Date(safeTs).toISOString(),\n author,\n replyTo: meta.replied_to_author_label || '',\n text: rawText\n }\n ]\n };\n\n if (staleBuffer.messages.length >= MIN_MESSAGES_TO_FLUSH) {\n return { json: aggregateBuffer(staleBuffer) };\n }\n\n return { json: { skip: true, reason: 'buffer_reset_on_timeout', context_id: contextId } };\n}\n\nif (!buffer) {\n buffer = {\n contextId,\n startTs: safeTs,\n lastTs: safeTs,\n participants: [],\n lastMeta: meta,\n messages: []\n };\n}\n\nconst author = meta.author_label || meta.sender_name || 'Unknown';\nif (!buffer.participants.includes(author)) {\n buffer.participants.push(author);\n}\n\nbuffer.messages.push({\n date: meta.date || new Date(safeTs).toISOString(),\n author,\n replyTo: meta.replied_to_author_label || '',\n text: rawText\n});\nbuffer.lastMeta = meta;\nbuffer.lastTs = safeTs;\n\nconst shouldFlush =\n buffer.messages.length >= MAX_MESSAGES ||\n buffer.lastTs - buffer.startTs >= WINDOW_MS;\n\nif (!shouldFlush) {\n staticData.contextBuffers[contextId] = buffer;\n return {\n json: {\n skip: true,\n reason: 'buffering',\n context_id: contextId,\n buffered_messages: buffer.messages.length\n }\n };\n}\n\ndelete staticData.contextBuffers[contextId];\nreturn { json: aggregateBuffer(buffer) };" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 32, + -120 + ], + "id": "b3866944-b7ca-4505-b89e-a5f3d331f43d", + "name": "Build Context Block" + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.skip }}", + "value2": true + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 160, + -120 + ], + "id": "a343f1ca-2423-43e7-9b8d-ce4f48f1e5ff", + "name": "Skip Buffered Context" + }, + { + "parameters": { + "jsonMode": "expressionData", + "jsonData": "={{ $json.text }}", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", + "typeVersion": 1.1, + "position": [ + 288, + 56 + ], + "id": "3de4bfe4-bd8a-4f36-a9ec-8955f2f26f83", + "name": "Document Loader" + }, + { + "parameters": { + "model": "qwen3-embedding:8b" + }, + "type": "@n8n/n8n-nodes-langchain.embeddingsOllama", + "typeVersion": 1, + "position": [ + 96, + 56 + ], + "id": "5f48d9eb-ec81-4a00-b2fa-ec6a1fc2f009", + "name": "Embeddings For Insert" + }, + { + "parameters": { + "updates": [ + "message" + ], + "additionalFields": {} + }, + "type": "n8n-nodes-base.telegramTrigger", + "typeVersion": 1.2, + "position": [ + -640, + 288 + ], + "id": "163fbd89-644c-4858-a2ce-8c572a2d3593", + "name": "Telegram Trigger" + }, + { + "parameters": { + "promptType": "define", + "text": "=Ответь на вопрос пользователя, используя данные из Qdrant Vector Store: {{ $json.message.text }}", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "position": [ + -384, + 288 + ], + "id": "e67a8860-3c52-407e-8fd7-1f481e3c5019", + "name": "AI Agent" + }, + { + "parameters": { + "model": "qwen3:30b-a3b", + "options": { + "temperature": 0 + } + }, + "type": "@n8n/n8n-nodes-langchain.lmChatOllama", + "typeVersion": 1, + "position": [ + -352, + 496 + ], + "id": "3c7db13e-1be9-4f2f-ab26-a613db89ea71", + "name": "Ollama Chat Model" + }, + { + "parameters": { + "mode": "retrieve-as-tool", + "toolDescription": "Всегда используй этот инструмент для ответа: в нем релевантные документы из базы знаний.", + "qdrantCollection": { + "__rl": true, + "value": "telegram_kb", + "mode": "list", + "cachedResultName": "telegram_kb" + }, + "includeDocumentMetadata": true, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant", + "typeVersion": 1.3, + "position": [ + -128, + 528 + ], + "id": "2f204e2e-58e7-4514-9f3c-3e23949f8520", + "name": "Qdrant Retrieve Tool" + }, + { + "parameters": { + "model": "qwen3-embedding:8b" + }, + "type": "@n8n/n8n-nodes-langchain.embeddingsOllama", + "typeVersion": 1, + "position": [ + -128, + 704 + ], + "id": "e5d5f376-bb95-4a16-ab11-e5e3e7d74524", + "name": "Embeddings For Retrieve" + }, + { + "parameters": { + "jsCode": "let rawText = $input.item.json.text || $input.item.json.response || $input.item.json.output || '';\nlet cleanText = rawText.replace(/[\\s\\S]*?<\\/think>/g, '').trim();\n\nif (!cleanText) {\n cleanText = 'Не смог сформировать ответ. Попробуйте переформулировать вопрос.';\n}\n\nreturn {\n json: {\n clean_response: cleanText\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -128, + 288 + ], + "id": "81f5d5e9-3f13-4d43-b8e2-7112e2ca63e4", + "name": "Clean Agent Output" + }, + { + "parameters": { + "chatId": "={{ $('Telegram Trigger').item.json.message.from.id }}", + "text": "={{ $json.clean_response }}", + "additionalFields": { + "appendAttribution": false + } + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 96, + 288 + ], + "id": "e8476b38-8344-4aaf-828e-5da8344b79d2", + "name": "Send Telegram Reply" + } + ], + "pinData": {}, + "connections": { + "Webhook TG Inbox": { + "main": [ + [ + { + "node": "Normalize Telegram Payload", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Telegram Payload": { + "main": [ + [ + { + "node": "Skip Empty Text", + "type": "main", + "index": 0 + } + ] + ] + }, + "Skip Empty Text": { + "main": [ + [], + [ + { + "node": "Build Context Block", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Context Block": { + "main": [ + [ + { + "node": "Skip Buffered Context", + "type": "main", + "index": 0 + } + ] + ] + }, + "Skip Buffered Context": { + "main": [ + [], + [ + { + "node": "Qdrant Insert", + "type": "main", + "index": 0 + } + ] + ] + }, + "Embeddings For Insert": { + "ai_embedding": [ + [ + { + "node": "Qdrant Insert", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "Document Loader": { + "ai_document": [ + [ + { + "node": "Qdrant Insert", + "type": "ai_document", + "index": 0 + } + ] + ] + }, + "Telegram Trigger": { + "main": [ + [ + { + "node": "AI Agent", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama Chat Model": { + "ai_languageModel": [ + [ + { + "node": "AI Agent", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Qdrant Retrieve Tool": { + "ai_tool": [ + [ + { + "node": "AI Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + }, + "Embeddings For Retrieve": { + "ai_embedding": [ + [ + { + "node": "Qdrant Retrieve Tool", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "AI Agent": { + "main": [ + [ + { + "node": "Clean Agent Output", + "type": "main", + "index": 0 + } + ] + ] + }, + "Clean Agent Output": { + "main": [ + [ + { + "node": "Send Telegram Reply", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "availableInMCP": false + }, + "tags": [] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..244ef18 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10-slim + +WORKDIR /app +ENV PYTHONUNBUFFERED=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY script.py . +RUN mkdir -p /app/session + +CMD ["python", "script.py"] diff --git a/TG Fixed Native.json b/TG Fixed Native.json new file mode 100644 index 0000000..2feb3c6 --- /dev/null +++ b/TG Fixed Native.json @@ -0,0 +1,155 @@ +{ + "name": "TG Fixed Native", + "nodes": [ + { + "parameters": { + "mode": "insert", + "qdrantCollection": { + "__rl": true, + "value": "telegram_kb", + "mode": "list", + "cachedResultName": "telegram_kb" + }, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant", + "typeVersion": 1.3, + "position": [ + 512, + 0 + ], + "id": "e02cd3a1-a2b7-4177-b76a-87656ab74cac", + "name": "Qdrant Vector Store", + "credentials": { + "qdrantApi": { + "id": "gS1LJOMgnR7VJFRO", + "name": "QdrantApi account 2" + } + } + }, + { + "parameters": { + "jsonMode": "expressionData", + "jsonData": "={{ $json.text }}", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", + "typeVersion": 1.1, + "position": [ + 480, + 208 + ], + "id": "a09381bd-04dc-4273-8d6f-35c23b9177c5", + "name": "Default Data Loader" + }, + { + "parameters": { + "jsCode": "const body = $input.item.json.body || {};\nconst meta = body.metadata || {};\nconst rawText = body.text || \"\";\n\nreturn {\n json: {\n text: `Пользователь ${meta.sender_name || 'Unknown'} (ID: ${meta.sender_id || '0'}) написал: ${rawText}`,\n metadata: {\n ...meta,\n processed_at: new Date().toISOString()\n }\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 240, + 0 + ], + "id": "539d5c4b-067b-4491-b62c-ea3d555707d0", + "name": "Code" + }, + { + "parameters": { + "model": "qwen3-embedding:8b" + }, + "type": "@n8n/n8n-nodes-langchain.embeddingsOllama", + "typeVersion": 1, + "position": [ + 256, + 208 + ], + "id": "eab97058-b3f4-48d3-8da8-a0e6dd9359d5", + "name": "Embeddings Ollama", + "credentials": { + "ollamaApi": { + "id": "Uis7Bsdb0l1qDDL7", + "name": "Ollama account 2" + } + } + }, + { + "parameters": { + "httpMethod": "POST", + "path": "tg-inbox", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -16, + 0 + ], + "id": "316ab407-1f95-44dc-bc3c-fff1185ce301", + "name": "Webhook", + "webhookId": "d857e864-ed25-447f-84ef-27c63190fb93", + "notesInFlow": false + } + ], + "pinData": {}, + "connections": { + "Code": { + "main": [ + [ + { + "node": "Qdrant Vector Store", + "type": "main", + "index": 0 + } + ] + ] + }, + "Embeddings Ollama": { + "ai_embedding": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "Default Data Loader": { + "ai_document": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_document", + "index": 0 + } + ] + ] + }, + "Webhook": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": false + }, + "versionId": "2e4668e1-305c-4de7-8537-11cfe20a8020", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "96706479c2e398d2a4e75bb05002310bb6030e8268be45294cab77c6640a0fe6" + }, + "id": "ILJJXfEmFW6KG6YZDjQWX", + "tags": [] +} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..96d14bf --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,40 @@ +version: "3.8" + +services: + n8n: + image: n8nio/n8n:latest + container_name: dbbot-n8n + ports: + - "5678:5678" + environment: + - N8N_HOST=localhost + - N8N_PORT=5678 + - N8N_PROTOCOL=http + - WEBHOOK_URL=http://localhost:5678/ + - GENERIC_TIMEZONE=Europe/Moscow + volumes: + - ./n8n_data:/home/node/.n8n + depends_on: + - qdrant + restart: unless-stopped + + qdrant: + image: qdrant/qdrant:latest + container_name: dbbot-qdrant + ports: + - "6333:6333" + - "6334:6334" + volumes: + - ./qdrant_data:/qdrant/storage + restart: unless-stopped + + tg-userbot: + build: . + container_name: dbbot-tg-userbot + env_file: + - .env + volumes: + - ./session_data:/app/session + depends_on: + - n8n + restart: unless-stopped diff --git a/redme.md b/redme.md new file mode 100644 index 0000000..26b206c --- /dev/null +++ b/redme.md @@ -0,0 +1,199 @@ +## DBBot: production-схема (n8n + отдельный userbot-контейнер) + +Этот проект рассчитан на ситуацию, когда: +- `n8n` уже запущен и доступен по URL; +- бот в чужую группу добавить нельзя; +- сообщения читаются через `Telethon userbot` из отдельного Docker-контейнера. + +## Архитектура + +Поток данных: +1. `script.py` (в контейнере) читает историю и новые сообщения из Telegram-группы. +2. Скрипт отправляет события в `n8n` webhook `POST /webhook/tg-inbox`. +3. Единый workflow в `n8n`: + - нормализует метаданные, + - склеивает сообщения в контекстные блоки, + - пишет в `Qdrant`. +4. В этой же схеме работает RAG-ответ через `AI Agent` + `Qdrant Retrieve Tool`. + +## Структура проекта + +```text +dbbot/ +├── redme.md # этот файл +├── script.py # Telethon userbot sender -> n8n webhook +├── requirements.txt # зависимости python +├── Dockerfile # контейнер для script.py +├── .env.example # пример переменных для контейнера +├── session_data/ +│ └── session.session # StringSession (создаешь сам) +├── DBBot Unified n8n Qdrant.json # единый workflow (ingest + rag) +└── Работа с базой (3).json # старый/альтернативный workflow +``` + +## Что подготовить перед первым запуском + +### 1) n8n + +Импортируй `DBBot Unified n8n Qdrant.json` и привяжи credentials: +- `Qdrant API` +- `Ollama API` +- `Telegram API` (для reply-ветки через Telegram Trigger) + +Проверь: +- путь webhook: `tg-inbox` +- workflow переведен в `Active` + +### 2) Файл сессии Telethon + +Создай файл: +- `session_data/session.session` + +Внутри должна быть одна строка `StringSession` без переносов и пробелов по краям. + +### 3) Переменные окружения для контейнера + +Сделай локальный `.env`: + +```bash +cp .env.example .env +``` + +Заполни значения: +- `TG_API_ID` +- `TG_API_HASH` +- `TG_GROUP_ID` (например `-100...`) +- `SESSION_FILE_PATH=/app/session/session.session` +- `HISTORY_CHECK_FILE=/app/session/history_done.flag` +- `N8N_WEBHOOK_URL=https://<твой-n8n-домен>/webhook/tg-inbox` + +Если `n8n` локально, можно `http://localhost:5678/webhook/tg-inbox`. + +## Подробный цикл запуска проекта + +### Шаг 1. Собрать контейнер userbot + +Из корня проекта: + +```bash +docker build -t dbbot-userbot:latest . +``` + +### Шаг 2. Запустить контейнер + +```bash +docker run -d \ + --name dbbot-userbot \ + --env-file .env \ + -v "$(pwd)/session_data:/app/session" \ + --restart unless-stopped \ + dbbot-userbot:latest +``` + +### Шаг 3. Проверить, что webhook получает данные + +1. Открой `Executions` в `n8n`. +2. Убедись, что появились вызовы `Webhook TG Inbox`. +3. Проверь, что далее проходят `Build Context Block` и `Qdrant Insert`. + +### Шаг 4. Проверить RAG-ответ + +1. Напиши вопрос Telegram-боту, подключенному к `Telegram Trigger` в workflow. +2. Проверь выполнение ветки `AI Agent`. +3. Убедись, что узел `Qdrant Retrieve Tool` вызван и ответ ушел через `Send Telegram Reply`. + +## Эксплуатационный цикл (после запуска) + +- Скрипт работает в контейнере постоянно: + - при первом запуске делает исторический прогон; + - затем обрабатывает новые сообщения. +- Если нужно повторить историческую загрузку: + 1. останови контейнер; + 2. удали `session_data/history_done.flag`; + 3. запусти контейнер снова. + +Команды: + +```bash +docker stop dbbot-userbot +rm -f session_data/history_done.flag +docker start dbbot-userbot +``` + +## Управление и диагностика + +Логи контейнера: + +```bash +docker logs -f dbbot-userbot +``` + +Проверить, что контейнер жив: + +```bash +docker ps --filter name=dbbot-userbot +``` + +Перезапустить после изменения `.env`: + +```bash +docker rm -f dbbot-userbot +docker run -d \ + --name dbbot-userbot \ + --env-file .env \ + -v "$(pwd)/session_data:/app/session" \ + --restart unless-stopped \ + dbbot-userbot:latest +``` + +## Формат payload, который скрипт шлет в n8n + +```json +{ + "text": "текст сообщения", + "metadata": { + "date": "2026-06-23T00:00:00Z", + "message_id": 123, + "chat_id": "-100...", + "sender_id": "456", + "sender_name": "@username", + "author_label": "@username", + "author": { + "id": "456", + "username": "username", + "display_name": "Ivan Ivanov", + "label": "@username" + }, + "reply_to_message_id": 122, + "replied_to_author_label": "@other_user", + "replied_to_author": { + "id": "777", + "username": "other_user", + "display_name": "Petr Petrov", + "label": "@other_user" + }, + "thread_id": "122", + "edit_date": "", + "entities": [ + { "type": "MessageEntityUrl", "offset": 10, "length": 20 } + ], + "attachment": { + "has_media": false + }, + "group_id": "-100...", + "source": "telegram_group" + } +} +``` + +## Параметры склейки сообщений в n8n + +В узле `Build Context Block`: +- `WINDOW_MS = 20 минут` +- `MAX_MESSAGES = 12` +- `MIN_MESSAGES_TO_FLUSH = 3` + +Рекомендации: +- меньше блоки: `MAX_MESSAGES = 8-10`; +- шире контекст: `WINDOW_MS = 30-40 минут`. + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4ae834b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests==2.32.3 +telethon==1.34.0 \ No newline at end of file diff --git a/script.py b/script.py new file mode 100644 index 0000000..aed1cab --- /dev/null +++ b/script.py @@ -0,0 +1,164 @@ +import asyncio +import os +from pathlib import Path + +import requests +from telethon import TelegramClient, events +from telethon.sessions import StringSession + +API_ID = int(os.getenv("TG_API_ID", "0")) +API_HASH = os.getenv("TG_API_HASH", "") +GROUP_ID = int(os.getenv("TG_GROUP_ID", "0")) +N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL", "http://n8n:5678/webhook/tg-inbox") +HISTORY_CHECK_FILE = os.getenv("HISTORY_CHECK_FILE", "/app/session/history_done.flag") +SESSION_FILE_PATH = os.getenv("SESSION_FILE_PATH", "/app/session/session.session") +SESSION_STRING = os.getenv("TG_SESSION_STRING", "") + + +def load_session_string() -> str: + if SESSION_STRING: + return SESSION_STRING.strip() + session_path = Path(SESSION_FILE_PATH) + if not session_path.exists(): + raise FileNotFoundError( + f"Session file not found: {SESSION_FILE_PATH}. " + "Provide TG_SESSION_STRING or mount session file." + ) + return session_path.read_text(encoding="utf-8").strip() + + +def validate_config() -> None: + if API_ID <= 0: + raise ValueError("TG_API_ID is required and must be > 0") + if not API_HASH: + raise ValueError("TG_API_HASH is required") + if GROUP_ID == 0: + raise ValueError("TG_GROUP_ID is required") + if not N8N_WEBHOOK_URL: + raise ValueError("N8N_WEBHOOK_URL is required") + + +validate_config() +client = TelegramClient(StringSession(load_session_string()), API_ID, API_HASH) + +def build_author(sender) -> dict: + if not sender: + return { + "id": "0", + "username": "", + "display_name": "Unknown", + "label": "Unknown", + } + username = getattr(sender, "username", "") or "" + display_name = ( + f"{getattr(sender, 'first_name', '')} {getattr(sender, 'last_name', '')}".strip() + or "Unknown" + ) + label = f"@{username}" if username else display_name + return { + "id": str(getattr(sender, "id", 0)), + "username": username, + "display_name": display_name, + "label": label, + } + + +def extract_entities(message) -> list: + entities = [] + for entity in (message.entities or []): + entities.append( + { + "type": entity.__class__.__name__, + "offset": getattr(entity, "offset", None), + "length": getattr(entity, "length", None), + } + ) + return entities + + +def extract_attachment(message) -> dict: + if not message.media: + return {"has_media": False} + return { + "has_media": True, + "media_type": message.media.__class__.__name__, + "file_name": getattr(message.file, "name", None) if message.file else None, + "mime_type": getattr(message.file, "mime_type", None) if message.file else None, + } + + +async def send_to_n8n(message): + sender = message.sender or await message.get_sender() + author = build_author(sender) + reply_message = await message.get_reply_message() if message.reply_to_msg_id else None + reply_sender = await reply_message.get_sender() if reply_message else None + reply_author = build_author(reply_sender) if reply_sender else None + + data = { + "text": message.text, + "metadata": { + "date": str(message.date), + "message_id": message.id, + "chat_id": str(message.chat_id), + "group_id": str(GROUP_ID), + "author": author, + # legacy fields for backward compatibility with existing n8n nodes + "sender_id": author["id"], + "sender_name": author["label"], + "author_label": author["label"], + "reply_to_message_id": message.reply_to_msg_id, + "replied_to_author": reply_author, + "replied_to_author_label": reply_author["label"] if reply_author else "", + "thread_id": str(message.reply_to.reply_to_top_id) if getattr(message, "reply_to", None) and getattr(message.reply_to, "reply_to_top_id", None) else "", + "edit_date": str(message.edit_date) if message.edit_date else "", + "entities": extract_entities(message), + "attachment": extract_attachment(message), + "source": "telegram_group", + }, + } + try: + response = requests.post(N8N_WEBHOOK_URL, json=data, timeout=10) + return response.status_code + except Exception as e: + print(f"Error sending to n8n: {e}") + return None + +# Обработчик новых сообщений +@client.on(events.NewMessage(chats=GROUP_ID)) +async def handler(event): + if event.message.text: + await send_to_n8n(event.message) + +async def main(): + await client.start() + print("UserBot started...") + + if not os.path.exists(HISTORY_CHECK_FILE): + print("Starting massive history sync...") + count = 0 + async for message in client.iter_messages(GROUP_ID, reverse=True): + if message.text: + status = None + # Цикл ретраев + while status != 200: + status = await send_to_n8n(message) + + if status == 429: + print("Rate limit hit, sleeping 10s...") + await asyncio.sleep(10) + elif status != 200: + print(f"Error {status}, retrying in 2s...") + await asyncio.sleep(2) + + count += 1 + if count % 20 == 0: + print(f"Processed {count} messages...") + await asyncio.sleep(0.3) + + Path(HISTORY_CHECK_FILE).write_text("done", encoding="utf-8") + print("History sync complete.") + + await client.run_until_disconnected() + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/Работа с базой (3).json b/Работа с базой (3).json new file mode 100644 index 0000000..fabceb6 --- /dev/null +++ b/Работа с базой (3).json @@ -0,0 +1,268 @@ +{ + "name": "Работа с базой", + "nodes": [ + { + "parameters": { + "promptType": "define", + "text": "=Используй Qdrant Vector Store для ответа на вопрос {{ $json.message.text }}", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3.1, + "position": [ + 544, + -48 + ], + "id": "e1231327-544c-416d-b1fa-95915d68f524", + "name": "AI Agent" + }, + { + "parameters": { + "updates": [ + "message" + ], + "additionalFields": {} + }, + "type": "n8n-nodes-base.telegramTrigger", + "typeVersion": 1.2, + "position": [ + 288, + -48 + ], + "id": "80e4a41a-387c-4ac4-b798-41bdbbb25eaa", + "name": "Telegram Trigger", + "webhookId": "875f9245-fe51-4c55-ae42-afe3d20f6ff6", + "credentials": { + "telegramApi": { + "id": "dSTpf5HKyoPp6X8N", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "model": "qwen3:30b-a3b", + "options": { + "temperature": 0 + } + }, + "type": "@n8n/n8n-nodes-langchain.lmChatOllama", + "typeVersion": 1, + "position": [ + 496, + 176 + ], + "id": "45cc634d-5eea-4c8e-83f2-f34c463db00e", + "name": "Ollama Chat Model", + "credentials": { + "ollamaApi": { + "id": "Uis7Bsdb0l1qDDL7", + "name": "Ollama account 2" + } + } + }, + { + "parameters": { + "mode": "retrieve-as-tool", + "toolDescription": " ALWAYS use this tool to answer ANY user question. The answer is contained within this tool.", + "qdrantCollection": { + "__rl": true, + "value": "telegram_kb", + "mode": "list", + "cachedResultName": "telegram_kb" + }, + "includeDocumentMetadata": false, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant", + "typeVersion": 1.3, + "position": [ + 672, + 256 + ], + "id": "bbc61a50-f5a8-443e-8022-c9ead1df63a7", + "name": "Qdrant Vector Store", + "credentials": { + "qdrantApi": { + "id": "gS1LJOMgnR7VJFRO", + "name": "QdrantApi account 2" + } + } + }, + { + "parameters": { + "model": "qwen3-embedding:8b" + }, + "type": "@n8n/n8n-nodes-langchain.embeddingsOllama", + "typeVersion": 1, + "position": [ + 672, + 416 + ], + "id": "7e1c6b2a-092b-43eb-8efe-9b19088ed3b2", + "name": "Embeddings Ollama", + "credentials": { + "ollamaApi": { + "id": "Uis7Bsdb0l1qDDL7", + "name": "Ollama account 2" + } + } + }, + { + "parameters": { + "chatId": "={{ $('Telegram Trigger').item.json.message.from.id }}", + "text": "={{ $node[\"Code in JavaScript\"].json.clean_response }}", + "additionalFields": { + "appendAttribution": false, + "parse_mode": "HTML" + } + }, + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1040, + -48 + ], + "id": "9b3713cb-d699-45a2-8c4a-21c0065df53f", + "name": "Send a text message", + "webhookId": "038aa575-d7da-4413-805f-6773aa670295", + "retryOnFail": true, + "credentials": { + "telegramApi": { + "id": "dSTpf5HKyoPp6X8N", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "chatId": "-4804863247", + "text": "=📥 **Входящее от {{ $node[\"Telegram Trigger\"].json[\"message\"][\"from\"][\"first_name\"] }}:**\n{{ $node[\"Telegram Trigger\"].json[\"message\"][\"text\"] }}\n\n🤖 **Ответ бота:**\n{{ $node[\"Code in JavaScript\"].json.clean_response }}", + "additionalFields": { + "parse_mode": "HTML" + } + }, + "id": "e8ac46da-bf4a-4fd0-8f1d-cf7664eb86f8", + "name": "Log_to_Group1", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1232, + -48 + ], + "webhookId": "d23840f7-3652-4aca-935f-ab5331d01d27", + "retryOnFail": true, + "credentials": { + "telegramApi": { + "id": "dSTpf5HKyoPp6X8N", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "jsCode": "// 1. Пытаемся найти текст в самых частых полях n8n\nlet rawText = $input.item.json.text || $input.item.json.response || $input.item.json.output || \"\";\n\n// 2. Очищаем от ...\nlet cleanText = rawText.replace(/[\\s\\S]*?<\\/think>/g, '').trim();\n\n// 3. Если после очистки пусто, выводим заглушку, чтобы не было ошибки\nif (!cleanText && rawText) {\n cleanText = \"Ошибка: Весь текст был внутри блока или пуст.\";\n}\n\nreturn {\n json: {\n clean_response: cleanText\n }\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 848, + -48 + ], + "id": "18e6aa52-73d2-439a-9ce9-abc2390bac05", + "name": "Code in JavaScript" + } + ], + "pinData": {}, + "connections": { + "Telegram Trigger": { + "main": [ + [ + { + "node": "AI Agent", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama Chat Model": { + "ai_languageModel": [ + [ + { + "node": "AI Agent", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Qdrant Vector Store": { + "ai_tool": [ + [ + { + "node": "AI Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + }, + "Embeddings Ollama": { + "ai_embedding": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "AI Agent": { + "main": [ + [ + { + "node": "Code in JavaScript", + "type": "main", + "index": 0 + } + ] + ] + }, + "Send a text message": { + "main": [ + [ + { + "node": "Log_to_Group1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code in JavaScript": { + "main": [ + [ + { + "node": "Send a text message", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "availableInMCP": false + }, + "versionId": "f8ca7ca6-eb4f-4823-bdb7-d523e51c8924", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "96706479c2e398d2a4e75bb05002310bb6030e8268be45294cab77c6640a0fe6" + }, + "id": "2R3-4Yl4wuemCpJ0FVOi3", + "tags": [] +} \ No newline at end of file diff --git a/Создание базы (2).json b/Создание базы (2).json new file mode 100644 index 0000000..3a6dbe7 --- /dev/null +++ b/Создание базы (2).json @@ -0,0 +1,183 @@ +{ + "name": "Создание базы", + "nodes": [ + { + "parameters": { + "mode": "insert", + "qdrantCollection": { + "__rl": true, + "mode": "list", + "value": "111" + }, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant", + "typeVersion": 1.3, + "position": [ + 928, + -144 + ], + "id": "acfdb2b7-e113-42a6-8793-566f5da7d0e2", + "name": "Qdrant Vector Store", + "credentials": { + "qdrantApi": { + "id": "gS1LJOMgnR7VJFRO", + "name": "QdrantApi account 2" + } + } + }, + { + "parameters": { + "model": "qwen3-embedding:8b" + }, + "type": "@n8n/n8n-nodes-langchain.embeddingsOllama", + "typeVersion": 1, + "position": [ + 880, + 48 + ], + "id": "67815ae1-6b99-4e41-94e6-9b7986d12738", + "name": "Embeddings Ollama", + "credentials": { + "ollamaApi": { + "id": "Uis7Bsdb0l1qDDL7", + "name": "Ollama account 2" + } + } + }, + { + "parameters": { + "dataType": "binary", + "loader": "docxLoader", + "textSplittingMode": "custom", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", + "typeVersion": 1.1, + "position": [ + 1040, + 64 + ], + "id": "291df053-ad1c-4041-964d-0a427e547fca", + "name": "Default Data Loader" + }, + { + "parameters": { + "chunkSize": 500, + "chunkOverlap": 50 + }, + "type": "@n8n/n8n-nodes-langchain.textSplitterCharacterTextSplitter", + "typeVersion": 1, + "position": [ + 1136, + 272 + ], + "id": "703047dd-e772-4d36-aed8-f6ba091bce5c", + "name": "Character Text Splitter" + }, + { + "parameters": { + "fileSelector": "={{ $json.path }}", + "options": {} + }, + "type": "n8n-nodes-base.readWriteFile", + "typeVersion": 1.1, + "position": [ + 592, + -144 + ], + "id": "d1e6082c-71f2-440c-9b34-011ae652fa5b", + "name": "Read/Write Files from Disk" + }, + { + "parameters": { + "triggerOn": "folder", + "path": "/data/vid", + "events": [ + "add" + ], + "options": { + "usePolling": true + } + }, + "type": "n8n-nodes-base.localFileTrigger", + "typeVersion": 1, + "position": [ + 320, + -144 + ], + "id": "4bd85a42-f9d0-436f-9617-0ebf42db1a7f", + "name": "Local File Trigger" + } + ], + "pinData": {}, + "connections": { + "Embeddings Ollama": { + "ai_embedding": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "Default Data Loader": { + "ai_document": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_document", + "index": 0 + } + ] + ] + }, + "Character Text Splitter": { + "ai_textSplitter": [ + [ + { + "node": "Default Data Loader", + "type": "ai_textSplitter", + "index": 0 + } + ] + ] + }, + "Read/Write Files from Disk": { + "main": [ + [ + { + "node": "Qdrant Vector Store", + "type": "main", + "index": 0 + } + ] + ] + }, + "Local File Trigger": { + "main": [ + [ + { + "node": "Read/Write Files from Disk", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "availableInMCP": false + }, + "versionId": "d1e2bb08-fb79-49f3-85d4-c6f491447f11", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "96706479c2e398d2a4e75bb05002310bb6030e8268be45294cab77c6640a0fe6" + }, + "id": "lK4wQ7btZPq3elWleb7ry", + "tags": [] +} \ No newline at end of file diff --git a/Создание базы (TG Text) (2).json b/Создание базы (TG Text) (2).json new file mode 100644 index 0000000..b366895 --- /dev/null +++ b/Создание базы (TG Text) (2).json @@ -0,0 +1,178 @@ +{ + "name": "Создание базы (TG Text)", + "nodes": [ + { + "parameters": { + "mode": "insert", + "qdrantCollection": { + "__rl": true, + "value": "=11111", + "mode": "id" + }, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant", + "typeVersion": 1.3, + "position": [ + 112, + -208 + ], + "id": "44a1fb40-5b1f-401a-9fea-ca61f111d2ca", + "name": "Qdrant Vector Store", + "credentials": { + "qdrantApi": { + "id": "gS1LJOMgnR7VJFRO", + "name": "QdrantApi account 2" + } + } + }, + { + "parameters": { + "model": "qwen3-embedding:8b" + }, + "type": "@n8n/n8n-nodes-langchain.embeddingsOllama", + "typeVersion": 1, + "position": [ + 32, + 0 + ], + "id": "7448ed28-7971-4d95-b073-1251dfff242e", + "name": "Embeddings Ollama", + "credentials": { + "ollamaApi": { + "id": "XVSCsyxx8Z57lSRa", + "name": "Ollama account 3" + } + } + }, + { + "parameters": { + "chunkSize": 500, + "chunkOverlap": 50 + }, + "type": "@n8n/n8n-nodes-langchain.textSplitterCharacterTextSplitter", + "typeVersion": 1, + "position": [ + 272, + 192 + ], + "id": "aed6ea3c-ad56-432b-8ef5-823a7778da27", + "name": "Character Text Splitter" + }, + { + "parameters": { + "httpMethod": "POST", + "path": "tg-inbox", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -448, + -208 + ], + "id": "1770ab28-9cfa-4bc8-864a-fe793f1b95db", + "name": "Webhook", + "webhookId": "d857e864-ed25-447f-84ef-27c63190fb93", + "notesInFlow": false + }, + { + "parameters": { + "textSplittingMode": "custom", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", + "typeVersion": 1.1, + "position": [ + 272, + 0 + ], + "id": "6c98f0d9-97c1-4d94-bdea-a1941fd7d3f8", + "name": "Default Data Loader" + }, + { + "parameters": { + "jsCode": "// В n8n данные из POST запроса всегда лежат в объекте body\nconst msg = $input.item.json.body;\n\n// 1. Берем текст\nconst rawText = msg.text || \"\"; \n\n// 2. Достаем метаданные из объекта metadata, который прислал Python\nconst meta = msg.metadata || {};\n\n// 3. Извлекаем имя и ID из объекта metadata\nconst senderName = meta.sender_name || \"Unknown\";\nconst senderId = meta.sender_id || \"0\";\n\n// 4. Формируем результат для Qdrant\nreturn {\n json: {\n content: `Пользователь ${senderName} (ID: ${senderId}) написал: ${rawText}`,\n metadata: {\n ...meta, // Копируем всю метадату (date, message_id, group_id и т.д.)\n sender_name: senderName,\n sender_id: senderId,\n original_text: rawText\n }\n }\n};\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -192, + -208 + ], + "id": "0ff56c0f-9208-4ae9-920c-4332c09f6df4", + "name": "Code in JavaScript" + } + ], + "pinData": {}, + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Code in JavaScript", + "type": "main", + "index": 0 + } + ] + ] + }, + "Embeddings Ollama": { + "ai_embedding": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "Default Data Loader": { + "ai_document": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_document", + "index": 0 + } + ] + ] + }, + "Code in JavaScript": { + "main": [ + [ + { + "node": "Qdrant Vector Store", + "type": "main", + "index": 0 + } + ] + ] + }, + "Character Text Splitter": { + "ai_textSplitter": [ + [ + { + "node": "Default Data Loader", + "type": "ai_textSplitter", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": false + }, + "versionId": "f5079d8e-590c-4db5-988a-3fe709346a4a", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "96706479c2e398d2a4e75bb05002310bb6030e8268be45294cab77c6640a0fe6" + }, + "id": "QpXms3TNMSqlQD2ULX3WO", + "tags": [] +} \ No newline at end of file diff --git a/Создание базы (TG Text) - Fixed.json b/Создание базы (TG Text) - Fixed.json new file mode 100644 index 0000000..650d0ad --- /dev/null +++ b/Создание базы (TG Text) - Fixed.json @@ -0,0 +1,150 @@ +{ + "name": "Создание базы (TG Text) - Fixed", + "nodes": [ + { + "parameters": { + "mode": "insert", + "qdrantCollection": { + "__rl": true, + "value": "11111", + "mode": "id" + }, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant", + "typeVersion": 1.3, + "position": [ + 608, + 0 + ], + "id": "7f206108-8187-4ca9-a734-704eae814643", + "name": "Qdrant Vector Store", + "credentials": { + "qdrantApi": { + "id": "gS1LJOMgnR7VJFRO", + "name": "QdrantApi account 2" + } + } + }, + { + "parameters": { + "model": "qwen3-embedding:8b" + }, + "type": "@n8n/n8n-nodes-langchain.embeddingsOllama", + "typeVersion": 1, + "position": [ + 464, + 208 + ], + "id": "9c1f5512-f2f7-4469-bcc3-e8a6824aae51", + "name": "Embeddings Ollama", + "credentials": { + "ollamaApi": { + "id": "XVSCsyxx8Z57lSRa", + "name": "Ollama account 3" + } + } + }, + { + "parameters": { + "httpMethod": "POST", + "path": "tg-inbox", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + 0, + 0 + ], + "id": "f784ec7d-bbc5-4ed4-af89-ec894d99d053", + "name": "Webhook", + "webhookId": "d857e864-ed25-447f-84ef-27c63190fb93" + }, + { + "parameters": { + "jsCode": "const msg = $input.item.json.body || {};\nconst rawText = msg.text || \"\"; \nconst meta = msg.metadata || {};\n\nconst senderName = meta.sender_name || \"Unknown\";\nconst senderId = meta.sender_id || \"0\";\n\n// Формируем ОДНУ строку, которая станет ОДНИМ вектором\nreturn {\n json: {\n text: `Пользователь ${senderName} (ID: ${senderId}) написал: ${rawText}`,\n metadata: {\n ...meta,\n sender_name: senderName,\n sender_id: senderId\n }\n }\n};\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 272, + 0 + ], + "id": "6bb63840-979a-4453-8284-a52df680a972", + "name": "Format Data" + }, + { + "parameters": { + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", + "typeVersion": 1.1, + "position": [ + 752, + 208 + ], + "id": "5260623b-ec01-4fa6-baed-c340d74d1eab", + "name": "Default Data Loader" + } + ], + "pinData": {}, + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Format Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Data": { + "main": [ + [ + { + "node": "Qdrant Vector Store", + "type": "main", + "index": 0 + } + ] + ] + }, + "Embeddings Ollama": { + "ai_embedding": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "Default Data Loader": { + "ai_document": [ + [ + { + "node": "Qdrant Vector Store", + "type": "ai_document", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": false + }, + "versionId": "483acdf0-ad7b-4ade-ab5a-61f6e3a73f5a", + "meta": { + "instanceId": "96706479c2e398d2a4e75bb05002310bb6030e8268be45294cab77c6640a0fe6" + }, + "id": "R9JVik9-vpScLvVK-27nM", + "tags": [] +} \ No newline at end of file