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 <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 20:20:56 +03:00
commit a8d634f422
13 changed files with 1768 additions and 0 deletions

View File

@@ -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(/<think>[\\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": []
}