{"id" : "StL4PRyRwgNOyc4U", "name" : "kq-chatwoot-agent", "active" : true, "nodes" : [{"id":"trigger_id","name":"Chatwoot Webhook","type":"n8n-nodes-base.webhook","position":[0,0],"webhookId":"kq_webhook_id","parameters":{"path":"kq-chatwoot","options":{},"httpMethod":"POST"},"typeVersion":1},{"id":"proc_msg_id","name":"Procesar mensaje","type":"n8n-nodes-base.code","position":[224,0],"parameters":{"jsCode":"const body = $json.body || $json;\nconst event = body.event;\nconst messageType = body.message_type;\nconst isIncoming = messageType === 'incoming' || messageType === 0;\nconst CHATWOOT_API_TOKEN = 'ceb2e90b41acc15c5a09d0b84e9ab887';\nconst ACCOUNT_ID = 4;\n\nasync function chatwootRequest(method, path) {\n  const url = 'http://chatwoot-web:3000/api/v1/accounts/' + ACCOUNT_ID + path;\n  const options = {\n    method,\n    url,\n    headers: { api_access_token: CHATWOOT_API_TOKEN },\n    json: true,\n    returnFullResponse: true,\n    ignoreHttpStatusErrors: true,\n  };\n  if (this.helpers?.httpRequest) return await this.helpers.httpRequest(options);\n  if (typeof fetch === 'function') {\n    const response = await fetch(url, { method, headers: options.headers });\n    return { statusCode: response.status, body: await response.json().catch(() => null) };\n  }\n  throw new Error('no_http_client_available');\n}\n\nif (event !== 'message_created' || !isIncoming) {\n  return [{ json: { skip: true, reason: 'ignored: event=' + event + ' type=' + messageType } }];\n}\n\nif (body.private === true) {\n  return [{ json: { skip: true, reason: 'private note' } }];\n}\n\nif (!body.content && (!body.attachments || body.attachments.length === 0)) {\n  return [{ json: { skip: true, reason: 'empty message' } }];\n}\n\nconst conversation = body.conversation || {};\nconst conversationId = conversation.id || body.conversation_id || body.conversation?.id;\nconst webhookAssigneeId = conversation.assignee_id || conversation.assignee?.id || conversation.meta?.assignee?.id || body.assignee_id || null;\nconst webhookLabels = [\n  ...(Array.isArray(conversation.labels) ? conversation.labels : []),\n  ...(Array.isArray(body.labels) ? body.labels : []),\n].map(label => String(typeof label === 'string' ? label : (label?.title || label?.name || '')).toLowerCase().trim()).filter(Boolean);\n\nlet liveAssigneeId = webhookAssigneeId;\nlet liveLabels = webhookLabels;\nlet liveStatus = conversation.status || body.status || null;\nlet conversationCheckWarning = '';\n\nif (conversationId) {\n  try {\n    const response = await chatwootRequest.call(this, 'GET', '/conversations/' + conversationId);\n    const statusCode = response.statusCode || response.status || 200;\n    if (statusCode >= 200 && statusCode < 300) {\n      const liveConversation = response.body || response;\n      liveAssigneeId = liveConversation.assignee_id || liveConversation.meta?.assignee?.id || liveConversation.assignee?.id || liveAssigneeId || null;\n      liveStatus = liveConversation.status || liveStatus;\n      const labels = liveConversation.labels || liveConversation.label_list || liveConversation.custom_attributes?.labels || [];\n      liveLabels = [...liveLabels, ...(Array.isArray(labels) ? labels : [])]\n        .map(label => String(typeof label === 'string' ? label : (label?.title || label?.name || '')).toLowerCase().trim())\n        .filter(Boolean);\n    } else {\n      return [{ json: { skip: true, reason: 'chatwoot_conversation_check_failed: ' + statusCode, conversation_id: conversationId } }];\n    }\n  } catch (error) {\n    conversationCheckWarning = 'chatwoot_conversation_check_warning';\n  }\n}\n\nif (liveAssigneeId) {\n  return [{ json: { skip: true, reason: 'handoff: assignee_id=' + liveAssigneeId, conversation_id: conversationId } }];\n}\n\nif (['pending', 'snoozed', 'resolved'].includes(String(liveStatus || '').toLowerCase())) {\n  return [{ json: { skip: true, reason: 'conversation_status: ' + liveStatus, conversation_id: conversationId } }];\n}\n\nif (liveLabels.some(label => ['bot-pausa', 'bot_pausa', 'bot pausa', 'pausa-bot', 'no-followup'].includes(label))) {\n  return [{ json: { skip: true, reason: 'handoff: bot-pausa label', conversation_id: conversationId, labels: liveLabels } }];\n}\n\nreturn [{ json: {\n  skip: false,\n  message_id: body.id,\n  conversation_id: conversationId,\n  contact_name: body.sender?.name || 'Prospecto',\n  content: body.content || '',\n  content_type: body.content_type || 'text',\n  attachments: body.attachments || [],\n  contact_phone: body.sender?.phone_number || body.meta?.sender?.phone_number || '',\n  conversation_check_warning: conversationCheckWarning,\n} }];"},"typeVersion":2},{"id":"is_valid_id","name":"Mensaje valido?","type":"n8n-nodes-base.if","position":[448,0],"parameters":{"conditions":{"boolean":[{"value1":"={{ $json.skip }}"}]}},"typeVersion":1},{"id":"wait_id","name":"Esperar 20s","type":"n8n-nodes-base.wait","position":[672,0],"webhookId":"30216d1f-f40d-485a-83bb-c192be0b9f38","parameters":{"unit":"seconds","amount":20},"typeVersion":1},{"id":"get_msgs_id","name":"GET Mensajes Chatwoot","type":"n8n-nodes-base.httpRequest","maxTries":3,"position":[896,0],"parameters":{"url":"=http://chatwoot-web:3000/api/v1/accounts/4/conversations/{{ $('Procesar mensaje').first().json.conversation_id }}/messages","options":{},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"api_access_token","value":"ceb2e90b41acc15c5a09d0b84e9ab887"}]}},"retryOnFail":true,"typeVersion":4.4,"waitBetweenTries":5000},{"id":"check_last_id","name":"Verificar ultimo mensaje","type":"n8n-nodes-base.code","position":[1120,0],"parameters":{"jsCode":"const msgs = $input.first().json;\nconst all = msgs.payload || [];\nconst ourId  = $('Procesar mensaje').first().json.message_id;\n\nconst incoming = all.filter(m => m.message_type === 0 || m.message_type === 'incoming');\nif (!incoming.length) return [{ json: { isLast: false } }];\n\nconst lastIncomingId = Math.max(...incoming.map(m => m.id));\nif (lastIncomingId > ourId) return [{ json: { isLast: false, reason: 'newer message exists' } }];\n\nconst outgoing = all.filter(m => m.message_type === 1 || m.message_type === 'outgoing');\nconst lastBotId = outgoing.length ? Math.max(...outgoing.map(m => m.id)) : 0;\n\nconst pending = incoming.filter(m => m.id > lastBotId);\nif (pending.length === 0) return [{ json: { isLast: false, reason: 'already answered' } }];\n\n// Dedup: if a bot reply already exists for these pending msgs (double-webhook race), skip\nconst now = Math.floor(Date.now() / 1000);\nconst maxPendingId = Math.max(...pending.map(m => m.id));\nconst botAlreadyHandled = outgoing.find(m => m.id > maxPendingId && (now - m.created_at) < 60);\nif (botAlreadyHandled) return [{ json: { isLast: false, reason: 'dedup: bot already responded' } }];\n\nconst hasAudio = pending.some(m =>\n  (m.content_type||'').includes('audio') ||\n  (m.attachments||[]).some(a => (a.file_type||'').includes('audio'))\n);\nconst hasImage = pending.some(m =>\n  (m.content_type||'').includes('image') || (m.content_type||'').includes('sticker') ||\n  (m.attachments||[]).some(a => (a.file_type||'').includes('image') || (a.file_type||'').includes('sticker'))\n);\n\nconst textContent = pending.map(m => m.content || '').filter(Boolean).join('\\n');\n\nconst recentContext = all\n  .filter(m => !m.private && (m.message_type === 0 || m.message_type === 1 || m.message_type === 'incoming' || m.message_type === 'outgoing'))\n  .sort((a, b) => a.id - b.id)\n  .slice(-12)\n  .map(m => {\n    const role = (m.message_type === 0 || m.message_type === 'incoming') ? 'Cliente' : 'Agente';\n    const text = String(m.content || '').replace(/[\\r\\n]+/g, ' ').trim();\n    return text ? role + ': ' + text.slice(0, 500) : '';\n  })\n  .filter(Boolean);\n\nconst audioUrl = pending.flatMap(m => m.attachments || [])\n  .find(a => (a.file_type||'').includes('audio'))?.data_url;\nconst imageUrl = pending.flatMap(m => m.attachments || [])\n  .find(a => (a.file_type||'').includes('image') || (a.file_type||'').includes('sticker'))?.data_url;\n\nreturn [{ json: {\n  isLast: true, ourId, lastIncomingId,\n  contact_name: $('Procesar mensaje').first().json.contact_name,\n  textContent, recentContext, hasAudio, hasImage, audioUrl, imageUrl\n} }];"},"typeVersion":2},{"id":"is_last_if_id","name":"Es el ultimo?","type":"n8n-nodes-base.if","position":[1344,0],"parameters":{"conditions":{"boolean":[{"value1":"={{ $json.isLast }}","value2":true}]}},"typeVersion":1},{"id":"prep_ctx_id","name":"Preparar contexto","type":"n8n-nodes-base.code","position":[1568,0],"parameters":{"jsCode":"const d = $input.first().json;\nconst parts = [];\n\nif (Array.isArray(d.recentContext) && d.recentContext.length) {\n  parts.push('[SISTEMA: Historial reciente de Chatwoot. Usalo para mantener continuidad y no repetir preguntas ya respondidas.]');\n  parts.push(d.recentContext.join('\\n'));\n}\nif (d.hasImage && d.imageUrl) {\n  parts.push(`[SISTEMA: El usuario envi? una imagen. URL: ${d.imageUrl}. Antes de responder sobre ella, US? la herramienta analyze_image para entender qu? hay en la foto.]`);\n}\nif (d.hasAudio && d.audioUrl) {\n  parts.push(`[SISTEMA: El usuario envi? un audio. URL: ${d.audioUrl}. Antes de responder, US? la herramienta transcribe_audio para saber qu? dijo.]`);\n}\nif (d.textContent) {\n  parts.push(d.textContent);\n}\n\nconst prompt = parts.join('\\n') || '[sin texto]';\nreturn [{ json: { ...d, prompt } }];"},"typeVersion":2},{"id":"agent_id","name":"Sam - AI Agent","type":"@n8n/n8n-nodes-langchain.agent","maxTries":2,"position":[1976,0],"parameters":{"text":"={{ $json.prompt }}","agent":"openAiFunctionsAgent","options":{"systemMessage":"=[SISTEMA] Datos del cliente actual (usalos cuando escales a Ale y para no repetir preguntas):\n- nombre: {{ $node[\"Procesar mensaje\"].json.contact_name }}\n- telefono: {{ $node[\"Procesar mensaje\"].json.contact_phone }}\n- conversation_id: {{ $node[\"Procesar mensaje\"].json.conversation_id }}\n\nSos Sam, el asistente virtual oficial de Kings & Queens English Learning, un instituto de ingl?s en Bah?a Blanca dirigido por Alejandra Albornoz.\n\n## Tu Identidad\n- Responde en el idioma del cliente (espa?ol o ingl?s).\n- Tono c?lido, profesional y motivador. Mensajes cortos.\n- Si preguntan si sos IA: 'Soy Sam, el asistente virtual de Kings & Queens.'\n\n## Contexto de la conversacion\n- Usa el historial reciente de Chatwoot y la memoria de la conversacion antes de responder.\n- No vuelvas a preguntar nombre, modalidad o interes si ya aparece en mensajes anteriores.\n- Si el cliente responde corto, interpretalo segun el mensaje anterior.\n\n## Tu Objetivo\nEntender qu? busca la persona y conectarla con Ale de forma t?cnica.\n\n## Recolecci?n de Datos (Regla estricta)\nAntes de escalar a Ale, debes tener el NOMBRE del cliente. \n1. Si el sistema no te da el nombre (o dice \"Sin nombre\"), PREG?NTALO amablemente antes de cualquier otra cosa.\n2. Identifica el inter?s: ?Clases de ingl?s? ?Coffee Talk? ?Ex?menes internacionales?\n\n## Informaci?n de Clases e Instituto\n- Ubicaci?n: Sede f?sica en Bah?a Blanca (direcci?n: 11 de abril 686).\n- Modalidades: 100% Online (plataforma propia en https://www.kingsandqueens.com.ar/) o Presencial.\n- Tipos de clases: Individuales (particulares) o Grupos reducidos (m?ximo 6 personas) para asegurar el aprendizaje.\n- Niveles: Desde principiantes absolutos hasta avanzados.\n- Especialidades: Preparaci?n de ex?menes internacionales (Cambridge), Ingl?s para Negocios, Viajes y conversaci?n fluida.\n- Horarios: Amplia disponibilidad de lunes a jueves. Ale coordina la agenda espec?fica.\n\n\n## EVENTO ESPECIAL: COFFEE TALK\n- Qué es: práctica de inglés charlando y tomando café, en un ambiente relajado y diferente. Lo organiza Kings & Queens en Bahía Blanca.\n- Próxima edición: TODAVÍA SIN FECHA CONFIRMADA.\n- Si preguntan precio, lugar u horario: respondé que se anuncian junto con la fecha por Instagram. PROHIBIDO mencionar montos de entrada: ni de ediciones anteriores, ni estimados, ni aproximados. No existe ningún precio para informar.\n- Si preguntan: contá qué es y decí que la próxima fecha se anuncia en el Instagram de Kings & Queens (@kingsandqueensenglish). Recomendá seguir la cuenta para enterarse apenas salga.\n- Si muestran mucho interés, ofrecé avisarle a Ale para que les escriba apenas haya fecha (escalá como siempre si aceptan).\n\n\n## Nuevo programa de estudio: Ingl?s para universitarios\n- Curso de ingl?s para universitarios.\n- Modalidad grupal reducida (5 alumnos).\n- No requiere comprar libro.\n- No se cobra matr?cula ni inscripci?n.\n- Una clase semanal de 1 hora.\n- Inicio: ya comenzó (1 de junio). Por incorporaciones tardías, ofrecé conectarlo con Ale.\n- D?as y horarios: lunes de 15:30 a 16:30 hs.\n- Valor mensual: $50.000.\n- Para reservar el lugar, se solicita una se?a del 50%.\n- La propuesta est? pensada como una opci?n accesible para adquirir conocimientos b?sicos y pr?cticos de ingl?s.\n\n\n## Nuevo programa para adultos que quieren viajar. \n- Curso con grupo reducido o personalizado. \n- Pr?cticas de conversaci?n. \n\n\n## Reglas de Oro (CR?TICAS)\n- Para pasar el prospecto a Ale (escalar): respond? al cliente de forma c?lida que Ale lo va a contactar, y termin? tu mensaje con el marcador exacto [[ESCALAR]] en una l?nea aparte al final. El cliente NO ve ese marcador. NO uses ninguna herramienta para escalar.\n- Si el cliente dice \"Quiero la entrada\", \"Quiero ir\", \"Me interesa\" o pide hablar con un humano, escal? INMEDIATAMENTE (mensaje c?lido + [[ESCALAR]] al final).\n- Al escalar, decile al cliente: \"?Perfecto! Le aviso a Ale ahora mismo, te va a escribir por WhatsApp.\" y agreg? [[ESCALAR]] al final.\n- Nunca des precios de cursos, solo del Coffee Talk. Para cursos di: \"Ale te armar? el mejor plan seg?n tu nivel. ?Te conecto con ella?\".\n\n## Herramientas\n- qualify_student: ?sala cuando tengas detalles de nivel y disponibilidad.\n- Para escalar a Ale NO uses herramientas: termin? tu respuesta con [[ESCALAR]] (ver Reglas de Oro).\n- transcribe_audio / analyze_image: Para procesar audios o im?genes.\n\n## Regla dura de errores internos\n- Nunca menciones al cliente errores t?cnicos, n8n, workflows, webhooks, APIs, tokens, credenciales, stack traces, nombres de nodos, herramientas internas ni fallas de automatizaci?n.\n- Si una herramienta falla o no pod?s completar una acci?n interna, no expliques la falla. Respond? de forma humana y breve: \"Dame un segundito, voy a derivarlo internamente para que lo revisen y te respondamos bien.\"\n- Nunca pidas al cliente que reintente por un error del sistema. El problema se resuelve internamente.\n\n## KQ_SUPPORT_START\n## Apoyo escolar de inglés\n- Usá herramientas para horarios, cupos, seña y reservas. Nunca inventes disponibilidad.\n- Si preguntan por disponibilidad, horarios o cupos de apoyo escolar, llamá search_support_slots AHORA y mostrá las opciones reales en el momento. NUNCA digas que vas a derivar o consultar para ver disponibilidad de apoyo: la consultás vos con la herramienta.\n- Mostrá cada horario usando el campo \"label\" tal cual viene (ej. \"miércoles 1/7\"); NUNCA calcules ni cambies el día de la semana. Si el cliente pide un día puntual (ej. jueves), pasá ese día en preferred_days.\n- Podés informar únicamente el importe de seña devuelto por hold_support_slot.\n- Para otros cursos sigue prohibido dar precios sin derivar a Ale.\n- Pedí nombre, primario/secundario, grado o año, tema y preferencia horaria.\n- Mostrá como máximo las 3 opciones devueltas por search_support_slots.\n- No confirmes pago: el comprobante queda en revisión de Ale.\n## KQ_SUPPORT_END\n\n## KQ_ESCALATION_RULE\n- Para derivar/escalar a Ale usás SOLO el marcador [[ESCALAR]] al final del mensaje, en una línea aparte. NUNCA llames a una herramienta para escalar.\n- Si en tu respuesta decís que vas a avisar, derivar o escalar a Ale, en ESE MISMO mensaje tenés que terminar con [[ESCALAR]]. Prohibido decir que derivás y no poner el marcador."}},"retryOnFail":true,"typeVersion":1,"waitBetweenTries":25000},{"id":"model_id","name":"OpenAI GPT-4o","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[2048,224],"parameters":{"model":"gpt-4o","options":{}},"credentials":{"openAiApi":{"id":"cFtw4Caorz0YjexB","name":"OpenAi account"}},"typeVersion":1},{"id":"memory_id","name":"Window Buffer Memory","type":"@n8n/n8n-nodes-langchain.memoryBufferWindow","position":[2176,224],"parameters":{"sessionKey":"={{ $('Procesar mensaje').first().json.conversation_id }}","sessionIdType":"customKey","contextWindowLength":15},"typeVersion":1.3},{"id":"tool_0_id","name":"qualify_student","type":"@n8n/n8n-nodes-langchain.toolWorkflow","position":[2304,224],"parameters":{"name":"qualify_student","fields":{"values":[]},"workflowId":{"__rl":true,"mode":"id","value":"IOR5NJw4jjgN4Efn"},"description":"Califica un prospecto de Kings & Queens with score 0-10. Campos: nivel_actual, objetivo, disponibilidad_semanal_hs, urgencia_semanas, nombre_contacto"},"typeVersion":1.2},{"id":"tool_1_id","name":"escalate_to_ale","type":"@n8n/n8n-nodes-langchain.toolWorkflow","position":[0,328],"parameters":{"name":"escalate_to_ale","fields":{"values":[{"name":"nombre","type":"string"},{"name":"telefono","type":"string"},{"name":"resumen","type":"string"},{"name":"conversation_id","type":"number"}]},"workflowId":{"__rl":true,"mode":"id","value":"K3sm7VWSaTqbLbTv"},"description":"Avisa a Ale de una nueva oportunidad lista para que la contacte. Ale le escribira al cliente directamente desde su telefono. SIEMPRE completa: nombre, telefono, resumen con contexto real de la charla (curso/Coffee Talk/nivel/pago/fecha/etc), conversation_id."},"typeVersion":1.2},{"id":"tool_2_id","name":"transcribe_audio","type":"@n8n/n8n-nodes-langchain.toolWorkflow","position":[1920,224],"parameters":{"name":"transcribe_audio","fields":{"values":[{"name":"query","type":"string","stringValue":"={{ $('Verificar ultimo mensaje').first().json.audioUrl }}"}]},"workflowId":{"__rl":true,"mode":"id","value":"8jcBAMIq7SQkXYcK"},"description":"Transcribe una nota de voz. Par?metros: media_url (string)."},"typeVersion":1.2},{"id":"tool_3_id","name":"analyze_image","type":"@n8n/n8n-nodes-langchain.toolWorkflow","position":[1792,224],"parameters":{"name":"analyze_image","fields":{"values":[{"name":"query","type":"string","stringValue":"={{ $('Verificar ultimo mensaje').first().json.imageUrl }}"}]},"workflowId":{"__rl":true,"mode":"id","value":"ds2jEUbN03t39n4c"},"description":"Analiza una imagen enviada por el cliente. Par?metros: media_url (string)."},"typeVersion":1.2},{"id":"send_cw_id","name":"Enviar a Chatwoot","type":"n8n-nodes-base.httpRequest","maxTries":3,"position":[3856,0],"parameters":{"url":"=http://chatwoot-web:3000/api/v1/accounts/4/conversations/{{ $('Procesar mensaje').first().json.conversation_id }}/messages","method":"POST","options":{},"sendBody":true,"sendHeaders":true,"bodyParameters":{"parameters":[{"name":"content","value":"={{ $('Sanitizar respuesta').first().json.output }}"},{"name":"message_type","value":"outgoing"},{"name":"private","value":"false"}]},"headerParameters":{"parameters":[{"name":"api_access_token","value":"ceb2e90b41acc15c5a09d0b84e9ab887"}]}},"retryOnFail":true,"typeVersion":4.4,"waitBetweenTries":3000},{"id":"sanitize_response_id","name":"Sanitizar respuesta","type":"n8n-nodes-base.code","position":[2512,0],"parameters":{"jsCode":"const data = $input.first().json;\nlet raw = String(data.output || '').trim();\nconst escalated = /\\[\\[\\s*ESCALAR\\s*\\]\\]|\\[\\[\\s*ESCALA\\s*\\]?\\]?|\\bESCALAR\\b|\\bESCALA\\b/i.test(raw);\nraw = raw\n  .replace(/\\[\\[\\s*ESCALAR\\s*\\]\\]/gi, '')\n  .replace(/\\[\\[\\s*ESCALA\\s*\\]?\\]?/gi, '')\n  .replace(/\\bESCALAR\\b/gi, '')\n  .replace(/\\bESCALA\\b/gi, '')\n  .replace(/\\[\\[?\\s*\\]?\\]?\\s*$/g, '')\n  .replace(/[ \\t]+\\n/g, '\\n')\n  .replace(/\\n{3,}/g, '\\n\\n')\n  .trim();\nconst fallback = 'Dame un segundito, voy a derivarlo internamente para que lo revisen y te respondamos bien.';\nconst technicalPatterns = [/\b(n8n|workflow|webhook|api key|token|credencial|credential|stack trace|traceback)\b/i,/\b(internal error|technical error|error interno|workflow error|node error|api error)\b/i,/\b(https*[45][0-9]{2}|cannot read properties|undefined|null|econn|etimedout|enotfound)\b/i,/\b(Chatwoot|Evolution API|OpenAI|Whisper|Execute Workflow|tool call|toolWorkflow)\b/i];\nconst blocked = !raw || technicalPatterns.some((pattern) => pattern.test(raw));\n\nfunction normalize(value) {\n  return String(value || '')\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[̀-ͯ]/g, '')\n    .replace(/s+/g, ' ')\n    .trim();\n}\nlet clientText = '';\ntry { clientText = $('Verificar ultimo mensaje').first().json.textContent || ''; } catch (_) {}\ntry { if (!clientText) clientText = $('Procesar mensaje').first().json.content || ''; } catch (_) {}\nconst normalizedClient = normalize(clientText);\nconst closeAfterResponse = !escalated && (\n  /\bnos+gracias\b/.test(normalizedClient) ||\n  /\bnos+mes+interesa\b/.test(normalizedClient) ||\n  /\bnos+estoys+interesad[oa]\b/.test(normalizedClient) ||\n  /\bnos+quiero\b/.test(normalizedClient) ||\n  /\bpaso\b/.test(normalizedClient) ||\n  /\bgraciass+peros+no\b/.test(normalizedClient) ||\n  /\bnos+puedos+asistir\b/.test(normalizedClient)\n);\nreturn [{ json: { ...data, output: blocked ? fallback : raw, internal_error_masked: blocked, escalated, close_after_response: closeAfterResponse, original_output: blocked ? raw.slice(0, 1500) : undefined } }];"},"typeVersion":2},{"id":"if_escalo_id","name":"¿Escaló?","type":"n8n-nodes-base.if","position":[2736,0],"parameters":{"conditions":{"boolean":[{"value1":"={{ $json.escalated }}","value2":true}]}},"typeVersion":1},{"id":"wa_ale_id","name":"WhatsApp a Ale","type":"n8n-nodes-base.httpRequest","onError":"continueRegularOutput","position":[2960,72],"parameters":{"url":"http://evolution-api:8080/message/sendText/Kings-and-Queens","method":"POST","options":{},"sendBody":true,"sendHeaders":true,"bodyParameters":{"parameters":[{"name":"number","value":"5492914227793"},{"name":"text","value":"={{ (() => {\n  const p = $('Procesar mensaje').first().json || {};\n  const v = $('Verificar ultimo mensaje').first().json || {};\n  const nombre = p.contact_name || 'Sin nombre';\n  const telefono = p.contact_phone || 's/d';\n  const consulta = v.textContent || p.content || '(ver chat)';\n  const recent = Array.isArray(v.recentContext) ? v.recentContext.slice(-10).join('\\n') : '';\n  const motivo = 'El cliente pidio avanzar o acepto que Ale lo contacte.';\n  return [\n    'KINGS & QUEENS - Nueva oportunidad',\n    '',\n    'Cliente: ' + nombre,\n    'WhatsApp: ' + telefono,\n    '',\n    'Motivo:',\n    motivo,\n    '',\n    'Ultimo mensaje del cliente:',\n    consulta,\n    '',\n    'Resumen de la charla:',\n    recent || consulta,\n    '',\n    'Escribile vos directamente desde tu WhatsApp.'\n  ].join('\\n').slice(0, 3500);\n})() }}"}]},"headerParameters":{"parameters":[{"name":"apikey","value":"edd_secret_token"}]}},"typeVersion":4.4},{"id":"cerrar_id","name":"Cerrar conversacion","type":"n8n-nodes-base.httpRequest","onError":"continueRegularOutput","position":[3632,72],"parameters":{"url":"=http://chatwoot-web:3000/api/v1/accounts/4/conversations/{{ $('Procesar mensaje').first().json.conversation_id }}/toggle_status","method":"POST","options":{},"jsonBody":"{\"status\": \"resolved\"}","sendBody":true,"sendHeaders":true,"specifyBody":"json","headerParameters":{"parameters":[{"name":"api_access_token","value":"ceb2e90b41acc15c5a09d0b84e9ab887"}]}},"typeVersion":4.4},{"id":"nota_escalada_id","name":"Nota escalada Chatwoot","type":"n8n-nodes-base.httpRequest","onError":"continueRegularOutput","position":[3184,72],"parameters":{"url":"=http://chatwoot-web:3000/api/v1/accounts/4/conversations/{{ $('Procesar mensaje').first().json.conversation_id }}/messages","method":"POST","options":{},"sendBody":true,"sendHeaders":true,"bodyParameters":{"parameters":[{"name":"content","value":"={{ (() => {\n  const p = $('Procesar mensaje').first().json || {};\n  const v = $('Verificar ultimo mensaje').first().json || {};\n  const nombre = p.contact_name || 'Sin nombre';\n  const telefono = p.contact_phone || 's/d';\n  const consulta = v.textContent || p.content || '(ver chat)';\n  const recent = Array.isArray(v.recentContext) ? v.recentContext.slice(-10).join('\\n') : '';\n  const motivo = 'El cliente pidio avanzar o acepto que Ale lo contacte.';\n  return [\n    'KINGS & QUEENS - Nueva oportunidad',\n    '',\n    'Cliente: ' + nombre,\n    'WhatsApp: ' + telefono,\n    '',\n    'Motivo:',\n    motivo,\n    '',\n    'Ultimo mensaje del cliente:',\n    consulta,\n    '',\n    'Resumen de la charla:',\n    recent || consulta,\n    '',\n    'Escribile vos directamente desde tu WhatsApp.'\n  ].join('\\n').slice(0, 3500);\n})() }}"},{"name":"message_type","value":"outgoing"},{"name":"private","value":"true"}]},"headerParameters":{"parameters":[{"name":"api_access_token","value":"ceb2e90b41acc15c5a09d0b84e9ab887"}]}},"typeVersion":4.4},{"id":"nf_19jedvmr","name":"Marcar no-followup escalada","type":"n8n-nodes-base.httpRequest","position":[3408,72],"parameters":{"url":"=http://chatwoot-web:3000/api/v1/accounts/4/conversations/{{ $('Procesar mensaje').first().json.conversation_id }}/labels","method":"POST","options":{},"jsonBody":"{\"labels\":[\"no-followup\"]}","sendBody":true,"sendHeaders":true,"specifyBody":"json","headerParameters":{"parameters":[{"name":"api_access_token","value":"ceb2e90b41acc15c5a09d0b84e9ab887"}]}},"typeVersion":4.2},{"id":"if_close_rejection","name":"?Cerrar por rechazo?","type":"n8n-nodes-base.if","position":[4080,0],"parameters":{"conditions":{"boolean":[{"value1":"={{ $(\"Sanitizar respuesta\").first().json.close_after_response === true }}","value2":true}]}},"typeVersion":1},{"id":"n_48g9048s","name":"Marcar no-followup rechazo","type":"n8n-nodes-base.httpRequest","position":[4304,0],"parameters":{"url":"=http://chatwoot-web:3000/api/v1/accounts/4/conversations/{{ $('Procesar mensaje').first().json.conversation_id }}/labels","method":"POST","options":{},"jsonBody":"{\"labels\":[\"no-followup\"]}","sendBody":true,"sendHeaders":true,"specifyBody":"json","headerParameters":{"parameters":[{"name":"api_access_token","value":"ceb2e90b41acc15c5a09d0b84e9ab887"}]}},"typeVersion":4.2},{"id":"n_uya6ls5m","name":"Cerrar por rechazo","type":"n8n-nodes-base.httpRequest","position":[4528,0],"parameters":{"url":"=http://chatwoot-web:3000/api/v1/accounts/4/conversations/{{ $('Procesar mensaje').first().json.conversation_id }}/toggle_status","method":"POST","options":{},"jsonBody":"{\"status\":\"resolved\"}","sendBody":true,"sendHeaders":true,"specifyBody":"json","headerParameters":{"parameters":[{"name":"api_access_token","value":"ceb2e90b41acc15c5a09d0b84e9ab887"}]}},"typeVersion":4.2},{"id":"kqs_search_support_slots","name":"search_support_slots","type":"@n8n/n8n-nodes-langchain.toolWorkflow","typeVersion":1.2,"position":[1740,640],"parameters":{"name":"search_support_slots","description":"Busca hasta 3 horarios de apoyo escolar de inglés disponibles. Devuelve id, fecha, hora, cupos restantes y seña. Nunca inventes disponibilidad. Campos opcionales: preferred_days, preferred_time (HH:MM). Cada horario ya trae los campos 'label' y 'weekday' calculados: usalos tal cual, NO calcules el día vos. Si el cliente pide un día (ej. jueves), pasalo en preferred_days.","workflowId":{"__rl":true,"value":"cr85ae0Lpg1P9Kof","mode":"id"},"fields":{"values":[]}}},{"id":"kqs_hold_support_slot","name":"hold_support_slot","type":"@n8n/n8n-nodes-langchain.toolWorkflow","typeVersion":1.2,"position":[1800,640],"parameters":{"name":"hold_support_slot","description":"Reserva (hold de 12 h) un horario de apoyo escolar. Devuelve la seña, alias e instrucciones de transferencia. Campos: slot_id, student_name, contact_phone, guardian_name, education_level (primary|secondary), school_year, topic.","workflowId":{"__rl":true,"value":"SENuSRYarWLDR6c6","mode":"id"},"fields":{"values":[{"name":"conversation_id","type":"string","stringValue":"={{ $('Procesar mensaje').first().json.conversation_id }}"}]}}},{"id":"kqs_attach_support_receipt","name":"attach_support_receipt","type":"@n8n/n8n-nodes-langchain.toolWorkflow","typeVersion":1.2,"position":[1860,640],"parameters":{"name":"attach_support_receipt","description":"Adjunta el comprobante de transferencia a la reserva activa y la deja en revisión de Ale. Campos: receipt_url (URL del comprobante recibido en el chat).","workflowId":{"__rl":true,"value":"nxtQHJn8Zj6L7zBj","mode":"id"},"fields":{"values":[{"name":"conversation_id","type":"string","stringValue":"={{ $('Procesar mensaje').first().json.conversation_id }}"}]}}},{"id":"kqs_cancel_support_booking","name":"cancel_support_booking","type":"@n8n/n8n-nodes-langchain.toolWorkflow","typeVersion":1.2,"position":[1920,640],"parameters":{"name":"cancel_support_booking","description":"Cancela una reserva de apoyo. Con más de 24 h genera crédito; con menos, se pierde la seña. Campos: booking_code, contact_phone.","workflowId":{"__rl":true,"value":"ZKOLKiQkA70UrrF6","mode":"id"},"fields":{"values":[]}}},{"id":"kqs_reschedule_support_booking","name":"reschedule_support_booking","type":"@n8n/n8n-nodes-langchain.toolWorkflow","typeVersion":1.2,"position":[1980,640],"parameters":{"name":"reschedule_support_booking","description":"Reprograma un crédito de apoyo a otro horario (una sola vez). Campos: booking_code, contact_phone, slot_id (horario destino).","workflowId":{"__rl":true,"value":"J9DCCNOKk7MLLPST","mode":"id"},"fields":{"values":[]}}}], "connections" : {"Esperar 20s":{"main":[[{"node":"GET Mensajes Chatwoot","type":"main","index":0}]]},"Es el ultimo?":{"main":[[{"node":"Preparar contexto","type":"main","index":0}]]},"OpenAI GPT-4o":{"ai_languageModel":[[{"node":"Sam - AI Agent","type":"ai_languageModel","index":0}]]},"analyze_image":{"ai_tool":[[{"node":"Sam - AI Agent","type":"ai_tool","index":3}]]},"Sam - AI Agent":{"main":[[{"node":"Sanitizar respuesta","type":"main","index":0}]]},"WhatsApp a Ale":{"main":[[{"node":"Nota escalada Chatwoot","type":"main","index":0}]]},"Mensaje valido?":{"main":[[{"node":"Esperar 20s","type":"main","index":0}]]},"qualify_student":{"ai_tool":[[{"node":"Sam - AI Agent","type":"ai_tool","index":0}]]},"Chatwoot Webhook":{"main":[[{"node":"Procesar mensaje","type":"main","index":0}]]},"Procesar mensaje":{"main":[[{"node":"Mensaje valido?","type":"main","index":0}]]},"transcribe_audio":{"ai_tool":[[{"node":"Sam - AI Agent","type":"ai_tool","index":2}]]},"Preparar contexto":{"main":[[{"node":"Sam - AI Agent","type":"main","index":0}]]},"Cerrar conversacion":{"main":[[{"node":"Enviar a Chatwoot","type":"main","index":0}]]},"Sanitizar respuesta":{"main":[[{"node":"¿Escaló?","type":"main","index":0}]]},"Window Buffer Memory":{"ai_memory":[[{"node":"Sam - AI Agent","type":"ai_memory","index":0}]]},"GET Mensajes Chatwoot":{"main":[[{"node":"Verificar ultimo mensaje","type":"main","index":0}]]},"Nota escalada Chatwoot":{"main":[[{"node":"Marcar no-followup escalada","type":"main","index":0}]]},"Verificar ultimo mensaje":{"main":[[{"node":"Es el ultimo?","type":"main","index":0}]]},"¿Escaló?":{"main":[[{"node":"WhatsApp a Ale","type":"main","index":0}],[{"node":"Enviar a Chatwoot","type":"main","index":0}]]},"Marcar no-followup escalada":{"main":[[{"node":"Cerrar conversacion","type":"main","index":0}]]},"Enviar a Chatwoot":{"main":[[{"node":"?Cerrar por rechazo?","type":"main","index":0}]]},"?Cerrar por rechazo?":{"main":[[{"node":"Marcar no-followup rechazo","type":"main","index":0}],[]]},"Marcar no-followup rechazo":{"main":[[{"node":"Cerrar por rechazo","type":"main","index":0}]]},"search_support_slots":{"ai_tool":[[{"node":"Sam - AI Agent","type":"ai_tool","index":4}]]},"hold_support_slot":{"ai_tool":[[{"node":"Sam - AI Agent","type":"ai_tool","index":5}]]},"attach_support_receipt":{"ai_tool":[[{"node":"Sam - AI Agent","type":"ai_tool","index":6}]]},"cancel_support_booking":{"ai_tool":[[{"node":"Sam - AI Agent","type":"ai_tool","index":7}]]},"reschedule_support_booking":{"ai_tool":[[{"node":"Sam - AI Agent","type":"ai_tool","index":8}]]}}, "settings" : {"binaryMode":"separate","callerPolicy":"workflowsFromSameOwner","errorWorkflow":"agentErrorKQ001","availableInMCP":false,"executionOrder":"v1"}, "versionId" : "32920a94-176b-41c3-833c-552a8257de14"}
