{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "4f63a844-6f93-4880-a74f-aa40787aace6",
      "name": "\ud83d\udccc Main overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4608,
        736
      ],
      "parameters": {
        "width": 516,
        "height": 620,
        "content": "## WhatsApp AI shopping bot for Bytez\n\n## How it works\n1. User sends a WhatsApp message (text, button tap, or image).\n2. GPT-5-nano classifies the intent and routes to the correct flow.\n3. Product search checks Redis cache first, then runs MongoDB Atlas vector search using OpenAI embeddings.\n4. Matched products are sent as interactive WhatsApp cards with Order Now and Virtual Try-On buttons.\n5. Order tap \u2192 AI agent fetches product, creates order in MongoDB, logs to Google Sheets, sends confirmation.\n6. Try-On tap \u2192 user sends selfie \u2192 Gemini validates one person \u2192 generates try-on image \u2192 sends result back.\n\n## Setup steps\n1. Add WhatsApp Business API credentials.\n2. Add OpenAI credentials (embeddings + GPT-5-nano).\n3. Add Google Gemini API credentials.\n4. Add MongoDB Atlas credentials and create a vector index named `ShopingBot` on the `product` collection.\n5. Add Redis credentials.\n6. Add Google Drive and Sheets credentials (service account).\n7. Import product catalog with embeddings into the MongoDB `product` collection."
      },
      "typeVersion": 1
    },
    {
      "id": "271c0200-b2e1-4830-abd0-22a1994bb404",
      "name": "\ud83d\udccc Group: Entry & Router",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5184,
        736
      ],
      "parameters": {
        "color": 7,
        "width": 580,
        "height": 460,
        "content": "## \ud83d\udce5 Entry & Message Router\nReceives all incoming WhatsApp messages. Routes button taps and image uploads to the interactive handler. Routes plain text messages to the validation pipeline.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2c5fe6ef-414d-4184-9593-d373815ac389",
      "name": "\ud83d\udccc Group: Validation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5856,
        2160
      ],
      "parameters": {
        "color": 7,
        "width": 760,
        "height": 476,
        "content": "## \u2705 Text Message Validation\nValidates incoming text for empty input, spam, XSS, and unsupported types. Invalid messages are rejected with a user-friendly error reply before entering the session pipeline."
      },
      "typeVersion": 1
    },
    {
      "id": "d6513e3e-c805-4987-afce-dd7db7a5f739",
      "name": "\ud83d\udccc Group: Session Management",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6656,
        2064
      ],
      "parameters": {
        "color": 7,
        "width": 492,
        "height": 396,
        "content": "## \ud83d\udcbe Session Management (Redis)\nLoads or creates a per-user Redis session (TTL: 1 hr). Appends user and assistant messages. Keeps the last 20 messages using a sliding window."
      },
      "typeVersion": 1
    },
    {
      "id": "44cad1fa-b280-4824-9f37-ced7823009c5",
      "name": "\ud83d\udccc Group: AI Classification",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7296,
        1856
      ],
      "parameters": {
        "color": 7,
        "width": 1496,
        "height": 828,
        "content": "## \ud83e\udde0 AI Classification & Routing\nBytezBot classifies intent using GPT-5-nano. Returns JSON for product_search or recommend, or plain text for all other queries. Routes to the product pipeline or a direct WhatsApp reply."
      },
      "typeVersion": 1
    },
    {
      "id": "590a85d0-ed0d-4afe-ac0a-e35761a58356",
      "name": "\ud83d\udccc Group: Cache & Vector Search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8864,
        1600
      ],
      "parameters": {
        "color": 7,
        "width": 2508,
        "height": 732,
        "content": "## \u26a1 Product Search (Cache + Vector)\nChecks Redis cache first (TTL: 1 hr). On a miss, runs MongoDB Atlas vector search using OpenAI embeddings. \nDownloads product images from Drive and stores results in cache."
      },
      "typeVersion": 1
    },
    {
      "id": "ffbd2a6d-6185-4a9e-b665-be92ffa2731c",
      "name": "\ud83d\udccc Group: Product Sender",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        11424,
        1664
      ],
      "parameters": {
        "color": 7,
        "width": 1148,
        "height": 460,
        "content": "## \ud83d\udce4 Product Message Sender\nLoops through each matched product. Uploads image to WhatsApp media API. \nSends an interactive message with Order Now and Virtual Try-On buttons."
      },
      "typeVersion": 1
    },
    {
      "id": "8b304693-991b-49d1-899c-b9a53c5a48c0",
      "name": "\ud83d\udccc Group: Order Flow",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6960,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 1148,
        "height": 588,
        "content": "## \ud83d\uded2 Order Flow\nTriggered when user taps Order Now. AI agent fetches product, creates an order document, saves to MongoDB and Google Sheets, then sends a confirmation message via WhatsApp."
      },
      "typeVersion": 1
    },
    {
      "id": "5b487497-fb35-46f4-8b51-b7e3677608e0",
      "name": "\ud83d\udccc Group: VTO Flow",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6752,
        -560
      ],
      "parameters": {
        "color": 7,
        "width": 872,
        "height": 360,
        "content": "## \ud83d\udc57 Virtual Try-On (VTO) Flow\nStores product ID in Redis (TTL: 10 min) and prompts the user for a selfie. Gemini validates exactly one person, generates the try-on image, sends it via WhatsApp, then clears Redis context."
      },
      "typeVersion": 1
    },
    {
      "id": "42252d26-19e1-4359-872b-94c2ce3a9ea2",
      "name": "Check Redis cache",
      "type": "n8n-nodes-base.redis",
      "position": [
        9104,
        1872
      ],
      "parameters": {
        "key": "={{ $json.cacheKey }}",
        "options": {},
        "operation": "get",
        "propertyName": "cachedProductsJson"
      },
      "typeVersion": 1
    },
    {
      "id": "493b1ea1-356f-461a-b6fa-bdfc0c39be03",
      "name": "Store products in cache",
      "type": "n8n-nodes-base.redis",
      "position": [
        11024,
        1984
      ],
      "parameters": {
        "key": "={{ $json.cacheKey }}",
        "ttl": 3600,
        "value": "={{ $json.productsJson }}",
        "expire": true,
        "operation": "set"
      },
      "typeVersion": 1
    },
    {
      "id": "cc9a90f4-db40-4498-960a-e590a43376fc",
      "name": "Save session to Redis",
      "type": "n8n-nodes-base.redis",
      "position": [
        8208,
        2176
      ],
      "parameters": {
        "key": "={{ $json.redisKey }}",
        "ttl": 3600,
        "value": "={{ $json.updatedSessionString }}",
        "expire": true,
        "operation": "set"
      },
      "typeVersion": 1
    },
    {
      "id": "dcc659e3-24d9-40ab-b06e-ea7412a0642e",
      "name": "Validate incoming message",
      "type": "n8n-nodes-base.code",
      "position": [
        5984,
        2304
      ],
      "parameters": {
        "jsCode": "// Ultra-Advanced Validation for WhatsApp Messages\nconst items = $input.all();\n\nfunction isEmptyOrInvalid(str) {\n  if (!str || typeof str !== 'string') return true;\n  const trimmed = str.trim();\n  if (trimmed.length === 0) return true;\n  const invalidOnlyPattern = /^[\\s\\u00A0\\u1680\\u2000-\\u200B\\u202F\\u205F\\u3000\\uFEFF\"'`<>,.!?;:\\-_=+(){}\\[\\]\\/\\\\|~@#$%^&*]*$/;\n  if (invalidOnlyPattern.test(trimmed)) return true;\n  return false;\n}\n\nfunction isMeaningless(str) {\n  if (!str || typeof str !== 'string') return true;\n  const trimmed = str.trim();\n  if (trimmed.length === 1) {\n    const char = trimmed;\n    if (/[a-zA-Z0-9]/.test(char)) return false;\n    if (['\u2713', '\u2714', '\u2705', '\u274c', '\ud83d\udc4d', '\ud83d\udc4e', '\u270b', '\ud83d\ude4f'].includes(char)) return false;\n    if (/[<>!?.,;:'\"=+\\-_*&^%$#@~`|\\\\\\/(){}\\[\\]]/.test(char)) return true;\n  }\n  if (trimmed.length <= 2) {\n    if (/^[^a-zA-Z0-9\\u{1F300}-\\u{1F9FF}]+$/u.test(trimmed)) return true;\n  }\n  if (/^(.)\\1{2,}$/.test(trimmed) && trimmed.length <= 10) {\n    if (!/^[a-zA-Z0-9]+$/.test(trimmed)) return false;\n    return true;\n  }\n  return false;\n}\n\nfunction sanitizeMessage(str) {\n  if (!str || typeof str !== 'string') return '';\n  let sanitized = str.trim();\n  sanitized = sanitized.replace(/\\s+/g, ' ');\n  sanitized = sanitized.replace(/[\\u200B-\\u200D\\uFEFF\\u00AD]/g, '');\n  sanitized = sanitized.replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]/g, '');\n  sanitized = sanitized.replace(/([!?.;,]){4,}/g, '$1$1$1');\n  sanitized = sanitized.replace(/^[<>{}()\\[\\]]+|[<>{}()\\[\\]]+$/g, '');\n  return sanitized.trim();\n}\n\nfunction containsSuspiciousPatterns(str) {\n  if (!str || typeof str !== 'string') return false;\n  const suspiciousPatterns = [\n    /<script[\\s\\S]*?>[\\s\\S]*?<\\/script>/gi,\n    /javascript:/gi,\n    /on\\w+\\s*=/gi,\n    /<iframe/gi,\n    /eval\\s*\\(/gi,\n    /expression\\s*\\(/gi,\n    /<object/gi,\n    /<embed/gi,\n    /vbscript:/gi,\n    /data:text\\/html/gi,\n  ];\n  return suspiciousPatterns.some(pattern => pattern.test(str));\n}\n\nfunction isSpamPattern(str) {\n  if (!str || typeof str !== 'string') return false;\n  const capitals = (str.match(/[A-Z]/g) || []).length;\n  const letters = (str.match(/[a-zA-Z]/g) || []).length;\n  if (letters > 5 && (capitals / letters) > 0.7) return true;\n  const words = str.toLowerCase().split(/\\s+/);\n  const uniqueWords = new Set(words);\n  if (words.length > 10 && uniqueWords.size < words.length * 0.3) return true;\n  return false;\n}\n\nfor (const item of items) {\n  const message = item.json.messages?.[0];\n  let isValid = false;\n  let validationError = null;\n  let userFriendlyError = null;\n  let messageContent = null;\n  let messageType = null;\n\n  if (!message) {\n    validationError = \"Message data is missing in the incoming payload\";\n    userFriendlyError = \"Sorry, we couldn't process your message. Please try again.\";\n  } else if (message.type === 'text') {\n    const rawText = message.text?.body;\n    if (!rawText) {\n      validationError = \"Text message body is null or undefined\";\n      userFriendlyError = \"Please send a valid text message.\";\n    } else if (isEmptyOrInvalid(rawText)) {\n      validationError = `Text message is empty or contains only invalid characters: \"${rawText}\"`;\n      userFriendlyError = \"Your message appears to be empty or contains only special characters. Please send a valid text message.\";\n    } else {\n      const sanitized = sanitizeMessage(rawText);\n      if (sanitized.length === 0) {\n        validationError = `Text message contains no valid content after sanitization. Original: \"${rawText}\"`;\n        userFriendlyError = \"Your message doesn't contain any valid text. Please try sending your message again.\";\n      } else if (isMeaningless(sanitized)) {\n        validationError = `Message is too short or meaningless: \"${sanitized}\"`;\n        userFriendlyError = \"Your message is too short. Please send a complete message (at least 2-3 characters).\";\n      } else if (containsSuspiciousPatterns(sanitized)) {\n        validationError = `Text message contains potentially malicious content: \"${sanitized}\"`;\n        userFriendlyError = \"Your message contains invalid content. Please send a normal text message.\";\n      } else if (isSpamPattern(sanitized)) {\n        validationError = `Text message appears to be spam: \"${sanitized}\"`;\n        userFriendlyError = \"Your message looks unusual. Please send a normal message.\";\n      } else if (sanitized.length > 4096) {\n        validationError = `Text message exceeds maximum length: ${sanitized.length} characters`;\n        userFriendlyError = \"Your message is too long. Please keep it under 4000 characters.\";\n      } else {\n        isValid = true;\n        messageContent = sanitized;\n        messageType = 'text';\n      }\n    }\n  } else if (message.type === 'interactive') {\n    const interactive = message.interactive;\n    if (!interactive) {\n      validationError = \"Interactive message data is missing\";\n      userFriendlyError = \"Button/list selection failed. Please try clicking again.\";\n    } else if (interactive.type === 'button_reply' && interactive.button_reply) {\n      const buttonId = interactive.button_reply.id;\n      const buttonTitle = interactive.button_reply.title;\n      if (!buttonId || isEmptyOrInvalid(buttonId)) {\n        validationError = `Button reply ID is empty or invalid: \"${buttonId}\"`;\n        userFriendlyError = \"Button selection failed. Please try clicking the button again.\";\n      } else if (containsSuspiciousPatterns(buttonId)) {\n        validationError = `Button reply contains malicious content: \"${buttonId}\"`;\n        userFriendlyError = \"Invalid button selection. Please contact support.\";\n      } else {\n        const sanitizedId = sanitizeMessage(buttonId);\n        if (sanitizedId.length === 0) {\n          validationError = `Button ID empty after sanitization: \"${buttonId}\"`;\n          userFriendlyError = \"Button selection failed. Please try again.\";\n        } else {\n          isValid = true;\n          messageContent = sanitizedId;\n          messageType = 'button_reply';\n          item.json.buttonTitle = buttonTitle ? sanitizeMessage(buttonTitle) : null;\n        }\n      }\n    } else if (interactive.type === 'list_reply' && interactive.list_reply) {\n      const listId = interactive.list_reply.id;\n      const listTitle = interactive.list_reply.title;\n      const listDescription = interactive.list_reply.description;\n      if (!listId || isEmptyOrInvalid(listId)) {\n        validationError = `List reply ID is empty or invalid: \"${listId}\"`;\n        userFriendlyError = \"List selection failed. Please try selecting from the list again.\";\n      } else {\n        const sanitizedId = sanitizeMessage(listId);\n        if (sanitizedId.length === 0) {\n          validationError = `List ID empty after sanitization: \"${listId}\"`;\n          userFriendlyError = \"List selection failed. Please try again.\";\n        } else {\n          isValid = true;\n          messageContent = sanitizedId;\n          messageType = 'list_reply';\n          item.json.listTitle = listTitle ? sanitizeMessage(listTitle) : null;\n          item.json.listDescription = listDescription ? sanitizeMessage(listDescription) : null;\n        }\n      }\n    } else {\n      validationError = `Unsupported interactive type: '${interactive.type || 'unknown'}'`;\n      userFriendlyError = \"Unsupported selection type. Please use the provided buttons or lists.\";\n    }\n  } else if (message.type === 'image') {\n    validationError = \"Image message received\";\n    userFriendlyError = \"\ud83d\udcf8 Image messages are not supported here. Please send a text message instead.\";\n  } else if (message.type === 'video') {\n    validationError = \"Video message received\";\n    userFriendlyError = \"\ud83c\udfa5 Video messages are not supported. Please send a text message instead.\";\n  } else if (message.type === 'audio' || message.type === 'voice') {\n    validationError = \"Audio/voice message received\";\n    userFriendlyError = \"\ud83c\udfa4 Voice messages are not supported. Please send a text message instead.\";\n  } else if (message.type === 'document') {\n    validationError = \"Document message received\";\n    userFriendlyError = \"\ud83d\udcc4 Document messages are not supported. Please send a text message instead.\";\n  } else if (message.type === 'sticker') {\n    validationError = \"Sticker message received\";\n    userFriendlyError = \"\ud83d\ude0a Stickers are not supported. Please send a text message instead.\";\n  } else {\n    validationError = `Unknown message type: '${message.type}'`;\n    userFriendlyError = \"\u274c This message type is not supported. Please send a text message.\";\n  }\n\n  item.json.validation = {\n    isValid: isValid,\n    error: validationError,\n    userFriendlyError: userFriendlyError,\n    messageType: messageType,\n    timestamp: new Date().toISOString()\n  };\n\n  if (isValid) {\n    item.json.messageText = messageContent;\n    item.json.messageType = messageType;\n    item.json.senderId = item.json.contacts?.[0]?.wa_id || null;\n    item.json.userName = item.json.contacts?.[0]?.profile?.name\n      ? sanitizeMessage(item.json.contacts[0].profile.name)\n      : \"User\";\n    item.json.messageId = message.id || null;\n    item.json.timestamp = message.timestamp || null;\n    item.json.messageMetadata = {\n      originalLength: messageType === 'text' ? (message.text?.body?.length || 0) : messageContent.length,\n      sanitizedLength: messageContent.length,\n      wordCount: messageContent.split(/\\s+/).length,\n      containsEmojis: /[\\u{1F600}-\\u{1F64F}]|[\\u{1F300}-\\u{1F5FF}]|[\\u{1F680}-\\u{1F6FF}]|[\\u{2600}-\\u{26FF}]|[\\u{2700}-\\u{27BF}]/u.test(messageContent),\n      containsNumbers: /\\d/.test(messageContent),\n      containsUrls: /(https?:\\/\\/[^\\s]+)/gi.test(messageContent),\n      isQuestion: /[?\uff1f]/.test(messageContent),\n      language: /[\\u0900-\\u097F]/.test(messageContent) ? 'hindi' :\n                /[a-zA-Z]/.test(messageContent) ? 'english' : 'unknown'\n    };\n  } else {\n    item.json.senderId = item.json.contacts?.[0]?.wa_id || null;\n    item.json.userName = item.json.contacts?.[0]?.profile?.name || \"User\";\n    item.json.messageId = message?.id || null;\n  }\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "2ec6a4c2-06eb-4ab9-b0e5-09b779ecd7b8",
      "name": "Send validation error to user",
      "type": "n8n-nodes-base.whatsApp",
      "disabled": true,
      "position": [
        6432,
        2400
      ],
      "parameters": {
        "textBody": "=\u26a0\ufe0f Validation failed: {{ $json.validation.error }}",
        "operation": "send",
        "phoneNumberId": "804155476125570",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Check if message is button or image').item.json.statuses[0].recipient_id }}"
      },
      "typeVersion": 1
    },
    {
      "id": "05cfe114-d777-4c15-9e33-495b376860db",
      "name": "Get user session from Redis",
      "type": "n8n-nodes-base.redis",
      "position": [
        6704,
        2224
      ],
      "parameters": {
        "key": "={{ $json.senderId }}",
        "options": {},
        "operation": "get",
        "propertyName": "sessionValue"
      },
      "typeVersion": 1
    },
    {
      "id": "2497bfb1-6cc9-4243-996d-25f91f7e57de",
      "name": "GPT-5-nano (intent classifier)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        7312,
        2432
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-nano",
          "cachedResultName": "gpt-5-nano"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "1117d593-3437-4e96-9e31-448b090062c9",
      "name": "Set VTO context in Redis",
      "type": "n8n-nodes-base.redis",
      "position": [
        6816,
        -432
      ],
      "parameters": {
        "key": "=vto_context_{{ $json.waId }}",
        "ttl": 600,
        "value": "={{ $json.productId }}",
        "expire": true,
        "operation": "set"
      },
      "typeVersion": 1
    },
    {
      "id": "26f4dfd0-95c5-4af1-b0d1-213991fbf947",
      "name": "GPT-4o (order agent)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        7120,
        272
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "4c10f728-35e4-44e0-93b4-356c389cc63b",
      "name": "Create order in MongoDB",
      "type": "n8n-nodes-base.mongoDbTool",
      "position": [
        7392,
        272
      ],
      "parameters": {
        "fields": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Fields', 'A JSON string representing the complete order document to be inserted.', 'string') }}",
        "options": {},
        "operation": "insert",
        "collection": "orders"
      },
      "typeVersion": 1.2
    },
    {
      "id": "3f3518a8-c956-43ba-9d06-f3c221b1ff23",
      "name": "Get product info from MongoDB",
      "type": "n8n-nodes-base.mongoDbTool",
      "position": [
        7520,
        272
      ],
      "parameters": {
        "query": "={ \"_id\": \"{{ $fromAI('productId', 'The Object ID of the product to find', 'string') }}\" }",
        "options": {},
        "collection": "product"
      },
      "typeVersion": 1.2
    },
    {
      "id": "0c5258dc-ab79-4e54-afa9-9ca3e104c42b",
      "name": "Order orchestration AI agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        7296,
        48
      ],
      "parameters": {
        "text": "=User with phone number {{ $json.waId }} (name: {{ $json.userName }}) has sent an order request for product ID {{ $json.productId }}. Please handle the entire order process.",
        "options": {
          "systemMessage": "You are an automated order processing agent for Bytez, a T-shirt store.\n\nYour job is to handle the complete order workflow end-to-end without asking the user for any additional information.\n\nYou have access to the following tools:\n- Get product info from MongoDB: fetch product details using the product ID\n- Create order in MongoDB: insert the completed order document\n- Log order to Google Sheets: append the order row\n- Session memory: maintain conversation context\n\nFollow this sequence for every order:\n\nStep 1 - Fetch product\nUse \"Get product info from MongoDB\" with the provided product ID to retrieve the product name and price.\n\nStep 2 - Create the order document\nBuild the order with these fields:\n- orderId: \"ORD\" followed by 12 random alphanumeric characters\n- productId: the provided product ID\n- productName: from the product lookup\n- userPhone: the provided waId\n- userName: the provided name\n- orderDate: current date and time in ISO 8601 format\n- status: \"pending\"\n- totalAmount: from the product lookup\n\nStep 3 - Save to MongoDB\nUse \"Create order in MongoDB\" to insert the order document.\n\nStep 4 - Log to Google Sheets\nUse \"Log order to Google Sheets\" with the same order fields.\n\nStep 5 - Send confirmation\nReply with this message:\nHello [userName]! Your order has been placed successfully. Order ID: [orderId]. Product: [productName]. We will update you once it ships!\n\nImportant rules:\n- Do not ask the user for any extra information\n- Always follow the sequence: Fetch, Create, Log, Confirm\n- If any step fails, reply with a clear, friendly error message"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "6528994a-08f3-415c-8c83-4f134a3505e8",
      "name": "Log order to Google Sheets",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        7648,
        272
      ],
      "parameters": {
        "columns": {
          "value": {
            "status": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('status', 'The current status of the order (e.g., pending).', 'string') }}",
            "orderId": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('orderId', 'The unique ID for the order.', 'string') }}",
            "userName": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('userName', 'The name of the user who placed the order.', 'string') }}",
            "orderDate": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('orderDate', 'The ISO 8601 timestamp for when the order was placed.', 'string') }}",
            "productId": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('productId', 'The ID of the product ordered.', 'string') }}",
            "userPhone": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('userPhone', 'The phone number of the user.', 'string') }}",
            "productName": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('productName', 'The name of the product ordered.', 'string') }}",
            "totalAmount": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('totalAmount', 'The total cost of the order.', 'string') }}"
          },
          "schema": [
            {
              "id": "orderId",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "orderId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "productId",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "productId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "productName",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "productName",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "userName",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "userName",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "userPhone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "userPhone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "orderDate",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "orderDate",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "totalAmount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "totalAmount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Ps5p6Z4uXwBhvwEktv9ZNAdpLvA7Si80VjTtXRE77uo/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Ps5p6Z4uXwBhvwEktv9ZNAdpLvA7Si80VjTtXRE77uo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Ps5p6Z4uXwBhvwEktv9ZNAdpLvA7Si80VjTtXRE77uo/edit?usp=drivesdk",
          "cachedResultName": "Order details"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 4.7
    },
    {
      "id": "62b212a7-9298-4135-9ec1-caf2a9fe1685",
      "name": "Send order confirmation",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        7920,
        144
      ],
      "parameters": {
        "textBody": "={{ $('Order orchestration AI agent').item.json.output }}",
        "operation": "send",
        "phoneNumberId": "804155476125570",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Parse button and user data').item.json.waId }}"
      },
      "typeVersion": 1
    },
    {
      "id": "f10c9a50-4a13-4697-91c7-fe2e71f94671",
      "name": "AI shopping agent (BytezBot)",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        7392,
        2176
      ],
      "parameters": {
        "text": "={{ $('Validate incoming message').item.json.messages[0].text.body }}",
        "options": {
          "systemMessage": "=You are BytezBot, the official AI assistant for Bytez, India's premium T-shirt store.\n\nYour only job is to help users find and buy T-shirts with high accuracy.\n\nOutput format:\n- For product_search or recommendation \u2192 pure JSON: {\"intent\":\"product_search\",\"details\":\"...\",\"confidence\":0.95,\"language\":\"english\"}\n- For everything else \u2192 plain conversational text\n\nIntents:\n1. product_search \u2014 user wants to browse or find T-shirts\n2. recommend \u2014 user wants T-shirt advice\n\nAll other queries \u2192 plain text responses (greetings, size guide, delivery, redirects, etc.)"
        },
        "promptType": "define"
      },
      "typeVersion": 2
    },
    {
      "id": "c0a06cba-4eb1-4d0d-9972-c48467fa6fc4",
      "name": "MongoDB Atlas vector search",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreMongoDBAtlas",
      "position": [
        9552,
        1984
      ],
      "parameters": {
        "mode": "load",
        "topK": 3,
        "prompt": "={{ $('Build Redis cache key from search query').item.json.intentData.details }}",
        "options": {},
        "embedding": "text_embedding",
        "mongoCollection": {
          "__rl": true,
          "mode": "list",
          "value": "product",
          "cachedResultName": "product"
        },
        "vectorIndexName": "ShopingBot"
      },
      "typeVersion": 1.3
    },
    {
      "id": "5b799fdc-3a3f-4a55-a29b-7cfb1f6a40c6",
      "name": "OpenAI embeddings",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
      "position": [
        9616,
        2208
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "17c1b430-eb95-4059-b658-7d572dac8838",
      "name": "Get product image from MongoDB",
      "type": "n8n-nodes-base.mongoDb",
      "position": [
        7968,
        720
      ],
      "parameters": {
        "query": "={\n  \"_id\" : \"{{ $('Extract image ID, waId, and product ID').item.json.productId }}\"\n}",
        "options": {},
        "collection": "product"
      },
      "typeVersion": 1.2
    },
    {
      "id": "71738df2-3d3d-489c-95c0-417f4dec9cbf",
      "name": "Resize product image to 1024px",
      "type": "n8n-nodes-base.editImage",
      "position": [
        8416,
        720
      ],
      "parameters": {
        "width": 1024,
        "height": 1024,
        "options": {},
        "operation": "resize"
      },
      "typeVersion": 1
    },
    {
      "id": "0c55a6ff-a1ca-4a97-b862-dd97acc9c974",
      "name": "Extract product image as base64",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        8640,
        720
      ],
      "parameters": {
        "options": {},
        "operation": "binaryToPropery",
        "destinationKey": "product_image"
      },
      "typeVersion": 1
    },
    {
      "id": "9f838541-b2ab-4627-bec1-9c9054c887ba",
      "name": "Resize user selfie to 1024px",
      "type": "n8n-nodes-base.editImage",
      "position": [
        8416,
        912
      ],
      "parameters": {
        "width": 1024,
        "height": 1024,
        "options": {},
        "operation": "resize",
        "dataPropertyName": "user_image"
      },
      "typeVersion": 1
    },
    {
      "id": "755658dd-37eb-492a-a2a7-c76b5dd20bd4",
      "name": "Extract user selfie as base64",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        8640,
        912
      ],
      "parameters": {
        "options": {},
        "operation": "binaryToPropery",
        "destinationKey": "user_image",
        "binaryPropertyName": "user_image"
      },
      "typeVersion": 1
    },
    {
      "id": "19574e9a-3276-405e-9bc2-7ad1a75c10cd",
      "name": "Generate VTO with Gemini API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        9536,
        1008
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent",
        "method": "POST",
        "options": {
          "timeout": 120000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={{ $json.api_payload }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "googlePalmApi"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "130fa21d-dd0e-4580-bad8-13a00956cfda",
      "name": "Delete VTO context from Redis",
      "type": "n8n-nodes-base.redis",
      "position": [
        10432,
        1008
      ],
      "parameters": {
        "key": "=vto_context_{{ $json.contacts[0].wa_id }}",
        "operation": "delete"
      },
      "typeVersion": 1
    },
    {
      "id": "3339b281-64a3-497b-9653-b84f73841e2d",
      "name": "Get VTO context from Redis",
      "type": "n8n-nodes-base.redis",
      "position": [
        7296,
        1104
      ],
      "parameters": {
        "key": "=vto_context_{{ $json.contacts[0].wa_id }}",
        "options": {},
        "operation": "get"
      },
      "typeVersion": 1
    },
    {
      "id": "8bbb9949-4661-4696-ade9-7cd702e8c0b4",
      "name": "Get WhatsApp media URL",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        7968,
        1104
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $('Extract image ID, waId, and product ID').item.json.imageId }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "whatsAppApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "8003ff1b-ac5d-4f23-8b39-d7a54c32bfcf",
      "name": "Download user selfie",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        8192,
        1104
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "user_image"
            }
          }
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "whatsAppApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "0c89b60a-8bc7-4aed-b0c6-aed6ab7991cc",
      "name": "Download product image from Drive",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        9904,
        2064
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.document.metadata.product_image_url }}"
        },
        "options": {},
        "operation": "download",
        "authentication": "serviceAccount"
      },
      "typeVersion": 3
    },
    {
      "id": "5a8a8d84-4c55-44fb-af66-234d0d10be22",
      "name": "Convert product image to base64",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        10128,
        2064
      ],
      "parameters": {
        "options": {},
        "operation": "binaryToPropery"
      },
      "typeVersion": 1
    },
    {
      "id": "937723b2-490b-426a-bb1b-fc38873546c8",
      "name": "Upload product image to WhatsApp",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        12144,
        1792
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $json.key }}/media",
        "method": "POST",
        "options": {
          "timeout": 15000
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "messaging_product",
              "value": "whatsapp"
            },
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            },
            {
              "name": "type",
              "value": "image/jpeg"
            }
          ]
        },
        "nodeCredentialType": "whatsAppApi"
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "c296c374-7c20-4346-9ec3-c12820a33ad0",
      "name": "Send product message with buttons",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        12368,
        1872
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $json.key}}/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"messaging_product\": \"whatsapp\",\n  \"recipient_type\": \"individual\",\n  \"to\": \"{{ $('Convert product image base64 to binary').first().json.to_number }}\",\n  \"type\": \"interactive\",\n  \"interactive\": {\n    \"type\": \"button\",\n    \"header\": {\n      \"type\": \"image\",\n      \"image\": {\n        \"id\": \"{{ $json.id }}\"\n      }\n    },\n    \"body\": {\n      \"text\": {{ $('Convert product image base64 to binary').first().json.bodyTextEscaped }}\n    },\n    \"footer\": {\n      \"text\": \"Powered by Bytez | Reply for assistance\"\n    },\n    \"action\": {\n      \"buttons\": [\n        {\n          \"type\": \"reply\",\n          \"reply\": {\n            \"id\": \"ORDER_{{ $('Convert product image base64 to binary').first().json.productId }}\",\n            \"title\": \"\ud83d\uded2 Order Now\"\n          }\n        },\n        {\n          \"type\": \"reply\",\n          \"reply\": {\n            \"id\": \"VTO_{{ $('Convert product image base64 to binary').first().json.productId }}\",\n            \"title\": \"\ud83d\udc57 Virtual Try-On\"\n          }\n        }\n      ]\n    }\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "whatsAppApi"
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "6a7462e4-7038-4b3f-bf0f-11bb81403d8c",
      "name": "Loop through products",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        11472,
        1872
      ],
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "typeVersion": 3
    },
    {
      "id": "c044e979-26c4-43e2-b869-4260e384feec",
      "name": "Download product image from Drive (VTO)",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        8192,
        720
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.product_image_url }}"
        },
        "options": {},
        "operation": "download",
        "authentication": "serviceAccount"
      },
      "typeVersion": 3
    },
    {
      "id": "11937477-4aec-47c3-a613-e3f59d256722",
      "name": "Ask user to send correct photo",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        7744,
        1200
      ],
      "parameters": {
        "textBody": "Please send a clear, front-facing photo of yourself to use the virtual try-on feature.",
        "operation": "send",
        "phoneNumberId": "804155476125570",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Route interactive vs image message').item.json.contacts[0].wa_id }}"
      },
      "typeVersion": 1
    },
    {
      "id": "d436178b-d025-428e-8e88-35d1e0dd3b91",
      "name": "Prompt user to upload selfie",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        7360,
        -432
      ],
      "parameters": {
        "textBody": "\u2728 Virtual try-on activated!\n\nPlease upload a clear, front-facing photo of yourself to see how this T-shirt looks on you.",
        "operation": "send",
        "phoneNumberId": "804155476125570",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Parse button and user data').item.json.waId }}"
      },
      "typeVersion": 1
    },
    {
      "id": "5e8c9f77-39c7-409a-ac8a-4f17abd77112",
      "name": "Upload VTO result to WhatsApp",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        9984,
        1008
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $json.key}}/media",
        "method": "POST",
        "options": {
          "timeout": 15000
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "messaging_product",
              "value": "whatsapp"
            },
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "VTO"
            },
            {
              "name": "type",
              "value": "image/jpeg"
            }
          ]
        },
        "nodeCredentialType": "whatsAppApi"
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "cb15a9f3-766a-4cfd-87f7-e0f123b1eef8",
      "name": "Send VTO result to user",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        10208,
        1008
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{$json.key}}/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"messaging_product\": \"whatsapp\",\n  \"recipient_type\": \"individual\",\n  \"to\": \"{{ $('Route interactive vs image message').item.json.contacts[0].wa_id }}\",\n  \"type\": \"image\",\n  \"image\": {\n    \"id\": \"{{ $json.id }}\",\n    \"caption\": \"\u2728 Your virtual try-on result!\"\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "whatsAppApi"
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "dd950826-591a-43f6-b56e-8d3fc1e33779",
      "name": "Convert Gemini response to image file",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        9760,
        1008
      ],
      "parameters": {
        "options": {},
        "operation": "toBinary",
        "sourceProperty": "candidates[0].content.parts[0].inlineData.data",
        "binaryPropertyName": "VTO"
      },
      "typeVersion": 1.1
    },
    {
      "id": "d1c50b63-deee-47c0-ad1f-af1a413ac084",
      "name": "Analyze user selfie with Gemini",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        8416,
        1104
      ],
      "parameters": {
        "text": "You are an image analysis assistant.\n\nTask:\nAnalyze the image and determine whether EXACTLY ONE real human person is visible.\n\nRules:\n- Count only real human beings.\n- Do NOT count mannequins, statues, drawings, posters, images on screens, or reflections.\n- If there is exactly 1 person \u2192 respond \"YES\"\n- If there are 0 people \u2192 respond \"NO\"\n- If there are 2 or more people \u2192 respond \"NO\"\n\nOutput format:\nRespond with ONLY one word: YES or NO\nDo not include any extra text, explanation, punctuation, or formatting.\n",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-flash",
          "cachedResultName": "models/gemini-2.5-flash"
        },
        "options": {},
        "resource": "image",
        "inputType": "binary",
        "operation": "analyze",
        "binaryPropertyName": "user_image"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6dcebd94-f7a0-4455-8c2a-0470cbbac32a",
      "name": "Send text response to user",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        8656,
        2192
      ],
      "parameters": {
        "textBody": "={{ $json.responseText }}",
        "operation": "send",
        "phoneNumberId": "804155476125570",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('WhatsApp message trigger').item.json.contacts[0].wa_id }}"
      },
      "typeVersion": 1
    },
    {
      "id": "b8e70696-1f3e-49f2-82d1-6e0da5467d0b",
      "name": "Ask user to resend valid photo",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        9312,
        1200
      ],
      "parameters": {
        "textBody": "\u26a0\ufe0f We couldn't detect exactly one person in your photo.\n\nPlease make sure:\n\u2022 Only one person is in the frame\n\u2022 You have a clear front-facing view\n\u2022 The lighting is good\n\nPlease try again.",
        "operation": "send",
        "phoneNumberId": "804155476125570",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Route interactive vs image message').item.json.contacts[0].wa_id }}"
      },
      "typeVersion": 1
    },
    {
      "id": "50f03b72-dede-45a6-b110-d1c64f542dc6",
      "name": "Notify user VTO is processing",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        9312,
        816
      ],
      "parameters": {
        "textBody": "\u23f3 Creating your virtual try-on... This may take a moment. Please wait.",
        "operation": "send",
        "phoneNumberId": "804155476125570",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Route interactive vs image message').item.json.contacts[0].wa_id }}"
      },
      "typeVersion": 1
    },
    {
      "id": "c449500b-2303-498a-9d9d-d6c7115a78b6",
      "name": "Parse button and user data",
      "type": "n8n-nodes-base.code",
      "position": [
        6016,
        80
      ],
      "parameters": {
        "jsCode": "const message = $input.first().json.entry?.[0]?.changes?.[0]?.value?.messages?.[0];\nconst contact = $input.first().json.entry?.[0]?.changes?.[0]?.value?.contacts?.[0];\n\nconst buttonId = $input.first().json.messages[0].interactive.button_reply.id;\nconst waId = $input.first().json.contacts[0].wa_id;\nconst userName = $input.first().json.contacts[0].profile.name;\n\nlet prefix = null;\nlet productId = null;\n\nif (buttonId?.startsWith(\"VTO_\")) {\n    prefix = \"VTO_\";\n    productId = buttonId.substring(4);\n} else if (buttonId?.startsWith(\"ORDER_\")) {\n    prefix = \"ORDER_\";\n    productId = buttonId.substring(6);\n}\n\n$input.first().json.prefix = prefix;\n$input.first().json.productId = productId;\n$input.first().json.waId = waId;\n$input.first().json.userName = userName;\n\nreturn $input.first();"
      },
      "typeVersion": 2
    },
    {
      "id": "1de08af7-976a-482b-8ffc-bd121a557769",
      "name": "\ud83d\udccc Group: Entry & Router1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5632,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 956,
        "height": 608,
        "content": "## \ud83d\udce5 Entry & Message Router\nReceives all incoming WhatsApp messages.\nRoutes traffic by type:\n- Button/image \u2192 interactive handler\n- Text \u2192 validation pipeline"
      },
      "typeVersion": 1
    },
    {
      "id": "78e04d36-5352-4bc3-8419-5d82c93b7449",
      "name": "WhatsApp message trigger",
      "type": "n8n-nodes-base.whatsAppTrigger",
      "position": [
        5296,
        1024
      ],
      "parameters": {
        "options": {},
        "updates": [
          "messages"
        ]
      },
      "typeVersion": 1
    },
    {
      "id": "448fed6d-99a9-47fb-9073-fc14f23046f7",
      "name": "Group: VTO Context Check",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7072,
        880
      ],
      "parameters": {
        "color": 7,
        "width": 848,
        "height": 492,
        "content": "## \ud83d\udd34 VTO Context Check\nGETs `vto_context_{waId}` from Redis. If a product ID is stored (user previously tapped Try-On), proceed to image processing. If not found, ask user to tap the Try-On button on a product first."
      },
      "typeVersion": 1
    },
    {
      "id": "0ec56bf9-33a5-44ef-bcc7-2d8154f09c85",
      "name": "Group: Image Preparation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7936,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 1056,
        "height": 780,
        "content": "## \ud83d\uddbc\ufe0f Image Download & Preparation\nFetches product image from MongoDB \u2192 downloads from Google Drive \u2192 resizes to 1024\u00d71024. In parallel, fetches the user selfie from WhatsApp media API \u2192 downloads \u2192 resizes to 1024\u00d71024. Both images are converted to base64 for Gemini."
      },
      "typeVersion": 1
    },
    {
      "id": "d42de00f-749e-4c6d-8971-c64b66c60937",
      "name": "Group: Validate Generate Send",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        9024,
        688
      ],
      "parameters": {
        "color": 7,
        "width": 1600,
        "height": 716,
        "content": "## \ud83e\udd16 Validate, Generate & Send\n\nGemini checks if exactly one real person is in the selfie. If valid, both images are sent to Gemini 2.5 Flash to generate the try-on result. Output is uploaded to WhatsApp and sent to the user. Redis VTO context is cleared after delivery. Invalid photos prompt a retry."
      },
      "typeVersion": 1
    },
    {
      "id": "8a0ef9cf-6ebc-4d52-b25f-ac3f71364385",
      "name": "Check if message is button or image",
      "type": "n8n-nodes-base.if",
      "position": [
        5584,
        1024
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "f9b79970-779c-4668-9502-02e7a04a46fb",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.messages[0].type }}",
              "rightValue": "interactive"
            },
            {
              "id": "176886ef-31af-4331-8b1c-b77a8786f5e9",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.messages[0].type }}",
              "rightValue": "image"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8180e0b1-eb66-415f-a60c-20728897d6ed",
      "name": "Route interactive vs image message",
      "type": "n8n-nodes-base.switch",
      "position": [
        5824,
        368
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Button click",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "e6a2eb2d-bd6d-4952-b88d-71286c9f6a7d",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.messages[0].type }}",
                    "rightValue": "interactive"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Image received",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "180b7e41-002f-4889-80ac-a52145b377f0",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.messages[0].type }}",
                    "rightValue": "image"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "805b97d3-2874-43ff-b2d6-8a5a5b1b284e",
      "name": "Route VTO vs order button tap",
      "type": "n8n-nodes-base.switch",
      "position": [
        6240,
        80
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "VTO",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "87d43119-f8cf-4c44-a045-13fb5a8c586d",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.prefix }}",
                    "rightValue": "VTO_"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Order",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "8522fa38-d834-476b-83c8-0f22957831c3",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.prefix }}",
                    "rightValue": "ORDER_"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "13be1098-b721-40ba-8518-76250f54f965",
      "name": "Route JSON intent vs plain text reply",
      "type": "n8n-nodes-base.if",
      "position": [
        8432,
        2176
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "89f70c2d-204c-432a-9368-cf12cd5bb720",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.responseType }}",
              "rightValue": "json"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "eabef4a8-9a42-45a3-b4bd-e30b0311d358",
      "name": "Route product search vs recommend",
      "type": "n8n-nodes-base.switch",
      "position": [
        8656,
        1936
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "product_search",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "8b9ecbc4-30cb-4cf8-9e03-f21f6583f76a",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.intentData.intent }}",
                    "rightValue": "product_search"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "recommend",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "5ba5dc9c-1a53-42e1-af9e-860993f8b1c1",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.intentData.intent }}",
                    "rightValue": "recommend"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "c1bba81c-3a96-4a10-827b-bf9e27a2c7c2",
      "name": "Is product cached in Redis?",
      "type": "n8n-nodes-base.if",
      "position": [
        9328,
        1872
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "6b6fdef2-9976-4eb7-bb53-a74dd2afae94",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.cachedProductsJson }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "87239b9b-7596-4016-aab6-3d1d30007d63",
      "name": "Pass valid messages, block invalid",
      "type": "n8n-nodes-base.if",
      "position": [
        6208,
        2304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "26de16a8-ff08-4c93-a5e4-a3be678a6209",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.validation.isValid }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "32da4b15-461c-481e-9f0e-566c7e9d20b3",
      "name": "Is VTO product ID stored in Redis?",
      "type": "n8n-nodes-base.if",
      "position": [
        7520,
        1104
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.propertyName }}",
              "operation": "isNotEmpty"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d4fc7939-1fa9-4a2e-a991-c1a61b0f1e5d",
      "name": "Gemini: confirm exactly one person",
      "type": "n8n-nodes-base.if",
      "position": [
        8640,
        1104
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c783a5a2-a8a7-4016-b0fd-ad9121e6c2c1",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.content.parts[0].text }}",
              "rightValue": "YES"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "10b63728-4769-415c-b1de-c53c892f9cad",
      "name": "Validate merged selfie before try-on",
      "type": "n8n-nodes-base.if",
      "position": [
        9088,
        912
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f650a7b6-0602-405d-9bf3-9c11329d2375",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.content.parts[0].text }}",
              "rightValue": "YES"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e8b3f8b9-44f3-4de6-a299-b960d51a9d29",
      "name": "Parse cached product JSON array",
      "type": "n8n-nodes-base.code",
      "position": [
        11248,
        1744
      ],
      "parameters": {
        "jsCode": "const productsJson = $input.first().json.cachedProductsJson;\nconst products = JSON.parse(productsJson);\nconsole.log(`\u2705 Cache HIT - Found ${products.length} products.`);\nreturn products.map(p => ({ json: p }));"
      },
      "typeVersion": 2
    },
    {
      "id": "bfce07d0-b484-43ff-a031-4cee473a67e8",
      "name": "Unpack products from cache result",
      "type": "n8n-nodes-base.code",
      "position": [
        11248,
        1984
      ],
      "parameters": {
        "jsCode": "const products = $input.first().json.products;\n\nreturn products.map(p => ({ json: p }));"
      },
      "typeVersion": 2
    },
    {
      "id": "5fd38a9a-15df-4d86-91cd-2f351891c938",
      "name": "Build Redis cache key from search query",
      "type": "n8n-nodes-base.code",
      "position": [
        8880,
        1872
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nconst userMessage = item.intentData.details;\n\n// Generate a cache key based on the specific search details\nconst cacheKey = `cache:product:${userMessage.toLowerCase().replace(/[^a-z0-9]/g, '_').substring(0, 100)}`;\n\nitem.cacheKey = cacheKey;\nreturn item;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "24ee484d-e390-4d40-a1b7-9754042e99dc",
      "name": "Extract image ID, waId, and product ID",
      "type": "n8n-nodes-base.code",
      "position": [
        7744,
        1008
      ],
      "parameters": {
        "jsCode": "const context = $('Get VTO context from Redis').first().json.propertyName;\n\nreturn {\n  json: {\n    imageId: $('Route interactive vs image message').first().json.messages[0].image.id,\n    waId: $('Route interactive vs image message').first().json.contacts[0].wa_id,\n    productId: context\n  }\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "e4d2709c-95b3-493c-9fd0-32fc5bc7374d",
      "name": "Combine products with base64 images",
      "type": "n8n-nodes-base.code",
      "position": [
        10800,
        1984
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nconst productItems = items.filter(item => item.json.document && item.json.document.metadata);\nconst imageData = items.find(item => item.json.data && Array.isArray(item.json.data));\n\nconst products = productItems.map(item => item.json.document.metadata);\n\nif (products.length === 0) {\n  return [];\n}\n\nconst base64Images = imageData && imageData.json.data ? imageData.json.data : [];\n\nconst productsWithImages = products.map((product, index) => {\n  return {\n    ...product,\n    imageBase64: base64Images[index] || null\n  };\n});\n\nconst cacheKey = $('Build Redis cache key from search query').first().json.cacheKey;\n\nreturn {\n  json: {\n    products: productsWithImages,\n    productsJson: JSON.stringify(productsWithImages),\n    cacheKey: cacheKey,\n    totalProducts: productsWithImages.length,\n    imagesAttached: base64Images.length\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "257e7891-8b5f-4718-855f-4e23794031e2",
      "name": "Load session and append user message",
      "type": "n8n-nodes-base.code",
      "position": [
        6928,
        2224
      ],
      "parameters": {
        "jsCode": "// Get the item from the previous node\nconst item = $input.first().json;\n\n// --- 1. Extract Data ---\nconst { senderId, messageText, sessionValue, userName } = item;\n\n/**\n * Creates a new session object with default values.\n */\nconst createNewSession = (userId, uName) => {\n  const currentTime = new Date().toISOString();\n  return {\n    userId: userId,\n    userName: uName,\n    messageHistory: [],\n    createdAt: currentTime,\n    lastActivity: currentTime,\n    messageCount: 0\n  };\n};\n\n// --- 2. Load or Create Session ---\nlet session;\nlet isNewSession = false;\n\nif (sessionValue) {\n  try {\n    session = JSON.parse(sessionValue);\n  } catch (e) {\n    session = createNewSession(senderId, userName);\n    isNewSession = true;\n  }\n} else {\n  session = createNewSession(senderId, userName);\n  isNewSession = true;\n}\n\n// --- 3. Update Session State ---\nsession.lastActivity = new Date().toISOString();\nsession.userName = userName;\nsession.messageCount = (session.messageCount || 0) + 1;\n\n// Add user's message to history\nsession.messageHistory.push({\n  role: 'user',\n  content: messageText,\n  timestamp: session.lastActivity\n});\n\n// Keep only the last 20 messages.\nif (session.messageHistory.length > 20) {\n  session.messageHistory = session.messageHistory.slice(-20);\n}\n\n// --- 4. Prepare Output ---\nitem.session = session;\nitem.isNewSession = isNewSession;\n\nreturn item;"
      },
      "typeVersion": 2
    },
    {
      "id": "08d0b3f8-c5c9-4274-8ee5-c0c810e65a6b",
      "name": "Append AI reply to session history",
      "type": "n8n-nodes-base.code",
      "position": [
        7984,
        2176
      ],
      "parameters": {
        "jsCode": "// This node adds the AI's final response to the session history.\nconst item = $input.first().json;\n\nlet session = item.session;\n\nif (!session) {\n  const currentTime = new Date().toISOString();\n  session = {\n    userId: item.senderId || item.contacts?.[0]?.wa_id || 'unknown_user',\n    userName: item.userName || 'User',\n    messageHistory: [],\n    createdAt: currentTime,\n    lastActivity: currentTime,\n    messageCount: 0\n  };\n  item.session = session;\n}\n\nlet assistantMessage = '';\n\nif (item.responseType === 'json') {\n  const intent = item.intentData.intent;\n  const details = item.intentData.details;\n  assistantMessage = `Understood intent: '${intent}'. Details: '${details}'.`;\n  if (intent === 'product_search') {\n      assistantMessage = `Okay, searching for: ${details}. Let me find some great options for you! \ud83d\udecd\ufe0f`;\n  }\n} else {\n  assistantMessage = item.responseText;\n}\n\nsession.messageHistory.push({\n  role: 'assistant',\n  content: assistantMessage,\n  timestamp: new Date().toISOString()\n});\n\nif (session.messageHistory.length > 20) {\n  session.messageHistory = session.messageHistory.slice(-20);\n}\n\nlet userIdentifier = item.senderId;\n\nif (!userIdentifier) {\n  userIdentifier = item.contacts?.[0]?.wa_id;\n}\n\nif (!userIdentifier) {\n  userIdentifier = session.userId;\n}\n\nitem.redisKey = userIdentifier || `error_unknown_sender_${new Date().getTime()}`;\nitem.updatedSessionString = JSON.stringify(session);\n\nreturn item;"
      },
      "typeVersion": 2
    },
    {
      "id": "ce62ffc8-679b-4eca-a604-9640b2f2889f",
      "name": "Detect JSON intent or plain text reply",
      "type": "n8n-nodes-base.code",
      "position": [
        7760,
        2176
      ],
      "parameters": {
        "jsCode": "// This node checks the AI's response, parses it if it's JSON, and sets a response type.\nconst item = $input.first().json;\nconst aiOutput = item.output;\n\n// Default to a text response\nitem.responseType = 'text';\nitem.responseText = aiOutput;\n\nif (typeof aiOutput === 'string' && aiOutput.trim().startsWith('{')) {\n  try {\n    const parsedJson = JSON.parse(aiOutput);\n    if (typeof parsedJson === 'object' && parsedJson !== null && parsedJson.intent) {\n      item.responseType = 'json';\n      item.intentData = parsedJson;\n      delete item.responseText;\n    } else {\n        item.responseText = \"I received a response, but it was not in the expected format. Please try again.\";\n    }\n  } catch (e) {\n    console.log('Could not parse AI output as JSON. Treating as text.');\n  }\n} else if (typeof aiOutput !== 'string') {\n    item.responseText = String(aiOutput);\n}\n\nreturn item;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e0470c00-81e8-4578-8d74-926907833d45",
      "name": "Build product card message body",
      "type": "n8n-nodes-base.code",
      "position": [
        11696,
        1792
      ],
      "parameters": {
        "jsCode": "const product = $input.first().json;\nconst to_number = $('WhatsApp message trigger').first().json.contacts[0].wa_id;\n\nlet senderId;\ntry {\n  senderId = $('Load session and append user message').first().json.senderId;\n} catch (error) {\n  senderId = product.senderId || product.recipient_phone || to_number || '919638137734';\n}\n\nconst productName = product.title || product.name || product.product_name || 'Product';\nconst price = product.price || product.product_price || '0';\nconst seller = product.seller_name || product.seller || 'In-House';\nconst availability = product.availability || product.stock_status || 'In Stock';\nconst productId = product._id || product.product_id || product.id || Date.now();\n\nconst formattedPrice = $input.first().json.discounted_price;\n\nlet bodyText = `\ud83d\udecd\ufe0f *${productName}*\\n\\n\ud83d\udcb0 *Price:* \u20b9${formattedPrice}\\n\ud83e\uddd1\u200d\ud83d\udcbc *Seller:* ${seller}\\n\ud83d\udce6 *Availability:* ${availability}`;\n\nconst escapedBodyText = JSON.stringify(bodyText);\n\nlet imageBase64 = product.imageBase64 || product.image_base64 || product.base64Image || null;\n\nif (imageBase64 && imageBase64.includes('base64,')) {\n  imageBase64 = imageBase64.split('base64,')[1];\n}\n\nconst returnData = {\n  recipientPhone: senderId,\n  to_number: to_number,\n  bodyText: bodyText,\n  bodyTextEscaped: escapedBodyText,\n  productId: productId,\n  productName: productName,\n  price: formattedPrice,\n  seller: seller,\n  availability: availability,\n  hasImage: !!imageBase64,\n  originalProduct: product\n};\n\nif (imageBase64) {\n  returnData.imageBase64 = imageBase64;\n  returnData.imageType = 'image/jpeg';\n  returnData.imageName = `${productName.replace(/[^a-zA-Z0-9]/g, '_')}.jpg`;\n} else {\n  returnData.imageBase64 = null;\n  returnData.note = 'No image available for this product';\n}\n\nreturn {json: returnData};"
      },
      "typeVersion": 2
    },
    {
      "id": "19a1c507-9071-4706-8e0c-262ba992797e",
      "name": "Convert product image base64 to binary",
      "type": "n8n-nodes-base.code",
      "position": [
        11920,
        1792
      ],
      "parameters": {
        "jsCode": "const inputData = $input.first().json;\nconst imageBase64 = inputData.imageBase64;\n\nif (!imageBase64) {\n  return {\n    json: {\n      ...inputData,\n      skipImageUpload: true\n    }\n  };\n}\n\nlet cleanBase64 = imageBase64;\nif (imageBase64.includes('base64,')) {\n  cleanBase64 = imageBase64.split('base64,')[1];\n}\n\nreturn {\n  json: inputData,\n  binary: {\n    data: {\n      data: cleanBase64,\n      mimeType: 'image/jpeg',\n      fileName: inputData.imageName || 'product.jpg'\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "2603eb03-479f-4b89-8476-463d04461c7c",
      "name": "Merge product, selfie, and validation result",
      "type": "n8n-nodes-base.merge",
      "position": [
        8864,
        896
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition",
        "numberInputs": 3
      },
      "typeVersion": 3
    },
    {
      "id": "6011710c-a123-4309-8c04-5e81cc6f9557",
      "name": "Combine vector search results with images",
      "type": "n8n-nodes-base.merge",
      "position": [
        10576,
        1984
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "b62fd56d-b942-414d-937a-4545d062a128",
      "name": "Aggregate all product base64 images",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        10352,
        2064
      ],
      "parameters": {
        "options": {},
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "data"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a4901b1e-a1b8-419d-9d2d-b0cbb4870799",
      "name": "Build Gemini image generation API payload",
      "type": "n8n-nodes-base.code",
      "position": [
        9312,
        1008
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nlet productImageBase64, userImageBase64, productImageUrl, userImageUrl;\n\nif (items.length > 0) {\n  const item = items[0].json;\n  productImageBase64 = item.product_image_base64 || item.product_image || item.productImage;\n  userImageBase64 = item.user_image_base64 || item.user_image || item.modelImage || item.userImage;\n  productImageUrl = item.product_image_url || item.productImageUrl;\n  userImageUrl = item.user_image_url || item.userImageUrl || item.modelImageUrl;\n}\n\nif (!productImageBase64 && !productImageUrl) {\n  throw new Error('\u274c Missing product/garment image data');\n}\nif (!userImageBase64 && !userImageUrl) {\n  throw new Error('\u274c Missing model/person image data');\n}\n\nconst cleanProductImage = productImageBase64\n  ? String(productImageBase64).replace(/^data:image\\/[^;]+;base64,/, '').trim()\n  : null;\n\nconst cleanUserImage = userImageBase64\n  ? String(userImageBase64).replace(/^data:image\\/[^;]+;base64,/, '').trim()\n  : null;\n\nconst detectMimeType = (base64String) => {\n  if (!base64String) return 'image/jpeg';\n  const match = String(base64String).match(/^data:image\\/([^;]+);base64,/);\n  if (match) return `image/${match[1]}`;\n  const signatures = {'/9j/': 'image/jpeg','iVBORw0KGgo': 'image/png','R0lGOD': 'image/gif','UklGR': 'image/webp'};\n  for (const [sig, mime] of Object.entries(signatures)) {\n    if (base64String.startsWith(sig)) return mime;\n  }\n  return 'image/jpeg';\n};\n\nconst productMimeType = detectMimeType(productImageBase64);\nconst userMimeType = detectMimeType(userImageBase64);\n\nconst ultraAggressivePrompt = `Professional virtual try-on: Apply the garment from IMAGE 1 onto the person in IMAGE 2. Replace the existing upper body clothing with the new garment naturally and realistically. Keep the face, pose, background, and lower body unchanged.`;\n\nconst imageParts = [];\n\nif (cleanProductImage) {\n  imageParts.push({inlineData: {mimeType: productMimeType, data: cleanProductImage}});\n} else if (productImageUrl) {\n  imageParts.push({fileData: {mimeType: \"image/jpeg\", fileUri: productImageUrl}});\n}\n\nif (cleanUserImage) {\n  imageParts.push({inlineData: {mimeType: userMimeType, data: cleanUserImage}});\n} else if (userImageUrl) {\n  imageParts.push({fileData: {mimeType: \"image/jpeg\", fileUri: userImageUrl}});\n}\n\nconst apiPayload = {\n  contents: [{role: \"user\", parts: [{text: ultraAggressivePrompt}, ...imageParts]}],\n  generationConfig: {temperature: 0.2, topP: 0.9, topK: 30, maxOutputTokens: 8192, candidateCount: 1},\n  safetySettings: [\n    {category: \"HARM_CATEGORY_HARASSMENT\", threshold: \"BLOCK_ONLY_HIGH\"},\n    {category: \"HARM_CATEGORY_HATE_SPEECH\", threshold: \"BLOCK_ONLY_HIGH\"},\n    {category: \"HARM_CATEGORY_SEXUALLY_EXPLICIT\", threshold: \"BLOCK_MEDIUM_AND_ABOVE\"},\n    {category: \"HARM_CATEGORY_DANGEROUS_CONTENT\", threshold: \"BLOCK_ONLY_HIGH\"}\n  ]\n};\n\nreturn [{json: {product_image_clean: cleanProductImage, user_image_clean: cleanUserImage, product_mime_type: productMimeType, user_mime_type: userMimeType, api_payload: apiPayload, timestamp: new Date().toISOString()}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c3988767-bb62-4381-8d53-658266957f9f",
      "name": "Session memory for shopping agent",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        7552,
        2528
      ],
      "parameters": {
        "sessionKey": "={{ $('WhatsApp message trigger').item.json.contacts[0].wa_id }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 50
      },
      "typeVersion": 1.3
    },
    {
      "id": "699e8b08-66b0-4c2c-94b6-4b4f641ccc33",
      "name": "Session memory for order agent",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        7248,
        272
      ],
      "parameters": {
        "sessionKey": "={{ $json.waId }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 50
      },
      "typeVersion": 1.3
    }
  ],
  "connections": {
    "Check Redis cache": {
      "main": [
        [
          {
            "node": "Is product cached in Redis?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI embeddings": {
      "ai_embedding": [
        [
          {
            "node": "MongoDB Atlas vector search",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Download user selfie": {
      "main": [
        [
          {
            "node": "Analyze user selfie with Gemini",
            "type": "main",
            "index": 0
          },
          {
            "node": "Resize user selfie to 1024px",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4o (order agent)": {
      "ai_languageModel": [
        [
          {
            "node": "Order orchestration AI agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Loop through products": {
      "main": [
        [],
        [
          {
            "node": "Build product card message body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save session to Redis": {
      "main": [
        [
          {
            "node": "Route JSON intent vs plain text reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get WhatsApp media URL": {
      "main": [
        [
          {
            "node": "Download user selfie",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create order in MongoDB": {
      "ai_tool": [
        [
          {
            "node": "Order orchestration AI agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Send VTO result to user": {
      "main": [
        [
          {
            "node": "Delete VTO context from Redis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store products in cache": {
      "main": [
        [
          {
            "node": "Unpack products from cache result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set VTO context in Redis": {
      "main": [
        [
          {
            "node": "Prompt user to upload selfie",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WhatsApp message trigger": {
      "main": [
        [
          {
            "node": "Check if message is button or image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate incoming message": {
      "main": [
        [
          {
            "node": "Pass valid messages, block invalid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get VTO context from Redis": {
      "main": [
        [
          {
            "node": "Is VTO product ID stored in Redis?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log order to Google Sheets": {
      "ai_tool": [
        [
          {
            "node": "Order orchestration AI agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Parse button and user data": {
      "main": [
        [
          {
            "node": "Route VTO vs order button tap",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get user session from Redis": {
      "main": [
        [
          {
            "node": "Load session and append user message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is product cached in Redis?": {
      "main": [
        [
          {
            "node": "Parse cached product JSON array",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "MongoDB Atlas vector search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MongoDB Atlas vector search": {
      "main": [
        [
          {
            "node": "Download product image from Drive",
            "type": "main",
            "index": 0
          },
          {
            "node": "Combine vector search results with images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI shopping agent (BytezBot)": {
      "main": [
        [
          {
            "node": "Detect JSON intent or plain text reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate VTO with Gemini API": {
      "main": [
        [
          {
            "node": "Convert Gemini response to image file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Order orchestration AI agent": {
      "main": [
        [
          {
            "node": "Send order confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resize user selfie to 1024px": {
      "main": [
        [
          {
            "node": "Extract user selfie as base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract user selfie as base64": {
      "main": [
        [
          {
            "node": "Merge product, selfie, and validation result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get product info from MongoDB": {
      "ai_tool": [
        [
          {
            "node": "Order orchestration AI agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Route VTO vs order button tap": {
      "main": [
        [
          {
            "node": "Set VTO context in Redis",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Order orchestration AI agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload VTO result to WhatsApp": {
      "main": [
        [
          {
            "node": "Send VTO result to user",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-5-nano (intent classifier)": {
      "ai_languageModel": [
        [
          {
            "node": "AI shopping agent (BytezBot)",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Get product image from MongoDB": {
      "main": [
        [
          {
            "node": "Download product image from Drive (VTO)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resize product image to 1024px": {
      "main": [
        [
          {
            "node": "Extract product image as base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Session memory for order agent": {
      "ai_memory": [
        [
          {
            "node": "Order orchestration AI agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Analyze user selfie with Gemini": {
      "main": [
        [
          {
            "node": "Gemini: confirm exactly one person",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build product card message body": {
      "main": [
        [
          {
            "node": "Convert product image base64 to binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert product image to base64": {
      "main": [
        [
          {
            "node": "Aggregate all product base64 images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract product image as base64": {
      "main": [
        [
          {
            "node": "Merge product, selfie, and validation result",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Parse cached product JSON array": {
      "main": [
        [
          {
            "node": "Loop through products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload product image to WhatsApp": {
      "main": [
        [
          {
            "node": "Send product message with buttons",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download product image from Drive": {
      "main": [
        [
          {
            "node": "Convert product image to base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route product search vs recommend": {
      "main": [
        [
          {
            "node": "Build Redis cache key from search query",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Redis cache key from search query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send product message with buttons": {
      "main": [
        [
          {
            "node": "Loop through products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Session memory for shopping agent": {
      "ai_memory": [
        [
          {
            "node": "AI shopping agent (BytezBot)",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Unpack products from cache result": {
      "main": [
        [
          {
            "node": "Loop through products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append AI reply to session history": {
      "main": [
        [
          {
            "node": "Save session to Redis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini: confirm exactly one person": {
      "main": [
        [
          {
            "node": "Merge product, selfie, and validation result",
            "type": "main",
            "index": 2
          }
        ],
        [
          {
            "node": "Merge product, selfie, and validation result",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Is VTO product ID stored in Redis?": {
      "main": [
        [
          {
            "node": "Extract image ID, waId, and product ID",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ask user to send correct photo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pass valid messages, block invalid": {
      "main": [
        [
          {
            "node": "Get user session from Redis",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send validation error to user",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route interactive vs image message": {
      "main": [
        [
          {
            "node": "Parse button and user data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get VTO context from Redis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate all product base64 images": {
      "main": [
        [
          {
            "node": "Combine vector search results with images",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Check if message is button or image": {
      "main": [
        [
          {
            "node": "Route interactive vs image message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Validate incoming message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine products with base64 images": {
      "main": [
        [
          {
            "node": "Store products in cache",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load session and append user message": {
      "main": [
        [
          {
            "node": "AI shopping agent (BytezBot)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate merged selfie before try-on": {
      "main": [
        [
          {
            "node": "Build Gemini image generation API payload",
            "type": "main",
            "index": 0
          },
          {
            "node": "Notify user VTO is processing",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ask user to resend valid photo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert Gemini response to image file": {
      "main": [
        [
          {
            "node": "Upload VTO result to WhatsApp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route JSON intent vs plain text reply": {
      "main": [
        [
          {
            "node": "Route product search vs recommend",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send text response to user",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert product image base64 to binary": {
      "main": [
        [
          {
            "node": "Upload product image to WhatsApp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect JSON intent or plain text reply": {
      "main": [
        [
          {
            "node": "Append AI reply to session history",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract image ID, waId, and product ID": {
      "main": [
        [
          {
            "node": "Get product image from MongoDB",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get WhatsApp media URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Redis cache key from search query": {
      "main": [
        [
          {
            "node": "Check Redis cache",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download product image from Drive (VTO)": {
      "main": [
        [
          {
            "node": "Resize product image to 1024px",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Gemini image generation API payload": {
      "main": [
        [
          {
            "node": "Generate VTO with Gemini API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine vector search results with images": {
      "main": [
        [
          {
            "node": "Combine products with base64 images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge product, selfie, and validation result": {
      "main": [
        [
          {
            "node": "Validate merged selfie before try-on",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}