{
  "name": "6_Multi-Agent_4vaEvzlaMrgovhNz",
  "nodes": [
    {
      "parameters": {
        "jsCode": "// \u0410\u0414\u0410\u041f\u0422\u0415\u0420: \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u043d\u043e\u0432\u043e\u0439 \u0446\u0435\u043f\u043e\u0447\u043a\u0438\n     // \u0412\u0445\u043e\u0434: turn_analysis.merged_message, session_id, user_id\n     // \u0418\u041b\u0418 \u0441\u0442\u0430\u0440\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 body.message (\u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0439 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u0438)\n\n     const raw = $json;\n\n     // \u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u0430\u043d\u043d\u044b\u0445\n     let phone, remoteJid, text, originalText, senderName, messageId, msgTimestamp;\n\n     if (raw.turn_analysis) {\n       // \u041d\u041e\u0412\u042b\u0419 \u0424\u041e\u0420\u041c\u0410\u0422 \u0438\u0437 TurnDetector\n       phone = raw.user_id || '';\n       remoteJid = raw.session_id || '';\n       text = raw.turn_analysis.merged_message || '';\n       originalText = raw.merged_text || text;\n       senderName = raw.sender_name || '';\n       messageId = raw.buffered_messages?.[0]?.message_id || `turn_${Date.now()}`;\n       msgTimestamp = Date.now();\n     } else {\n       // \u0421\u0422\u0410\u0420\u042b\u0419 \u0424\u041e\u0420\u041c\u0410\u0422 (body.message) - \u043e\u0431\u0440\u0430\u0442\u043d\u0430\u044f \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u044c\n       const body = raw.body || {};\n       const metadata = body.metadata || {};\n       remoteJid = metadata.remoteJid || '';\n       phone = remoteJid.replace('@s.whatsapp.net', '');\n       text = body.message || '';\n       originalText = text;\n       senderName = metadata.sender || '';\n       messageId = metadata.messageId || '';\n       msgTimestamp = (metadata.timestamp || 0) * 1000 || Date.now();\n     }\n\n     // === VALIDATION ===\n     if (!text.trim() || text.trim().length < 2) {\n       return [];\n     }\n\n     if (text.length > 1000) {\n       text = text.substring(0, 1000) + '...';\n     }\n\n     // \u0417\u0430\u0449\u0438\u0442\u0430 \u043e\u0442 replay \u0430\u0442\u0430\u043a (\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0440\u0448\u0435 5 \u043c\u0438\u043d\u0443\u0442) - \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0430\n     const now = Date.now();\n     if (msgTimestamp && now - msgTimestamp > 300000) {\n       // \u041f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0434\u043b\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0430 (\u0431\u0443\u0444\u0435\u0440 \u0443\u0436\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u043b)\n       if (!$json.turn_analysis) {\n         return [];\n       }\n     }\n\n     return [{\n       json: {\n         phone,\n         remoteJid,\n         text: text.trim(),\n         originalText: originalText.trim(),\n         messageType: 'text',\n         senderName,\n         messageId,\n         timestamp: msgTimestamp,\n         client_slug: raw.client_slug || 'truffles'\n       }\n     }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -4368,
        688
      ],
      "id": "22a5bb61-5a2f-4c04-a222-08a8917d6da5",
      "name": "Parse Input"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH client AS (\n  SELECT id FROM clients WHERE name = '{{ $('Parse Input').first().json.client_slug }}'\n),\nupserted_user AS (\n  INSERT INTO users (client_id, phone, remote_jid, name, last_active_at)\n  SELECT\n    (SELECT id FROM client),\n    '{{ $('Parse Input').first().json.phone }}',\n    '{{ $('Parse Input').first().json.remoteJid }}',\n    '{{ $('Parse Input').first().json.senderName }}',\n    NOW()\n  ON CONFLICT (client_id, phone) DO UPDATE SET\n    last_active_at = NOW(),\n    name = COALESCE(NULLIF('{{ $('Parse Input').first().json.senderName }}', ''), users.name)\n  RETURNING id\n),\nexisting_conv AS (\n  SELECT id FROM conversations\n  WHERE user_id = (SELECT id FROM upserted_user)\n    AND status = 'active'\n  ORDER BY last_message_at DESC\n  LIMIT 1\n),\nnew_conv AS (\n  INSERT INTO conversations (client_id, user_id, channel, status, last_message_at)\n  SELECT\n    (SELECT id FROM client),\n    (SELECT id FROM upserted_user),\n    'whatsapp',\n    'active',\n    NOW()\n  WHERE NOT EXISTS (SELECT 1 FROM existing_conv)\n  RETURNING id\n)\nSELECT\n  (SELECT id FROM upserted_user) AS user_id,\n  COALESCE(\n    (SELECT id FROM existing_conv),\n    (SELECT id FROM new_conv)\n  ) AS conversation_id,\n  (SELECT id FROM client) AS client_id;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -2672,
        656
      ],
      "id": "3f451a65-5b58-4fe1-a0e9-4d1df86a7c93",
      "name": "Upsert User",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT role, content\n     FROM messages\n     WHERE conversation_id = '{{ $('Upsert User').item.json.conversation_id }}'\n       AND content IS NOT NULL\n       AND content != ''\n       AND LENGTH(content) < 1000\n     ORDER BY created_at DESC\n     LIMIT 10;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -2224,
        656
      ],
      "id": "d51cd10e-5881-412e-9a94-5d27fb1f0eda",
      "name": "Load History",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO messages (conversation_id, client_id, role, content)\n     VALUES\n       ('{{ $json.conversation_id }}', '{{ $json.client_id }}', 'user', '{{ $json.safe_message }}'),\n       ('{{ $json.conversation_id }}', '{{ $json.client_id }}', 'assistant', '{{ $json.safe_response }}');",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        720,
        688
      ],
      "id": "58d4e715-9929-43ea-83eb-b2bc07fa07f1",
      "name": "Save Messages",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook?url=https://n8n.truffles.kz/webhook/flow",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -4592,
        -320
      ],
      "id": "644656fb-e2e8-4b15-8bdd-d19e1dde7074",
      "name": "HTTP Request1",
      "disabled": true
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO messages (conversation_id, client_id, role, content, metadata)\n     SELECT\n       '{{ $json.conversation_id }}',\n       '{{ $json.client_id }}',\n       'user',\n       '{{ $('Parse Input').first().json.text.replace(/'/g, \"''\") }}',\n       '{\"message_id\": \"{{ $('Parse Input').first().json.messageId }}\"}'\n     WHERE NOT EXISTS (\n       SELECT 1 FROM messages\n       WHERE metadata->>'message_id' = '{{ $('Parse Input').first().json.messageId }}'\n     );",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -2448,
        656
      ],
      "id": "e8b5bc48-6bc1-4146-b4f7-adbb0219efcd",
      "name": "Save User Message",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "builtInTools": {},
        "options": {
          "temperature": 0
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        -3248,
        688
      ],
      "id": "bb864994-88b2-4b95-a0fe-64371bfcd49d",
      "name": "OpenAI Chat Model",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n       \"type\": \"object\",\n       \"properties\": {\n         \"intent\": {\n           \"type\": \"string\",\n           \"enum\": [\"greeting\", \"pricing\", \"what_is_this\", \"how_to_start\", \"availability\", \"order\", \"payment\", \"delivery\", \"details\", \"integration\", \"objection_expensive\", \"objection_later\", \"objection_doubt\", \"complaint\", \"frustration\", \"human_request\", \"thanks_positive\", \"thanks_bye\", \"out_of_domain\", \"attack\"],\n           \"description\": \"\u0422\u0438\u043f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430\"\n         },\n         \"confidence\": {\n           \"type\": \"number\",\n           \"description\": \"\u0423\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u044c 0.0-1.0\"\n         }\n       },\n       \"required\": [\"intent\", \"confidence\"]\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        -3120,
        688
      ],
      "id": "fd40fa94-ed28-4bb3-b92e-2f95b6f28d85",
      "name": "Structured Output Parser"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "5b6e6bf2-e38c-4d2d-9772-aed284b3fec2",
              "leftValue": "={{ !['out_of_domain', 'attack'].includes($json.output.intent) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -2896,
        560
      ],
      "id": "d433b9bc-de00-4acc-8146-0a2d43fce766",
      "name": "Is On Topic"
    },
    {
      "parameters": {
        "jsCode": "const prev = $('RAG Search').first().json;\n\nreturn [{\n    json: {\n        ...prev\n    }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1104,
        800
      ],
      "id": "cdc8f869-bffa-4d1d-b5f1-ad03b4e0d53a",
      "name": "Add Knowledge"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "gpt-5",
          "mode": "list",
          "cachedResultName": "gpt-5"
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        -656,
        1024
      ],
      "id": "382e3b3c-c97b-4283-97b9-a79d9b6c083d",
      "name": "OpenAI Chat Model1",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"thinking\": {\n      \"type\": \"string\",\n      \"description\": \"\u0422\u0432\u043e\u0438 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043d\u0438\u044f: \u0447\u0442\u043e \u0441\u043f\u0440\u043e\u0441\u0438\u043b \u043a\u043b\u0438\u0435\u043d\u0442, \u0435\u0441\u0442\u044c \u043b\u0438 \u043e\u0442\u0432\u0435\u0442 \u0432 \u0431\u0430\u0437\u0435, \u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0442\u044c\"\n    },\n    \"has_answer\": {\n      \"type\": \"boolean\",\n      \"description\": \"true \u0435\u0441\u043b\u0438 \u043c\u043e\u0436\u0435\u0448\u044c \u0434\u0430\u0442\u044c \u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442, false \u0435\u0441\u043b\u0438 \u043d\u0435 \u0437\u043d\u0430\u0435\u0448\u044c\"\n    },\n    \"needs_escalation\": {\n      \"type\": \"boolean\",\n      \"description\": \"true \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u0435\u043d \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 (\u0441\u043b\u043e\u0436\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441, \u0436\u0430\u043b\u043e\u0431\u0430, \u0432\u043e\u0437\u0432\u0440\u0430\u0442)\"\n    },\n    \"source\": {\n      \"type\": \"string\",\n      \"description\": \"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043e\u0442\u043a\u0443\u0434\u0430 \u0432\u0437\u044f\u043b \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e: faq.md, objections.md, cases.md, examples.md, \u0438\u043b\u0438 none \u0435\u0441\u043b\u0438 \u043d\u0435\u0442 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\"\n    },\n    \"response\": {\n      \"type\": \"string\",\n      \"description\": \"\u041e\u0442\u0432\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0443\"\n    }\n  },\n  \"required\": [\n    \"thinking\",\n    \"has_answer\",\n    \"needs_escalation\",\n    \"source\",\n    \"response\"\n  ]\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        -528,
        1024
      ],
      "id": "e2decc2c-d8a7-4dc8-9718-2dd1d7244575",
      "name": "Structured Output Parser1"
    },
    {
      "parameters": {
        "jsCode": "const input = $('Parse Input').first().json;\nconst history = $('Load History').all() || [];\nconst user = $('Upsert User').first().json;\n\n// Get summary if exists\nlet summaryContext = '';\ntry {\n  const summaryData = $('Load Summary').first()?.json;\n  if (summaryData?.summary) {\n    summaryContext = '[\u041f\u0420\u0415\u0414\u042b\u0414\u0423\u0429\u0418\u0419 \u041a\u041e\u041d\u0422\u0415\u041a\u0421\u0422] ' + summaryData.summary + '\\n\\n';\n  }\n} catch (e) {}\n\n// Check escalation cooldown\nlet isInCooldown = false;\nlet escalatedAt = null;\ntry {\n  const escStatus = $('Load Escalation Status').first()?.json;\n  isInCooldown = escStatus?.is_in_cooldown || false;\n  escalatedAt = escStatus?.escalated_at;\n} catch (e) {}\n\n// \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c intent\nlet currentIntent = 'unknown';\ntry {\n  const classifyResult = $('Classify Intent').first()?.json?.output;\n  if (classifyResult?.intent) {\n    currentIntent = classifyResult.intent;\n  }\n} catch (e) {\n  const routing = input._routing || {};\n  if (routing.isGreetingByText || routing.routeReason === 'greeting_on_topic') {\n    currentIntent = 'greeting';\n  } else if (routing.isShortAnswer || routing.routeReason === 'answer_in_dialog') {\n    currentIntent = 'details';\n  }\n}\n\n// History\nconst historyLimit = summaryContext ? 5 : 10;\nconst recentHistory = history.slice(0, historyLimit);\nconst historyText = recentHistory\n  .reverse()\n  .map(m => { const r = m.json.role; if (r === 'user') return '\u041a\u043b\u0438\u0435\u043d\u0442: ' + m.json.content; if (r === 'assistant') return '\u0411\u043e\u0442: ' + m.json.content; if (r === 'manager') return '\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440: ' + m.json.content; if (r === 'system') return '[' + m.json.content + ']'; return m.json.content; })\n  .join('\\n') || '(\u043d\u043e\u0432\u044b\u0439 \u0434\u0438\u0430\u043b\u043e\u0433)';\n\n// Deadlock detection (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e, \u043d\u043e \u0434\u043b\u044f \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f)\nconst isFrustration = currentIntent === 'frustration';\nconst isHumanRequest = currentIntent === 'human_request';\nconst isDeadlock = isFrustration || isHumanRequest;\nlet deadlockReason = null;\nif (isFrustration) deadlockReason = 'frustration';\nelse if (isHumanRequest) deadlockReason = 'human_request';\n\nreturn [{\n  json: {\n    message: input.text,\n    originalMessage: input.originalText || input.text,\n    history: summaryContext + historyText,\n    conversation_id: user.conversation_id,\n    client_id: user.client_id,\n    remoteJid: input.remoteJid,\n    phone: input.phone,\n    currentIntent,\n    isInCooldown,\n    escalatedAt,\n    isDeadlock,\n    deadlockReason\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1776,
        656
      ],
      "id": "11c44d56-f39f-4514-9685-e901f862bcad",
      "name": "Build Context"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": " INSERT INTO messages (conversation_id, client_id, role, content, metadata)\n     VALUES\n       ('{{ $json.conversation_id }}', '{{ $json.client_id }}', 'user', '{{ $json.safe_message }}', '{\"fallback\": true}'),\n       ('{{ $json.conversation_id }}', '{{ $json.client_id }}', 'assistant', '{{ $json.safe_response }}', '{\"fallback\":\n     true, \"reason\": \"{{ $json.quality_reason }}\"}');",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -2896,
        368
      ],
      "id": "37ab71b5-1ee1-4c88-beae-49e5c6a0e435",
      "name": "Save Messages (Fallback)1",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "https://app.chatflow.kz/api/v1/send-text",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "REDACTED_JWT"
            },
            {
              "name": "instance_id",
              "value": "={{ $('Prepare Response').first().json.instance_id }}"
            },
            {
              "name": "jid",
              "value": "={{ $('Parse Quality').item.json.remoteJid }}"
            },
            {
              "name": "msg",
              "value": "={{ $('Quality Decision').item.json.response }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -2672,
        368
      ],
      "id": "c9dbd13f-0523-48e1-ae96-379e85dd7548",
      "name": "Send Fallback2"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT m.role, m.content FROM messages m JOIN conversations c ON m.conversation_id = c.id JOIN users u ON c.user_id = u.id WHERE u.phone = '{{ $json.phone }}' AND m.content IS NOT NULL AND m.content != '' ORDER BY m.created_at DESC LIMIT 5;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -3696,
        464
      ],
      "id": "load-history-classifier-001",
      "name": "Load History for Classifier",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const input = $('Skip Classifier?').first().json;\nconst historyRows = $('Load History for Classifier').all() || [];\n\nconst historyText = historyRows\n  .reverse()\n  .map(m => { const r = m.json.role; if (r === 'user') return '\u041a\u043b\u0438\u0435\u043d\u0442: ' + m.json.content; if (r === 'assistant') return '\u0411\u043e\u0442: ' + m.json.content; if (r === 'manager') return '\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440: ' + m.json.content; if (r === 'system') return '[' + m.json.content + ']'; return m.json.content; })\n  .join('\\n') || '(\u043d\u043e\u0432\u044b\u0439 \u0434\u0438\u0430\u043b\u043e\u0433)';\n\nreturn [{\n  json: {\n    ...input,\n    text: input.text,\n    historyContext: historyText\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -3472,
        464
      ],
      "id": "format-classifier-input-001",
      "name": "Format Classifier Input"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0434\u0438\u0430\u043b\u043e\u0433\u0430:\n{{ $json.historyContext }}\n\n\u0422\u0435\u043a\u0443\u0449\u0435\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430:\n{{ $json.text }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438 intent \u0422\u0415\u041a\u0423\u0429\u0415\u0413\u041e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0434\u0430\u043d\u0430 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430, \u043d\u043e \u043e\u0446\u0435\u043d\u0438\u0432\u0430\u0439 \u0422\u0415\u041a\u0423\u0429\u0415\u0415 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435.\n\nINTENT'\u042b:\n- greeting: \u043f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435\n- pricing: \u0432\u043e\u043f\u0440\u043e\u0441 \u043e \u0446\u0435\u043d\u0435\n- what_is_this: \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\n- how_to_start: \u043a\u0430\u043a \u043d\u0430\u0447\u0430\u0442\u044c/\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\n- availability: \u043d\u0430\u043b\u0438\u0447\u0438\u0435/\u0437\u0430\u043f\u0438\u0441\u044c\n- order: \u0437\u0430\u043a\u0430\u0437\n- payment: \u043e\u043f\u043b\u0430\u0442\u0430/Kaspi\n- delivery: \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0430\n- details: \u0443\u0442\u043e\u0447\u043d\u0435\u043d\u0438\u0435 \u0434\u0435\u0442\u0430\u043b\u0435\u0439, \u043e\u0442\u0432\u0435\u0442 \u043d\u0430 \u0432\u043e\u043f\u0440\u043e\u0441 \u0431\u043e\u0442\u0430\n- integration: \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n- objection_expensive: \u0434\u043e\u0440\u043e\u0433\u043e\n- objection_later: \u043f\u043e\u0434\u0443\u043c\u0430\u044e/\u043f\u043e\u0437\u0436\u0435\n- objection_doubt: \u0441\u043e\u043c\u043d\u0435\u043d\u0438\u044f, \u0441\u043a\u0435\u043f\u0442\u0438\u0446\u0438\u0437\u043c (\u0432\u044b \u0442\u043e\u0447\u043d\u043e \u0443\u043c\u0435\u0435\u0442\u0435? \u0430 \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442? \u043d\u0435 \u0432\u0435\u0440\u044e)\n- complaint: \u0436\u0430\u043b\u043e\u0431\u0430 \u043d\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0442/\u0441\u0435\u0440\u0432\u0438\u0441 (\u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u0432\u043e\u0437\u0432\u0440\u0430\u0442)\n- frustration: \u0422\u041e\u041b\u042c\u041a\u041e \u043c\u0430\u0442, \u043e\u0441\u043a\u043e\u0440\u0431\u043b\u0435\u043d\u0438\u044f, \u044f\u0432\u043d\u0430\u044f \u0430\u0433\u0440\u0435\u0441\u0441\u0438\u044f (\u0431\u043b\u044f\u0442\u044c, \u0445\u0443\u0439\u043d\u044f, \u0438\u0434\u0438\u043e\u0442\u044b)\n- human_request: \u0445\u043e\u0447\u0435\u0442 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430/\u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430\n- thanks_positive: \u0431\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u043d\u043e\u0441\u0442\u044c\n- thanks_bye: \u043f\u0440\u043e\u0449\u0430\u043d\u0438\u0435\n- out_of_domain: \u0441\u043e\u0432\u0441\u0435\u043c \u043d\u0435 \u043f\u043e \u0442\u0435\u043c\u0435 (\u043f\u043e\u0433\u043e\u0434\u0430, \u0440\u0435\u0446\u0435\u043f\u0442\u044b)\n- attack: \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u0432\u0437\u043b\u043e\u043c\u0430\n\n\u0412\u0410\u0416\u041d\u041e:\n1. frustration = \u0422\u041e\u041b\u042c\u041a\u041e \u041c\u0410\u0422 \u0418 \u041e\u0421\u041a\u041e\u0420\u0411\u041b\u0415\u041d\u0418\u042f. \u0411\u0435\u0437 \u043c\u0430\u0442\u0430 = \u041d\u0415 frustration\n2. \u0421\u043e\u043c\u043d\u0435\u043d\u0438\u044f \u0431\u0435\u0437 \u043c\u0430\u0442\u0430 (\u0432\u044b \u0442\u043e\u0447\u043d\u043e \u0443\u043c\u0435\u0435\u0442\u0435?) = objection_doubt, \u041d\u0415 frustration\n3. \u0421\u043a\u0435\u043f\u0442\u0438\u0446\u0438\u0437\u043c = objection_doubt\n4. \u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0435\u0433\u0430\u0442\u0438\u0432\u043d\u043e\u0439, \u043d\u043e \u0435\u0441\u043b\u0438 \u0422\u0415\u041a\u0423\u0429\u0415\u0415 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043c\u0430\u0442\u0430 \u2014 \u044d\u0442\u043e \u041d\u0415 frustration"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3,
      "position": [
        -3248,
        464
      ],
      "id": "385c47d3-57b7-431d-87b6-841dd3a703cf",
      "name": "Classify Intent",
      "retryOnFail": true,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.message }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "={{ $json.full_prompt }}"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3,
      "position": [
        -656,
        800
      ],
      "id": "d0c2e59a-bda7-4af9-a4cb-5ec61efbbc78",
      "name": "Generate Response"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.output.needs_escalation }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "id": "0a52ed57-fcfd-4cc8-a9d5-520a62eb4f23"
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.3,
      "position": [
        -304,
        800
      ],
      "id": "38848881-1ebf-4306-8a7d-2d80fc7be538",
      "name": "Check Escalation"
    },
    {
      "parameters": {
        "jsCode": "const prev = $('Build Context').first().json;\nconst generation = $('Generate Response').first().json.output;\nconst instanceId = $('Prepare Prompt').first().json.instance_id;\n\nconst escapeSQL = (str) => String(str || '').replace(/'/g, \"''\");\n\nreturn [{\n  json: {\n    conversation_id: prev.conversation_id,\n    client_id: prev.client_id,\n    remoteJid: prev.remoteJid,\n    phone: prev.phone,\n    message: prev.message,\n    response: generation.response,\n    safe_message: escapeSQL(prev.message),\n    safe_response: escapeSQL(generation.response),\n    thinking: generation.thinking,\n    has_answer: generation.has_answer,\n    needs_escalation: generation.needs_escalation,\n    source: generation.source || 'none',\n    instance_id: instanceId\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        496,
        688
      ],
      "id": "29dc5b93-4bd1-4147-8a58-1b2bbf58077e",
      "name": "Prepare Response"
    },
    {
      "parameters": {
        "jsCode": "const input = $('Parse Input').first().json;\n     const intent = $json.output?.intent || $json.intent;\n\n     let response;\n     if (intent === 'attack') {\n       response = '\u042f \u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u043d\u0442 Truffles. \u0427\u0435\u043c \u043c\u043e\u0433\u0443 \u043f\u043e\u043c\u043e\u0447\u044c \u043f\u043e \u0432\u043e\u043f\u0440\u043e\u0441\u0430\u043c AI-\u0431\u043e\u0442\u043e\u0432 \u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430?';\n     } else {\n       response = '\u042f \u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u043d\u0442 Truffles \u2014 \u043c\u044b \u0434\u0435\u043b\u0430\u0435\u043c AI-\u0431\u043e\u0442\u043e\u0432 \u0434\u043b\u044f WhatsApp, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0442 \u0432\u0430\u0448\u0438\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c 24/7. \u0411\u043e\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0434\u0430\u0436\u0435 \u043d\u043e\u0447\u044c\u044e, \u043a\u043e\u0433\u0434\u0430 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u043f\u0438\u0442 \ud83d\ude0a\\n\\n\u0415\u0441\u043b\u0438 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u0435\u0442 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430 \u2014 \u0441\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0439\u0442\u0435!'; }\n\n     return [{\n       json: {\n         phone: input.phone,\n         remoteJid: input.remoteJid,\n         response: response,\n         intent: intent\n       }\n     }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2672,
        848
      ],
      "id": "79f0bd09-f6a1-4310-a0a3-793b14fecd3c",
      "name": "Build Off-Topic Response"
    },
    {
      "parameters": {
        "url": "https://app.chatflow.kz/api/v1/send-text",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "REDACTED_JWT"
            },
            {
              "name": "instance_id",
              "value": "={{ $('Load Prompt').first().json.instance_id }}"
            },
            {
              "name": "jid",
              "value": "={{ $json.remoteJid }}"
            },
            {
              "name": "msg",
              "value": "={{ $json.response }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -2448,
        944
      ],
      "id": "9393af23-76e9-4624-81e2-588808d82b48",
      "name": "Send Off-Topic"
    },
    {
      "parameters": {
        "url": "https://app.chatflow.kz/api/v1/send-text",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "REDACTED_JWT"
            },
            {
              "name": "instance_id",
              "value": "={{ $('Prepare Response').first().json.instance_id }}"
            },
            {
              "name": "jid",
              "value": "={{ $('Prepare Response').item.json.remoteJid }}"
            },
            {
              "name": "msg",
              "value": "={{ $('Prepare Response').first().json.response }}"
            }
          ]
        },
        "options": {}
      },
      "name": "Me",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        944,
        784
      ],
      "id": "6e23777c-3f15-43a5-8f7a-4f046b7e291e"
    },
    {
      "parameters": {
        "url": "https://app.chatflow.kz/api/v1/send-text",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "REDACTED_JWT"
            },
            {
              "name": "instance_id",
              "value": "={{ $('Prepare Response').first().json.instance_id }}"
            },
            {
              "name": "jid",
              "value": "={{ $('Prepare Response').item.json.remoteJid }}"
            },
            {
              "name": "msg",
              "value": "={{ $('Prepare Response').item.json.response }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -4592,
        -96
      ],
      "id": "dbaa64be-2315-4980-9766-076ba5f3f1ff",
      "name": "Send to WhatsApp",
      "disabled": true
    },
    {
      "parameters": {
        "chatId": "1969855532",
        "text": "=\ud83d\udcf1 \u041a\u043b\u0438\u0435\u043d\u0442: {{ $('Prepare Response').item.json.phone }}\n\ud83d\udcac \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435: {{ $('Prepare Response').item.json.message }}\n\n\ud83e\udd16 \u0411\u043e\u0442: {{ $('Prepare Response').item.json.response }}\n\ud83e\udde0 Intent: {{ $('Build Context').item.json.currentIntent }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "id": "dc623a97-5934-4324-9f24-61814ac3a6c0",
      "name": "Me1",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        944,
        592
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "disabled": true
    },
    {
      "parameters": {
        "chatId": "1969855532",
        "text": "=\ud83d\udea8 \u042d\u0421\u041a\u0410\u041b\u0410\u0426\u0418\u042f\n\n\ud83d\udcf1 \u041a\u043b\u0438\u0435\u043d\u0442: {{ $('Build Context').first().json.phone }}\n\ud83d\udcac \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435: {{ $('Build Context').first().json.message }}\n\n\ud83e\udd16 \u041e\u0442\u0432\u0435\u0442 \u0431\u043e\u0442\u0430: {{ $json.output.response }}\n\ud83e\udde0 \u041f\u0440\u0438\u0447\u0438\u043d\u0430: {{ $json.output.thinking }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "id": "12b399c1-a6d0-4752-a631-1206cda1b03d",
      "name": "Me2",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        208,
        1296
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -4592,
        688
      ],
      "id": "95c425cb-c680-4eee-87c0-e124020f1118",
      "name": "Start"
    },
    {
      "parameters": {
        "jsCode": "// Intent Router \u2014 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u043e\u0442\u043e\u043a \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 intent_type\n// greeting/answer \u2192 Main Flow (skip Classifier)\n// question/statement \u2192 Classifier\n\nconst input = $json;\n\n// \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0442 TurnDetector \u0447\u0435\u0440\u0435\u0437 Start\nconst rawData = $('Start').first().json;\nconst turnAnalysis = rawData.turn_analysis || {};\nconst intentType = turnAnalysis.intent_type || 'unknown';\nconst mergedMessage = turnAnalysis.merged_message || input.text || '';\nconst contextHint = turnAnalysis.context_hint || '';\n\n// \u041d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0442\u0435\u043a\u0441\u0442 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438\nconst textLower = mergedMessage.toLowerCase().trim();\n\n// === \u042d\u0412\u0420\u0418\u0421\u0422\u0418\u041a\u0418 \u0414\u041b\u042f \u041f\u0420\u0418\u0412\u0415\u0422\u0421\u0422\u0412\u0418\u0419 ===\nconst greetingPatterns = [\n  '\u043f\u0440\u0438\u0432\u0435\u0442', '\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439', '\u0434\u043e\u0431\u0440\u044b\u0439 \u0434\u0435\u043d\u044c', '\u0434\u043e\u0431\u0440\u044b\u0439 \u0432\u0435\u0447\u0435\u0440',\n  '\u0434\u043e\u0431\u0440\u043e\u0435 \u0443\u0442\u0440\u043e', '\u0434\u043e\u0431\u0440\u044b\u0439', '\u0445\u0430\u0439', '\u0445\u0435\u043b\u043b\u043e', 'hello', 'hi',\n  '\u0441\u0430\u043b\u0435\u043c', '\u0441\u04d9\u043b\u0435\u043c', '\u0430\u0441\u0441\u0430\u043b\u0430\u043c', '\u0437\u0434\u043e\u0440\u043e\u0432\u043e', '\u043f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e',\n  '\u0434\u043e\u0431\u0440\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438', '\u0434\u0435\u043d\u044c \u0434\u043e\u0431\u0440\u044b\u0439', '\u0432\u0435\u0447\u0435\u0440 \u0434\u043e\u0431\u0440\u044b\u0439'\n];\n\nconst isGreetingByText = greetingPatterns.some(p => textLower.includes(p));\nconst isGreetingByHint = contextHint.toLowerCase().includes('\u043f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432');\n\n// === \u042d\u0412\u0420\u0418\u0421\u0422\u0418\u041a\u0418 \u0414\u041b\u042f \u041a\u041e\u0420\u041e\u0422\u041a\u0418\u0425 \u041e\u0422\u0412\u0415\u0422\u041e\u0412 ===\nconst shortAnswers = [\n  '\u0434\u0430', '\u043d\u0435\u0442', '\u043e\u043a', '\u043e\u043a\u0435\u0439', '\u0445\u043e\u0440\u043e\u0448\u043e', '\u043b\u0430\u0434\u043d\u043e', '\u043f\u043e\u043d\u044f\u043b',\n  '\u0430\u0433\u0430', '\u0443\u0433\u0443', '\u0434\u0430\u0432\u0430\u0439', '\u0433\u043e', '\u043a\u043e\u043d\u0435\u0447\u043d\u043e', '\u0441\u043e\u0433\u043b\u0430\u0441\u0435\u043d'\n];\nconst isShortAnswer = shortAnswers.includes(textLower);\n\n// === \u041e\u041f\u0420\u0415\u0414\u0415\u041b\u042f\u0415\u041c \u0420\u041e\u0423\u0422\u0418\u041d\u0413 ===\nlet skipClassifier = false;\nlet routeReason = '';\n\n// 1. \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435 \u2014 \u0432\u0441\u0435\u0433\u0434\u0430 ON-TOPIC\nif (intentType === 'greeting' || isGreetingByText || isGreetingByHint) {\n  skipClassifier = true;\n  routeReason = 'greeting_on_topic';\n}\n// 2. \u041e\u0442\u0432\u0435\u0442 \u043d\u0430 \u0432\u043e\u043f\u0440\u043e\u0441 \u0431\u043e\u0442\u0430 \u2014 \u043a\u043b\u0438\u0435\u043d\u0442 \u0443\u0436\u0435 \u0432 \u0434\u0438\u0430\u043b\u043e\u0433\u0435\nelse if (intentType === 'answer' || isShortAnswer) {\n  skipClassifier = true;\n  routeReason = 'answer_in_dialog';\n}\n// 3. \u0412\u043e\u043f\u0440\u043e\u0441 \u0438\u043b\u0438 \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u2014 \u043d\u0443\u0436\u043d\u0430 \u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\nelse {\n  skipClassifier = false;\n  routeReason = 'needs_topic_check';\n}\n\nreturn [{\n  json: {\n    ...input,\n    _routing: {\n      intentType,\n      skipClassifier,\n      routeReason,\n      isGreetingByText,\n      isGreetingByHint,\n      isShortAnswer,\n      mergedMessage\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -4144,
        592
      ],
      "id": "045ea37e-bd83-4bef-9b90-bfaa8404b665",
      "name": "Intent Router"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "f5f218a5-0a1e-412f-92a3-b3b5c7521df0",
              "leftValue": "={{ $json._routing.skipClassifier }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -3920,
        592
      ],
      "id": "aa432e5b-f3bd-482c-9c15-137f4be2b020",
      "name": "Skip Classifier?"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "deadlock-check-1",
              "leftValue": "={{ $json.isDeadlock }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -1552,
        656
      ],
      "id": "deadlock-router-001",
      "name": "Is Deadlock"
    },
    {
      "parameters": {
        "jsCode": "const ctx = $('Build Context').first().json;\nconst escapeSQL = (str) => String(str || '').replace(/'/g, \"''\");\n\nlet response;\nif (ctx.deadlockReason === 'human_request') {\n  response = '\u041f\u0435\u0440\u0435\u0434\u0430\u044e \u0432\u0430\u0448 \u0432\u043e\u043f\u0440\u043e\u0441 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0443 \u2014 \u0441\u0432\u044f\u0436\u0435\u0442\u0441\u044f \u0432 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0435 \u0432\u0440\u0435\u043c\u044f.';\n} else if (ctx.deadlockReason === 'frustration') {\n  response = '\u041f\u043e\u043d\u0438\u043c\u0430\u044e, \u0447\u0442\u043e \u0432\u044b \u0440\u0430\u0441\u0441\u0442\u0440\u043e\u0435\u043d\u044b. \u041f\u0435\u0440\u0435\u0434\u0430\u044e \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0443 \u2014 \u0441\u0432\u044f\u0436\u0435\u0442\u0441\u044f \u0441 \u0432\u0430\u043c\u0438 \u043b\u0438\u0447\u043d\u043e.';\n} else if (ctx.deadlockReason === 'post_deadlock') {\n  response = '\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0443\u0436\u0435 \u0432 \u043a\u0443\u0440\u0441\u0435 \u0438 \u0441\u0432\u044f\u0436\u0435\u0442\u0441\u044f \u0441 \u0432\u0430\u043c\u0438.';\n} else {\n  response = '\u041f\u0435\u0440\u0435\u0434\u0430\u044e \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0443 \u2014 \u0441\u0432\u044f\u0436\u0435\u0442\u0441\u044f \u0432 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0435 \u0432\u0440\u0435\u043c\u044f.';\n}\n\nreturn [{\n  json: {\n    conversation_id: ctx.conversation_id,\n    client_id: ctx.client_id,\n    remoteJid: ctx.remoteJid,\n    phone: ctx.phone,\n    message: ctx.message,\n    response: response,\n    safe_message: escapeSQL(ctx.message),\n    safe_response: escapeSQL(response),\n    isDeadlock: true,\n    deadlockReason: ctx.deadlockReason\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2672,
        1040
      ],
      "id": "deadlock-response-001",
      "name": "Deadlock Response"
    },
    {
      "parameters": {
        "jsCode": "const ctx = $('Build Context').first().json;\nconst prep = $('Prepare Response').first().json;\n\n// Format conversation for summarization\nconst history = ctx.history || '';\nconst lastMessage = ctx.message;\nconst botResponse = prep.response;\n\nconst fullConversation = history + '\\n\u041a\u043b\u0438\u0435\u043d\u0442: ' + lastMessage + '\\n\u0411\u043e\u0442: ' + botResponse;\n\nreturn [{\n  json: {\n    conversation_id: ctx.conversation_id,\n    client_id: ctx.client_id,\n    conversation: fullConversation,\n    current_intent: ctx.currentIntent\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -80,
        928
      ],
      "id": "summarize-prep-001",
      "name": "Prepare for Summary"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.conversation }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "\u0422\u044b \u2014 \u0441\u0443\u043c\u043c\u0430\u0440\u0438\u0437\u0430\u0442\u043e\u0440 \u0434\u0438\u0430\u043b\u043e\u0433\u043e\u0432. \u0421\u0436\u043c\u0438 \u0434\u0438\u0430\u043b\u043e\u0433 \u0432 \u043a\u0440\u0430\u0442\u043a\u0443\u044e \u0432\u044b\u0436\u0438\u043c\u043a\u0443.\n\n\u0427\u0422\u041e \u0412\u041a\u041b\u042e\u0427\u0410\u0422\u042c:\n- \u0427\u0442\u043e \u0445\u043e\u0442\u0435\u043b \u043a\u043b\u0438\u0435\u043d\u0442 (intent)\n- \u041a\u0430\u043a\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441 (\u0435\u0441\u043b\u0438 \u0441\u043a\u0430\u0437\u0430\u043b)\n- \u0427\u0442\u043e \u0440\u0435\u0448\u0438\u043b\u0438: interested, refused, thinking, unknown\n- \u0412\u0430\u0436\u043d\u044b\u0435 \u0434\u0435\u0442\u0430\u043b\u0438\n\n\u0427\u0422\u041e \u041d\u0415 \u0412\u041a\u041b\u042e\u0427\u0410\u0422\u042c:\n- \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u044f, \u043c\u0430\u0442, \u0436\u0430\u043b\u043e\u0431\u044b\n- \u041f\u043e\u0432\u0442\u043e\u0440\u044b\n\n\u0424\u041e\u0420\u041c\u0410\u0422 JSON:\n{\n  \"summary\": \"1-2 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f\",\n  \"key_facts\": {\n    \"intent\": \"\u0447\u0442\u043e \u0445\u043e\u0442\u0435\u043b\",\n    \"business\": \"\u0442\u0438\u043f \u0431\u0438\u0437\u043d\u0435\u0441\u0430 \u0438\u043b\u0438 unknown\",\n    \"decision\": \"interested | refused | thinking | unknown\"\n  },\n  \"last_intent\": \"\u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 intent\"\n}"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3,
      "position": [
        144,
        816
      ],
      "id": "summarize-llm-001",
      "name": "Summarize Conversation"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"summary\": {\"type\": \"string\"},\n    \"key_facts\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"intent\": {\"type\": \"string\"},\n        \"business\": {\"type\": \"string\"},\n        \"decision\": {\"type\": \"string\", \"enum\": [\"interested\", \"refused\", \"thinking\", \"unknown\"]}\n      }\n    },\n    \"last_intent\": {\"type\": \"string\"}\n  },\n  \"required\": [\"summary\", \"key_facts\", \"last_intent\"]\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        288,
        1040
      ],
      "id": "summary-parser-001",
      "name": "Summary Parser"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "gpt-4.1-mini",
          "mode": "list",
          "cachedResultName": "gpt-4.1-mini"
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        160,
        1040
      ],
      "id": "summary-model-001",
      "name": "Summary Model",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO conversation_summaries (conversation_id, client_id, summary, key_facts, last_intent)\nVALUES (\n  '{{ $('Prepare for Summary').item.json.conversation_id }}',\n  '{{ $('Prepare for Summary').item.json.client_id }}',\n  '{{ $json.output.summary.replace(/'/g, \"''\") }}',\n  '{{ JSON.stringify($json.output.key_facts).replace(/'/g, \"''\") }}'::jsonb,\n  '{{ $json.output.last_intent.replace(/'/g, \"''\") }}'\n)\nON CONFLICT (conversation_id) DO UPDATE SET\n  summary = EXCLUDED.summary,\n  key_facts = EXCLUDED.key_facts,\n  last_intent = EXCLUDED.last_intent,\n  updated_at = NOW();",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        496,
        1008
      ],
      "id": "save-summary-001",
      "name": "Save Summary",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT summary, key_facts, last_intent\nFROM conversation_summaries\nWHERE conversation_id = '{{ $('Upsert User').item.json.conversation_id }}'\nLIMIT 1;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -4592,
        1584
      ],
      "id": "load-summary-001",
      "name": "Load Summary",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  escalated_at,\n  CASE \n    WHEN escalated_at IS NULL THEN FALSE\n    WHEN escalated_at > NOW() - INTERVAL '30 minutes' THEN TRUE\n    ELSE FALSE\n  END as is_in_cooldown\nFROM conversations\nWHERE id = '{{ $('Upsert User').item.json.conversation_id }}';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -4592,
        128
      ],
      "id": "load-escalation-status-001",
      "name": "Load Escalation Status",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE conversations \nSET escalated_at = NOW()\nWHERE id = '{{ $('Build Context').first().json.conversation_id }}';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        496,
        1360
      ],
      "id": "update-escalation-001",
      "name": "Update Escalation Time",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const ctx = $('Build Context').first().json;\nconst clientSlug = $('Parse Input').first().json.client_slug || 'truffles';\n\n// 1. Get embedding from BGE-M3\nconst bgeResponse = await this.helpers.httpRequest({\n    method: 'POST',\n    url: 'http://bge-m3:80/embed',\n    body: { inputs: ctx.message },\n    json: true\n});\n\nconst vector = bgeResponse[0];\n\n// 2. Search Qdrant with client filter\nconst searchPayload = {\n    vector: vector,\n    limit: 5,\n    with_payload: true,\n    filter: {\n        must: [\n            { key: 'metadata.client_slug', match: { value: clientSlug } }\n        ]\n    }\n};\n\nconst searchResponse = await this.helpers.httpRequest({\n    method: 'POST',\n    url: 'http://qdrant:6333/collections/truffles_knowledge/points/search',\n    headers: {\n        'api-key': 'REDACTED_PASSWORD',\n        'Content-Type': 'application/json'\n    },\n    body: searchPayload,\n    json: true\n});\n\nconst results = searchResponse.result || [];\n\n// 3. Format knowledge\nconst knowledge = results\n    .map(r => r.payload?.content || '')\n    .filter(t => t)\n    .join('\\n\\n---\\n\\n') || '(\u043d\u0435\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0432 \u0431\u0430\u0437\u0435)';\n\nreturn {\n    json: {\n        ...ctx,\n        knowledge: knowledge,\n        rag_scores: results.map(r => r.score),\n        client_slug: clientSlug\n    }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1328,
        800
      ],
      "id": "7b1a9b49-1856-4f17-9c74-4a9e2fb3fa59",
      "name": "RAG Search"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO message_traces (\n    phone,\n    conversation_id,\n    message,\n    intent,\n    rag_top_score,\n    rag_top_doc,\n    rag_scores,\n    response,\n    has_answer,\n    needs_escalation\n) VALUES (\n    '{{ $('Build Context').first().json.phone }}',\n    '{{ $('Build Context').first().json.conversation_id }}',\n    '{{ $('Build Context').first().json.message.replace(/'/g, \"''\") }}',\n    '{{ $('Build Context').first().json.currentIntent }}',\n    {{ $('RAG Search').first().json.rag_scores[0] || 0 }},\n    '{{ $('RAG Search').first().json.knowledge.substring(0, 50).replace(/'/g, \"''\") }}',\n    '{{ JSON.stringify($('RAG Search').first().json.rag_scores || []) }}',\n    '{{ $json.response.replace(/'/g, \"''\") }}',\n    {{ $json.has_answer || false }},\n    {{ $json.needs_escalation || false }}\n);",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        720,
        880
      ],
      "id": "09f4ae61-aa8a-4f8e-9ab2-7c3e880b18e8",
      "name": "Save Trace",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  p.text as system_prompt,\n  c.config->>'instance_id' as instance_id\nFROM clients c\nLEFT JOIN prompts p ON p.client_id = c.id AND p.name IN ('system', 'system_prompt') AND p.is_active = true\nWHERE c.name = '{{ $('Parse Input').first().json.client_slug }}'\nLIMIT 1;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -2000,
        656
      ],
      "id": "load-prompt-001",
      "name": "Load Prompt",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// \u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u043f\u043e\u043b\u043d\u044b\u0439 prompt + instance_id\nconst ctx = $json; // \u043e\u0442 Add Knowledge\nconst loadedPrompt = $('Load Prompt').first().json;\n\nconst basePrompt = loadedPrompt.system_prompt || `\u0422\u044b \u2014 AI-\u043f\u043e\u043c\u043e\u0449\u043d\u0438\u043a. \u041e\u0442\u0432\u0435\u0447\u0430\u0439 \u043a\u0440\u0430\u0442\u043a\u043e \u0438 \u043f\u043e \u0434\u0435\u043b\u0443.`;\nconst instanceId = loadedPrompt.instance_id || '';\n\nconst fullPrompt = basePrompt + `\n\n## \u0414\u0410\u041d\u041d\u042b\u0415\n\u0418\u0441\u0442\u043e\u0440\u0438\u044f: ${ctx.history}\n\u0411\u0430\u0437\u0430 \u0437\u043d\u0430\u043d\u0438\u0439: ${ctx.knowledge}\n\nIntent: ${ctx.currentIntent}\nisInCooldown: ${ctx.isInCooldown}\n\n## \u042d\u0421\u041a\u0410\u041b\u0410\u0426\u0418\u042f (needs_escalation = true)\n\u0421\u0442\u0430\u0432\u044c needs_escalation = true \u043a\u043e\u0433\u0434\u0430:\n1. \u041c\u0410\u0422 \u0438\u043b\u0438 \u041e\u0421\u041a\u041e\u0420\u0411\u041b\u0415\u041d\u0418\u042f \u0432 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0438\n2. \u041a\u043b\u0438\u0435\u043d\u0442 \u042f\u0412\u041d\u041e \u043f\u0440\u043e\u0441\u0438\u0442 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430/\u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430\n3. \u041a\u043b\u0438\u0435\u043d\u0442 2+ \u0440\u0430\u0437\u0430 \u0432\u044b\u0440\u0430\u0436\u0430\u043b \u043d\u0435\u0434\u043e\u0432\u043e\u043b\u044c\u0441\u0442\u0432\u043e\n4. \u0421\u043b\u043e\u0436\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441 \u0432\u043d\u0435 \u0431\u0430\u0437\u044b \u0437\u043d\u0430\u043d\u0438\u0439\n\n## COOLDOWN\n\u0415\u0441\u043b\u0438 isInCooldown = true \u0418 intent \u041d\u0415 human_request:\n- \u041d\u0415 \u044d\u0441\u043a\u0430\u043b\u0438\u0440\u0443\u0439\n- \u041e\u0442\u0432\u0435\u0442\u044c: \"\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0443\u0436\u0435 \u0432 \u043a\u0443\u0440\u0441\u0435.\"\n\n## \u041f\u0420\u0410\u0412\u0418\u041b\u0410\n1. \u041a\u043e\u0440\u043e\u0442\u043a\u043e (3-4 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f)\n2. \u041d\u0415 \u0412\u042b\u0414\u0423\u041c\u042b\u0412\u0410\u0419 \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0437 \u0431\u0430\u0437\u044b \u0437\u043d\u0430\u043d\u0438\u0439\n3. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0439 SOURCE\n4. \u0415\u0441\u043b\u0438 \u043d\u0435\u0442 \u0438\u043d\u0444\u043e \u2014 \u0441\u043a\u0430\u0436\u0438 \u0447\u0435\u0441\u0442\u043d\u043e\n\n## SOURCE\n\u0423\u043a\u0430\u0436\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442: faq.md, services.md, objections.md, rules.md, \u0438\u043b\u0438 'none'\n`;\n\nreturn [{\n  json: {\n    ...ctx,\n    full_prompt: fullPrompt,\n    instance_id: instanceId\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -880,
        800
      ],
      "id": "prepare-prompt-001",
      "name": "Prepare Prompt"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  c.bot_status,\n  c.bot_muted_until,\n  CASE \n    WHEN c.bot_status = 'muted' AND c.bot_muted_until > NOW() THEN true\n    ELSE false\n  END as is_muted\nFROM conversations c\nJOIN users u ON c.user_id = u.id\nWHERE u.phone = '{{ $('Parse Input').first().json.phone }}'\n  AND c.status = 'active'\nORDER BY c.last_message_at DESC\nLIMIT 1;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -4144,
        992
      ],
      "id": "check-muted-001",
      "name": "Check Bot Muted",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "muted-check",
              "leftValue": "={{ $json.is_muted }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -3920,
        992
      ],
      "id": "is-muted-001",
      "name": "Is Bot Muted?"
    },
    {
      "parameters": {
        "jsCode": "// \u0411\u043e\u0442 \u0437\u0430\u043c\u044c\u044e\u0447\u0435\u043d - \u0432\u044b\u0445\u043e\u0434\u0438\u043c \u043c\u043e\u043b\u0447\u0430, \u043d\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u043c\nreturn [];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -3696,
        992
      ],
      "id": "silent-exit-muted-001",
      "name": "Silent Exit (Muted)"
    },
    {
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "7jGZrdbaAAvtTnQX",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "autoMapInputData",
          "value": {}
        },
        "options": {}
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.2,
      "position": [
        208,
        528
      ],
      "id": "call-escalation-001",
      "name": "Call Escalation Handler"
    },
    {
      "parameters": {
        "jsCode": "// \u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f Escalation Handler\nconst ctx = $('Build Context').first().json;\nconst genResponse = $json.output || {};\n\nreturn [{\n  json: {\n    conversation_id: ctx.conversation_id,\n    client_id: ctx.client_id,\n    phone: ctx.phone,\n    remoteJid: ctx.remoteJid,\n    message: ctx.message,\n    reason: ctx.deadlockReason || 'escalation',\n    bot_response: genResponse.response || ''\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -80,
        528
      ],
      "id": "prep-escalation-001",
      "name": "Prepare Escalation Data"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  h.id as handover_id,\n  h.conversation_id as handover_conversation_id,\n  c.telegram_topic_id,\n  cs.telegram_chat_id,\n  cs.telegram_bot_token,\n  COALESCE(u.name, u.phone, '\u041a\u043b\u0438\u0435\u043d\u0442') as client_name\nFROM conversations c\nLEFT JOIN handovers h ON h.conversation_id = c.id AND h.status = 'active'\nLEFT JOIN client_settings cs ON cs.client_id = c.client_id\nLEFT JOIN users u ON u.id = c.user_id\nWHERE c.id = '{{ $('Build Context').first().json.conversation_id }}'\nLIMIT 1;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1200,
        304
      ],
      "id": "ma-check-handover",
      "name": "Check Active Handover",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": true,
            "leftValue": ""
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-handover",
              "leftValue": "={{ $json.handover_id }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1408,
        304
      ],
      "id": "ma-has-handover",
      "name": "Handover Active?"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{ $('Check Active Handover').first().json.telegram_bot_token }}/sendMessage",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "chat_id",
              "value": "={{ $('Check Active Handover').first().json.telegram_chat_id }}"
            },
            {
              "name": "message_thread_id",
              "value": "={{ $('Check Active Handover').first().json.telegram_topic_id }}"
            },
            {
              "name": "text",
              "value": "=\ud83d\udcac {{ $('Check Active Handover').first().json.client_name }}: {{ $('Build Context').first().json.originalMessage }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1600,
        256
      ],
      "id": "ma-forward-topic",
      "name": "Forward to Topic"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE handovers \nSET messages = COALESCE(messages, '[]'::jsonb) || \n  jsonb_build_array(jsonb_build_object(\n    'from', 'client',\n    'text', '{{ $('Build Context').first().json.originalMessage }}',\n    'at', NOW()::text\n  ))\nWHERE id = '{{ $('Check Active Handover').first().json.handover_id }}';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1808,
        256
      ],
      "id": "ma-save-client-msg",
      "name": "Save Client Message",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2000,
        256
      ],
      "id": "ma-exit-handover",
      "name": "Exit (Handover Active)"
    }
  ],
  "connections": {
    "Parse Input": {
      "main": [
        [
          {
            "node": "Intent Router",
            "type": "main",
            "index": 0
          },
          {
            "node": "Check Bot Muted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert User": {
      "main": [
        [
          {
            "node": "Save User Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load History": {
      "main": [
        [
          {
            "node": "Load Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Messages": {
      "main": [
        [
          {
            "node": "Me",
            "type": "main",
            "index": 0
          },
          {
            "node": "Me1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save User Message": {
      "main": [
        [
          {
            "node": "Load History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Classify Intent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Classify Intent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Is On Topic": {
      "main": [
        [
          {
            "node": "Upsert User",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Off-Topic Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Knowledge": {
      "main": [
        [
          {
            "node": "Prepare Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Response",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser1": {
      "ai_outputParser": [
        [
          {
            "node": "Generate Response",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Build Context": {
      "main": [
        [
          {
            "node": "Check Active Handover",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Deadlock": {
      "main": [
        [
          {
            "node": "Prepare Escalation Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "RAG Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Deadlock Response": {
      "main": [
        [
          {
            "node": "Send Off-Topic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Messages (Fallback)1": {
      "main": [
        [
          {
            "node": "Send Fallback2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Intent": {
      "main": [
        [
          {
            "node": "Is On Topic",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save Messages (Fallback)1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Response": {
      "main": [
        [
          {
            "node": "Check Escalation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Escalation": {
      "main": [
        [
          {
            "node": "Prepare Escalation Data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare for Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Response": {
      "main": [
        [
          {
            "node": "Save Messages",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save Trace",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Off-Topic Response": {
      "main": [
        [
          {
            "node": "Send Off-Topic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Me2": {
      "main": [
        [
          {
            "node": "Prepare Response",
            "type": "main",
            "index": 0
          },
          {
            "node": "Update Escalation Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start": {
      "main": [
        [
          {
            "node": "Parse Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Intent Router": {
      "main": [
        [
          {
            "node": "Skip Classifier?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip Classifier?": {
      "main": [
        [
          {
            "node": "Upsert User",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Load History for Classifier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load History for Classifier": {
      "main": [
        [
          {
            "node": "Format Classifier Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Classifier Input": {
      "main": [
        [
          {
            "node": "Classify Intent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare for Summary": {
      "main": [
        [
          {
            "node": "Summarize Conversation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize Conversation": {
      "main": [
        [
          {
            "node": "Save Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summary Model": {
      "ai_languageModel": [
        [
          {
            "node": "Summarize Conversation",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Summary Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Summarize Conversation",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "RAG Search": {
      "main": [
        [
          {
            "node": "Add Knowledge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Prompt": {
      "main": [
        [
          {
            "node": "Build Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Prompt": {
      "main": [
        [
          {
            "node": "Generate Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Bot Muted": {
      "main": [
        [
          {
            "node": "Is Bot Muted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Bot Muted?": {
      "main": [
        [
          {
            "node": "Silent Exit (Muted)",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Call Escalation Handler": {
      "main": [
        [
          {
            "node": "Prepare Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Escalation Data": {
      "main": [
        [
          {
            "node": "Call Escalation Handler",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Active Handover": {
      "main": [
        [
          {
            "node": "Handover Active?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handover Active?": {
      "main": [
        [
          {
            "node": "Forward to Topic",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Is Deadlock",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Forward to Topic": {
      "main": [
        [
          {
            "node": "Save Client Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Client Message": {
      "main": [
        [
          {
            "node": "Exit (Handover Active)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  },
  "versionId": "c48e04ce-54dd-4e6e-888a-b8db0b633174",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "4vaEvzlaMrgovhNz",
  "tags": []
}