From 3b0a8feb82f0ce96846c19431fd4ed4103d2e905 Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Sun, 23 Aug 2026 04:01:06 +0000 Subject: [PATCH] Create backup: Cotizador WMC - Agente Gemini + Supabase - n8n 2.21.4 READY --- ...ente-gemini-supabase-n8n-2-21-4-ready.json | 1677 +++++++++++++++++ 1 file changed, 1677 insertions(+) create mode 100644 cotizador-wmc-agente-gemini-supabase-n8n-2-21-4-ready.json diff --git a/cotizador-wmc-agente-gemini-supabase-n8n-2-21-4-ready.json b/cotizador-wmc-agente-gemini-supabase-n8n-2-21-4-ready.json new file mode 100644 index 0000000..75571a8 --- /dev/null +++ b/cotizador-wmc-agente-gemini-supabase-n8n-2-21-4-ready.json @@ -0,0 +1,1677 @@ +{ + "updatedAt": "2026-08-23T01:27:05.180Z", + "createdAt": "2026-08-22T14:59:56.567Z", + "id": "rtnO23gHlJfj45pA", + "name": "Cotizador WMC - Agente Gemini + Supabase - n8n 2.21.4 READY", + "description": null, + "active": true, + "isArchived": false, + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "cotizador-wmc-chat", + "responseMode": "responseNode", + "options": { + "allowedOrigins": "*" + } + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -928, + 1040 + ], + "id": "e7623673-c683-4b33-a76e-231a0efbf5e0", + "name": "Webhook - Chat", + "webhookId": "23c9a253-ba3d-47f0-85ca-21b3fde221c7" + }, + { + "parameters": { + "jsCode": "const body = $json.body ?? {};\nconst headers = $json.headers ?? {};\nconst clean = (v, max = 10000) => typeof v === 'string' ? v.trim().slice(0, max) : '';\nconst auth = clean(headers.authorization || headers.Authorization, 12000);\nconst text = clean(body.text, 12000);\nconst conversationId = clean(body.conversationId, 200);\nconst messageId = clean(body.messageId, 200);\nconst userEmail = clean(body.userEmail, 320).toLowerCase();\nconst userName = clean(body.userName, 200);\nconst appSessionId = clean(body.appSessionId, 250) || `backend-${Date.now()}`;\nconst sentAt = clean(body.sentAt, 100) || new Date().toISOString();\nconst attachments = Array.isArray(body.attachments) ? body.attachments.slice(0, 8).map((a) => ({\n filename: clean(a?.filename, 180), mediaType: clean(a?.mediaType, 120), url: clean(a?.url, 6000), storagePath: clean(a?.storagePath, 1000) || null,\n})) : [];\nif (!auth.startsWith('Bearer ')) throw new Error('Falta Authorization Bearer de Supabase.');\nif (!conversationId || !messageId) throw new Error('Faltan conversationId/messageId.');\nif (!text && attachments.length === 0) throw new Error('La solicitud no contiene texto ni archivos.');\nconst countryMap = [\n ['GT','guatemala'],['CR','costa rica'],['SV','el salvador'],['SV','salvador'],['HN','honduras'],['NI','nicaragua']\n];\nconst low = text.toLowerCase();\nlet countryCode = null;\nfor (const [code, term] of countryMap) if (low.includes(term)) { countryCode = code; break; }\nreturn [{json:{auth,text,conversationId,messageId,userEmail,userName,appSessionId,sentAt,attachments,countryCode,startedAt:Date.now()}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -480, + 1040 + ], + "id": "382e3c39-c66e-433a-b96f-a8c2336c8081", + "name": "Normalizar solicitud" + }, + { + "parameters": { + "url": "=https://dbit.digitalcompass.agency/auth/v1/user", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + } + ] + }, + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 0, + 1040 + ], + "id": "81c45b99-29e7-4485-910c-d8cd25218e30", + "name": "Supabase - validar JWT", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const req = $node[\"Normalizar solicitud\"].json;\nconst authUser = $json ?? {};\nconst email = String(authUser.email ?? '').trim().toLowerCase();\nconst id = String(authUser.id ?? '').trim();\nconst valid = Boolean(id && email && email.endsWith('@gomezleemarketing.com') && (!req.userEmail || req.userEmail === email));\nreturn [{json:{...req, userId:id, verifiedEmail:email, jwtValid:valid, jwtError: valid ? null : 'Sesión inválida o correo no coincide.'}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 384, + 1040 + ], + "id": "424dd516-bbb9-4c3e-938b-81de5b71f855", + "name": "Validar identidad" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "0bfb8fea-20b0-4cf3-8e89-9160c914cc8e", + "leftValue": "={{ $json.jwtValid }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 608, + 1040 + ], + "id": "f8a57cf7-4949-435e-b18b-0db46b9801b4", + "name": "JWT válido?" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ { ok:false, reply:'Tu sesión no es válida. Vuelve a iniciar sesión.' } }}", + "options": { + "responseCode": 401 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.4, + "position": [ + 832, + 1280 + ], + "id": "a4a57411-c41e-4fd2-a38f-670b024f8931", + "name": "Responder 401" + }, + { + "parameters": { + "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cotizador_wmc_access?select=email,full_name,is_active,can_use_tarifario&email=eq.' + encodeURIComponent($json.verifiedEmail) + '&is_active=eq.true' + '&limit=1' }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + } + ] + }, + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 832, + 960 + ], + "id": "ce76b3c8-6ac9-4c65-8c14-5872165e696a", + "name": "Supabase - consultar acceso", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const req = $node[\"Validar identidad\"].json;\nconst raw = $json ?? {};\n\n// n8n puede entregar la respuesta de PostgREST de varias formas:\n// 1) una fila directamente como $json (caso habitual cuando la respuesta HTTP es un array de 1 fila),\n// 2) un array,\n// 3) { data: [...] }, o\n// 4) { body: [...] } según la configuración/versión del nodo HTTP Request.\nlet row = null;\nif (Array.isArray(raw)) {\n row = raw[0] ?? null;\n} else if (Array.isArray(raw?.data)) {\n row = raw.data[0] ?? null;\n} else if (Array.isArray(raw?.body)) {\n row = raw.body[0] ?? null;\n} else if (raw?.body && typeof raw.body === 'object' && !Array.isArray(raw.body)) {\n row = raw.body;\n} else if (raw && typeof raw === 'object' && ('email' in raw || 'is_active' in raw || 'full_name' in raw)) {\n row = raw;\n}\n\nconst normalizeEmail = (value) => String(value ?? '').trim().toLowerCase();\nconst rowEmail = normalizeEmail(row?.email);\nconst active = row?.is_active === true || String(row?.is_active ?? '').toLowerCase() === 'true';\nconst accessAllowed = Boolean(row && active && rowEmail === req.verifiedEmail);\n\nreturn [{\n json: {\n ...req,\n accessAllowed,\n fullName: row?.full_name || req.userName || req.verifiedEmail,\n canUseTarifario: accessAllowed && (row?.can_use_tarifario === true || String(row?.can_use_tarifario ?? '').toLowerCase() === 'true'),\n accessLookupOk: Boolean(row),\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1040, + 960 + ], + "id": "06aaa862-bfe8-4455-8634-8a319910ef4d", + "name": "Validar acceso" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "b01cd05b-234e-4d8d-a2f0-3f9b83cb857e", + "leftValue": "={{ $json.accessAllowed }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 1264, + 960 + ], + "id": "a23e7196-4260-463b-9a8a-309ee3bed74a", + "name": "Acceso permitido?" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ { ok:false, reply:'No tienes acceso activo al Cotizador Walmart Connect.' } }}", + "options": { + "responseCode": 403 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.4, + "position": [ + 1488, + 1248 + ], + "id": "d77bb6bb-4ed8-4823-bd9a-7409a1cc7b59", + "name": "Responder 403" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_get_my_agent_context", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $node[\"Validar acceso\"].json.auth }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({p_query:$node[\"Validar acceso\"].json.text,p_country_code:$node[\"Validar acceso\"].json.countryCode,p_limit:20}) }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1488, + 848 + ], + "id": "9613bcf5-900b-4c98-b122-e5d7709f4758", + "name": "Contexto Supabase" + }, + { + "parameters": { + "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cotizador_wmc_messages?select=role,content,attachments,created_at&conversation_id=eq.' + encodeURIComponent($node[\"Validar acceso\"].json.conversationId) + '&order=created_at.asc&limit=40' }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $node[\"Validar acceso\"].json.auth }}" + } + ] + }, + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1712, + 848 + ], + "id": "1eb8a5fb-b8b9-4bac-b264-671302a6d743", + "name": "Historial Supabase", + "continueOnFail": true + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_get_my_pending_actions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $node[\"Validar acceso\"].json.auth }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({p_limit:5}) }}", + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1936, + 848 + ], + "id": "31dcf9b8-a708-4c53-95c9-54c40d0ae67d", + "name": "Acciones pendientes", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const req = $node[\"Validar acceso\"].json;\nconst context = $node[\"Contexto Supabase\"].json ?? {};\nconst historyRaw = $node[\"Historial Supabase\"].json;\nconst pendingRaw = $node[\"Acciones pendientes\"].json;\n\nconst history = Array.isArray(historyRaw) ? historyRaw.slice(-30) : [];\nconst pending = Array.isArray(pendingRaw) ? pendingRaw : (Array.isArray(pendingRaw?.items) ? pendingRaw.items : []);\n\nconst system = `Eres el agente interno Cotizador Walmart Connect de GomezLee Marketing (GLM).\nTrabajas EXCLUSIVAMENTE con cotizaciones, presupuestos, tarifario, propuestas, ejecuciones y archivos relacionados con Walmart Connect/GLM. Si te piden algo fuera de este alcance, explica brevemente que no corresponde al Cotizador WMC.\n\nFUENTES Y REGLAS OBLIGATORIAS:\n1) El TARIFARIO SUPABASE incluido en CONTEXTO es la fuente oficial vigente. Nunca inventes una tarifa oficial.\n2) Los HISTÓRICOS son referencia para elementos específicos que no existan en el tarifario. Si debes estimar, usa referencias comparables de la forma más realista posible y marca isEstimated=true, estimationReason y confidence 0..1. Nunca presentes una estimación como tarifa oficial.\n3) Las cotizaciones Walmart Connect salen en USD y también incluyen moneda local usando el tipo de cambio entregado por CONTEXTO.\n4) Conserva la estructura funcional oficial WMC: Personal, Mobiliario - Equipo, Insumos de degustación y/o preparación, Logística e Instalación, Fee de agencia, total, condiciones comerciales, vigencia, observaciones/exclusiones y referencias visuales.\n5) Personal: total = unidades × días × costo unitario. Verifica TODA la matemática, subtotales, total USD y total local antes de responder.\n6) Respeta el país. No traslades precios de un país a otro salvo que el CONTEXTO lo permita. Referencias confirmadas para Guatemala: Impulsadora A capital USD 39/día; Supervisor capital USD 62/día; camisa polo/uniforme estándar USD 20/unidad.\n7) RTM/RTM+ suele implicar arcos, backpanels, mesas degustadoras, alfombras y montaje; cotizaciones de islas suelen concentrarse en impulso/personal, mobiliario y rebranding. No agregues partidas no solicitadas sin aclararlo.\n8) En CAM/multipaís, genera la combinación país × formato requerida; por ejemplo, 5 países × 3 formatos puede implicar 15 cotizaciones/hojas.\n9) Cuando Walmart haya dado el OK y pidan mensualización, usa action=generate_monthly_glm.\n10) Cambios de tarifario: JAMÁS los ejecutes silenciosamente. Primera solicitud -> tariff_change_proposal. Solo después de confirmación explícita del mismo usuario -> confirm_tariff_change con pendingActionId.\n11) Si permissions.canUseTarifario=false, NO propongas ni confirmes modificaciones del tarifario; explica que el usuario no tiene ese permiso.\n12) Si faltan datos esenciales para un archivo confiable, pregunta exactamente lo que falta y usa action=chat.\n13) Los adjuntos se entregan como metadatos y URLs firmadas/temporales. No inventes contenido de un adjunto que no esté disponible como texto en el contexto; si falta su contenido, indícalo claramente y solicita el dato mínimo necesario.\n14) Para referencias visuales o artes, genera prompts específicos, realistas y coherentes con el mobiliario/activación solicitada.\n15) Para propuestas en Google Slides, mantén estilo Walmart Connect: limpio, corporativo, azul marino profundo, títulos simples y visuales grandes, evitando inventar claims o especificaciones no entregadas.\n16) Cuando el usuario pida una imagen directamente, usa action=generate_image.\n17) Devuelve SOLO JSON válido, sin markdown ni texto fuera del JSON.\n\nESQUEMA DE RESPUESTA:\n{\n \"reply\":\"respuesta para el usuario\",\n \"action\":\"chat|generate_quote|generate_proposal|generate_image|generate_monthly_glm|tariff_change_proposal|confirm_tariff_change\",\n \"requiresConfirmation\":false,\n \"pendingAction\":null,\n \"pendingActionId\":null,\n \"quote\":null,\n \"artifact\":null,\n \"warnings\":[],\n \"confidence\":0.0\n}\n\nCuando action=generate_quote, quote debe incluir:\ncountryCode,country,brandSolution,project,advertiser,format,exchangeRate,currencyCode,totalUsd,totalLocal,status,items[],conditions,validity,observations,visualReferences.\nCada item: section,description,units,days,unitCostUsd,totalUsd,unitCostLocal,totalLocal,isEstimated,confidence,estimationReason,metadata.\nvisualReferences puede contener URLs existentes o objetos {title,imagePrompt,imageUrl}. Si el usuario pide una referencia visual nueva para la cotización, incluye un objeto con imagePrompt; el workflow generará esa imagen con el nodo nativo de Gemini y la insertará en Referencias visuales del Google Sheet.\n\nCuando action=generate_proposal:\nartifact={title,subtitle,slides:[{type:\"cover|section|visual|content|thankyou\",title,subtitle,body,imagePrompt,imageUrl}]}\n\nCuando action=generate_image:\nartifact={title,imagePrompt,aspectRatio}\n\nCuando action=generate_monthly_glm:\nartifact={title,sourceQuoteId,monthlyLines,metadata}\n\nCuando action=tariff_change_proposal:\npendingAction={operation:\"insert|update|deactivate\",tariffItemId,item,reason}.`;\n\nconst attachments = (req.attachments || []).map(a => ({\n filename: a.filename || '',\n mediaType: a.mediaType || '',\n url: a.url || '',\n storagePath: a.storagePath || null\n}));\n\nconst payload = {\n currentRequest: req.text,\n countryHint: req.countryCode,\n user: {name:req.fullName,email:req.verifiedEmail},\n permissions: {canUseTarifario:Boolean(req.canUseTarifario)},\n attachments,\n CONTEXTO: context,\n ACCIONES_PENDIENTES: pending,\n HISTORIAL: history.map(h => ({\n role:h.role,\n content:h.content,\n attachments:h.attachments || [],\n createdAt:h.created_at\n }))\n};\n\nconst attachmentLinks = attachments\n .filter(a => a.url)\n .map((a,i) => `Adjunto ${i+1}: ${a.filename || 'archivo'} (${a.mediaType || 'tipo no indicado'})\\nURL: ${a.url}`)\n .join('\\n\\n');\n\nconst geminiPrompt = [\n 'SOLICITUD Y CONTEXTO EN JSON:',\n JSON.stringify(payload),\n attachmentLinks ? '\\nENLACES DE ADJUNTOS PARA URL CONTEXT:\\n' + attachmentLinks : ''\n].join('\\n\\n');\n\nreturn [{json:{...req,system,geminiPrompt,model:\"gemini-3.7-flash\"}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2160, + 848 + ], + "id": "1827aa22-7d04-40a9-b0a3-97e6bfa800e5", + "name": "Construir Gemini" + }, + { + "parameters": { + "jsCode": "const req = $node[\"Construir Gemini\"].json;\nconst raw = $json ?? {};\n\nlet parsed = null;\nlet text = '';\n\n// El nodo nativo de Gemini puede devolver JSON ya simplificado o contenido en parts.\nif (raw && typeof raw === 'object' && typeof raw.action === 'string') {\n parsed = raw;\n try { text = JSON.stringify(raw); } catch {}\n}\n\nif (!parsed) {\n if (typeof raw.mergedResponse === 'string') {\n text = raw.mergedResponse;\n } else if (raw.content?.parts && Array.isArray(raw.content.parts)) {\n text = raw.content.parts.map(p => p?.text || '').join('');\n } else if (raw.candidates?.[0]?.content?.parts && Array.isArray(raw.candidates[0].content.parts)) {\n text = raw.candidates[0].content.parts.map(p => p?.text || '').join('');\n } else if (typeof raw.text === 'string') {\n text = raw.text;\n } else if (typeof raw.output === 'string') {\n text = raw.output;\n } else if (typeof raw.response === 'string') {\n text = raw.response;\n } else if (raw.output && typeof raw.output.text === 'string') {\n text = raw.output.text;\n }\n\n try {\n const cleaned = String(text || '').trim()\n .replace(/^```(?:json)?\\s*/i,'')\n .replace(/\\s*```$/,'');\n parsed = JSON.parse(cleaned);\n } catch {\n parsed = {\n reply:'No pude interpretar de forma segura la respuesta del modelo. Intenta nuevamente.',\n action:'chat',\n warnings:['La respuesta de Gemini no fue JSON válido.'],\n confidence:0\n };\n }\n}\n\nconst allowed = new Set([\n 'chat','generate_quote','generate_proposal','generate_image',\n 'generate_monthly_glm','tariff_change_proposal','confirm_tariff_change'\n]);\nlet action = allowed.has(parsed?.action) ? parsed.action : 'chat';\n\n// Seguridad adicional: usuarios sin permiso de Tarifario no pueden pedir mutaciones.\nif (!req.canUseTarifario && ['tariff_change_proposal','confirm_tariff_change'].includes(action)) {\n action = 'chat';\n parsed.reply = 'No tienes habilitado el permiso para modificar el Tarifario.';\n parsed.pendingAction = null;\n parsed.pendingActionId = null;\n parsed.requiresConfirmation = false;\n}\n\nconst normalized = {\n reply: String(parsed?.reply || '').trim() || 'Listo.',\n action,\n requiresConfirmation: Boolean(parsed?.requiresConfirmation || action === 'tariff_change_proposal'),\n pendingAction: parsed?.pendingAction ?? null,\n pendingActionId: parsed?.pendingActionId ?? null,\n quote: parsed?.quote ?? null,\n artifact: parsed?.artifact ?? null,\n warnings: Array.isArray(parsed?.warnings) ? parsed.warnings.map(String).slice(0,10) : [],\n confidence: Number.isFinite(Number(parsed?.confidence)) ? Math.max(0,Math.min(1,Number(parsed.confidence))) : null,\n model: 'gemini-3.7-flash'\n};\n\nreturn [{json:{...req,agent:normalized,rawModelText:text || JSON.stringify(parsed),durationMs:Date.now()-req.startedAt}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3008, + 816 + ], + "id": "c8f7bd9a-69a7-4a32-a216-2a78c999cd06", + "name": "Interpretar Gemini" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_process_my_agent_action", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({p_action:$json.agent,p_conversation_id:$json.conversationId,p_display_name:$json.fullName,p_session_id:$json.appSessionId}) }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3232, + 816 + ], + "id": "3fe0a765-1119-45a3-9f43-082d01b895a3", + "name": "Procesar acción sensible", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const x = $node[\"Interpretar Gemini\"].json;\nreturn [{json:{\n action:x.agent.action,\n quote:x.agent.quote,\n artifact:x.agent.artifact,\n user:{id:x.userId,email:x.verifiedEmail,name:x.fullName},\n conversationId:x.conversationId,\n appSessionId:x.appSessionId,\n model:x.agent.model,\n auth:x.auth\n}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3456, + 816 + ], + "id": "b3f12a97-75fa-4e33-afca-eb0801c55686", + "name": "Preparar artefacto" + }, + { + "parameters": { + "method": "POST", + "url": "https://agenteit.digitalcompass.agency/webhook/cotizador-wmc-artifacts", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({action:$json.action,quote:$json.quote,artifact:$json.artifact,user:$json.user,conversationId:$json.conversationId,appSessionId:$json.appSessionId,model:$json.model}) }}", + "options": { + "timeout": 180000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3664, + 816 + ], + "id": "ab187b90-2b3b-40d4-8aed-1ebdf2d7f000", + "name": "Generar artefacto", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const x = $node[\"Interpretar Gemini\"].json;\nlet actionResult = {};\nlet artifactResult = {};\ntry { actionResult = $node[\"Procesar acción sensible\"].json ?? {}; } catch {}\ntry { artifactResult = $node[\"Generar artefacto\"].json ?? {}; } catch {}\nconst out = {...x.agent};\nif (actionResult.pendingActionId) out.pendingActionId = actionResult.pendingActionId;\nif (typeof actionResult.requiresConfirmation === 'boolean') out.requiresConfirmation = actionResult.requiresConfirmation;\nif (actionResult.ok === false && actionResult.error) {\n out.warnings = [...(out.warnings||[]), String(actionResult.error)];\n out.reply += `\\n\\n⚠️ ${actionResult.error}`;\n}\nif (artifactResult?.artifactUrl) {\n out.artifactUrl = artifactResult.artifactUrl;\n out.artifactType = artifactResult.artifactType || null;\n out.quoteId = artifactResult.quoteId || null;\n} else if (['generate_quote','generate_proposal','generate_image','generate_monthly_glm'].includes(out.action)) {\n const err = artifactResult?.error || artifactResult?.message || 'No se pudo generar el archivo solicitado.';\n out.warnings = [...(out.warnings||[]), String(err)];\n out.reply += `\\n\\n⚠️ ${err}`;\n}\nreturn [{json:{...x, final:{ok:true,...out}}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3888, + 816 + ], + "id": "556318da-08cf-43be-8f1e-436ffa0a0734", + "name": "Respuesta final" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_log_my_agent_run", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({p_conversation_id:$json.conversationId,p_prompt:$json.text,p_response:$json.final.reply,p_action:$json.final.action,p_model:$json.final.model,p_duration_ms:$json.durationMs,p_status:'ok',p_metadata:{warnings:$json.final.warnings||[],confidence:$json.final.confidence,hasArtifact:!!$json.final.artifactUrl}}) }}", + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 4112, + 816 + ], + "id": "541f05ec-2144-4e2f-ad15-d43ecada68cc", + "name": "Registrar ejecución", + "continueOnFail": true + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ $node[\"Respuesta final\"].json.final }}", + "options": { + "responseCode": 200 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.4, + "position": [ + 4336, + 816 + ], + "id": "b4be408b-6ae1-417e-8503-0b39417f9ef7", + "name": "Responder al chat" + }, + { + "parameters": { + "jsCode": "// Configuración local del workflow: NO usa variables de entorno.\nreturn [{json:{\n ...$json,\n config:{\n supabaseUrl:\"https://dbit.digitalcompass.agency\",\n supabaseAnonKey:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE\",\n artifactsWebhookUrl:\"https://agenteit.digitalcompass.agency/webhook/cotizador-wmc-artifacts\"\n }\n}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -704, + 1040 + ], + "id": "844cbb4c-45ba-43d1-8191-95b10d47b3f4", + "name": "Configuración - sin ENV" + }, + { + "parameters": { + "promptType": "define", + "text": "={{ $json.system + '\\n\\n' + $json.geminiPrompt }}", + "batching": {} + }, + "type": "@n8n/n8n-nodes-langchain.chainLlm", + "typeVersion": 1.7, + "position": [ + 2384, + 848 + ], + "id": "62fcafb8-2581-4d7b-9f8a-8ba809972fe2", + "name": "Gemini - Basic LLM Chain", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1500 + }, + { + "parameters": { + "modelName": "models/gemini-3.7-flash", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", + "typeVersion": 1.1, + "position": [ + 2384, + 1056 + ], + "id": "682eb3ff-4f3c-42df-989a-df33ac0b2f3b", + "name": "Gemini 3.7 Flash - Chat Model", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + } + ], + "connections": { + "Webhook - Chat": { + "main": [ + [ + { + "node": "Configuración - sin ENV", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalizar solicitud": { + "main": [ + [ + { + "node": "Supabase - validar JWT", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - validar JWT": { + "main": [ + [ + { + "node": "Validar identidad", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar identidad": { + "main": [ + [ + { + "node": "JWT válido?", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWT válido?": { + "main": [ + [ + { + "node": "Supabase - consultar acceso", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Responder 401", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - consultar acceso": { + "main": [ + [ + { + "node": "Validar acceso", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar acceso": { + "main": [ + [ + { + "node": "Acceso permitido?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Acceso permitido?": { + "main": [ + [ + { + "node": "Contexto Supabase", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Responder 403", + "type": "main", + "index": 0 + } + ] + ] + }, + "Contexto Supabase": { + "main": [ + [ + { + "node": "Historial Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, + "Historial Supabase": { + "main": [ + [ + { + "node": "Acciones pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Acciones pendientes": { + "main": [ + [ + { + "node": "Construir Gemini", + "type": "main", + "index": 0 + } + ] + ] + }, + "Construir Gemini": { + "main": [ + [ + { + "node": "Gemini - Basic LLM Chain", + "type": "main", + "index": 0 + } + ] + ] + }, + "Interpretar Gemini": { + "main": [ + [ + { + "node": "Procesar acción sensible", + "type": "main", + "index": 0 + } + ] + ] + }, + "Procesar acción sensible": { + "main": [ + [ + { + "node": "Preparar artefacto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar artefacto": { + "main": [ + [ + { + "node": "Generar artefacto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Generar artefacto": { + "main": [ + [ + { + "node": "Respuesta final", + "type": "main", + "index": 0 + } + ] + ] + }, + "Respuesta final": { + "main": [ + [ + { + "node": "Registrar ejecución", + "type": "main", + "index": 0 + } + ] + ] + }, + "Registrar ejecución": { + "main": [ + [ + { + "node": "Responder al chat", + "type": "main", + "index": 0 + } + ] + ] + }, + "Configuración - sin ENV": { + "main": [ + [ + { + "node": "Normalizar solicitud", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Basic LLM Chain": { + "main": [ + [ + { + "node": "Interpretar Gemini", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini 3.7 Flash - Chat Model": { + "ai_languageModel": [ + [ + { + "node": "Gemini - Basic LLM Chain", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "staticData": null, + "meta": null, + "versionId": "cf8ad34d-f107-4a19-a585-54d3a48e43b5", + "activeVersionId": "cf8ad34d-f107-4a19-a585-54d3a48e43b5", + "versionCounter": 21, + "triggerCount": 1, + "shared": [ + { + "updatedAt": "2026-08-22T14:59:56.571Z", + "createdAt": "2026-08-22T14:59:56.571Z", + "role": "workflow:owner", + "workflowId": "rtnO23gHlJfj45pA", + "projectId": "PJpTANzTXIFibWsW", + "project": { + "updatedAt": "2026-04-22T14:25:09.686Z", + "createdAt": "2026-04-22T14:22:54.790Z", + "id": "PJpTANzTXIFibWsW", + "name": "Isaac Aracena ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + } + ], + "tags": [], + "activeVersion": { + "updatedAt": "2026-08-22T19:52:25.000Z", + "createdAt": "2026-08-22T19:52:21.360Z", + "versionId": "cf8ad34d-f107-4a19-a585-54d3a48e43b5", + "workflowId": "rtnO23gHlJfj45pA", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "cotizador-wmc-chat", + "responseMode": "responseNode", + "options": { + "allowedOrigins": "*" + } + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -928, + 1040 + ], + "id": "e7623673-c683-4b33-a76e-231a0efbf5e0", + "name": "Webhook - Chat", + "webhookId": "23c9a253-ba3d-47f0-85ca-21b3fde221c7" + }, + { + "parameters": { + "jsCode": "const body = $json.body ?? {};\nconst headers = $json.headers ?? {};\nconst clean = (v, max = 10000) => typeof v === 'string' ? v.trim().slice(0, max) : '';\nconst auth = clean(headers.authorization || headers.Authorization, 12000);\nconst text = clean(body.text, 12000);\nconst conversationId = clean(body.conversationId, 200);\nconst messageId = clean(body.messageId, 200);\nconst userEmail = clean(body.userEmail, 320).toLowerCase();\nconst userName = clean(body.userName, 200);\nconst appSessionId = clean(body.appSessionId, 250) || `backend-${Date.now()}`;\nconst sentAt = clean(body.sentAt, 100) || new Date().toISOString();\nconst attachments = Array.isArray(body.attachments) ? body.attachments.slice(0, 8).map((a) => ({\n filename: clean(a?.filename, 180), mediaType: clean(a?.mediaType, 120), url: clean(a?.url, 6000), storagePath: clean(a?.storagePath, 1000) || null,\n})) : [];\nif (!auth.startsWith('Bearer ')) throw new Error('Falta Authorization Bearer de Supabase.');\nif (!conversationId || !messageId) throw new Error('Faltan conversationId/messageId.');\nif (!text && attachments.length === 0) throw new Error('La solicitud no contiene texto ni archivos.');\nconst countryMap = [\n ['GT','guatemala'],['CR','costa rica'],['SV','el salvador'],['SV','salvador'],['HN','honduras'],['NI','nicaragua']\n];\nconst low = text.toLowerCase();\nlet countryCode = null;\nfor (const [code, term] of countryMap) if (low.includes(term)) { countryCode = code; break; }\nreturn [{json:{auth,text,conversationId,messageId,userEmail,userName,appSessionId,sentAt,attachments,countryCode,startedAt:Date.now()}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -480, + 1040 + ], + "id": "382e3c39-c66e-433a-b96f-a8c2336c8081", + "name": "Normalizar solicitud" + }, + { + "parameters": { + "url": "=https://dbit.digitalcompass.agency/auth/v1/user", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + } + ] + }, + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 0, + 1040 + ], + "id": "81c45b99-29e7-4485-910c-d8cd25218e30", + "name": "Supabase - validar JWT", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const req = $node[\"Normalizar solicitud\"].json;\nconst authUser = $json ?? {};\nconst email = String(authUser.email ?? '').trim().toLowerCase();\nconst id = String(authUser.id ?? '').trim();\nconst valid = Boolean(id && email && email.endsWith('@gomezleemarketing.com') && (!req.userEmail || req.userEmail === email));\nreturn [{json:{...req, userId:id, verifiedEmail:email, jwtValid:valid, jwtError: valid ? null : 'Sesión inválida o correo no coincide.'}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 384, + 1040 + ], + "id": "424dd516-bbb9-4c3e-938b-81de5b71f855", + "name": "Validar identidad" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "0bfb8fea-20b0-4cf3-8e89-9160c914cc8e", + "leftValue": "={{ $json.jwtValid }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 608, + 1040 + ], + "id": "f8a57cf7-4949-435e-b18b-0db46b9801b4", + "name": "JWT válido?" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ { ok:false, reply:'Tu sesión no es válida. Vuelve a iniciar sesión.' } }}", + "options": { + "responseCode": 401 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.4, + "position": [ + 832, + 1280 + ], + "id": "a4a57411-c41e-4fd2-a38f-670b024f8931", + "name": "Responder 401" + }, + { + "parameters": { + "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cotizador_wmc_access?select=email,full_name,is_active,can_use_tarifario&email=eq.' + encodeURIComponent($json.verifiedEmail) + '&is_active=eq.true' + '&limit=1' }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + } + ] + }, + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 832, + 960 + ], + "id": "ce76b3c8-6ac9-4c65-8c14-5872165e696a", + "name": "Supabase - consultar acceso", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const req = $node[\"Validar identidad\"].json;\nconst raw = $json ?? {};\n\n// n8n puede entregar la respuesta de PostgREST de varias formas:\n// 1) una fila directamente como $json (caso habitual cuando la respuesta HTTP es un array de 1 fila),\n// 2) un array,\n// 3) { data: [...] }, o\n// 4) { body: [...] } según la configuración/versión del nodo HTTP Request.\nlet row = null;\nif (Array.isArray(raw)) {\n row = raw[0] ?? null;\n} else if (Array.isArray(raw?.data)) {\n row = raw.data[0] ?? null;\n} else if (Array.isArray(raw?.body)) {\n row = raw.body[0] ?? null;\n} else if (raw?.body && typeof raw.body === 'object' && !Array.isArray(raw.body)) {\n row = raw.body;\n} else if (raw && typeof raw === 'object' && ('email' in raw || 'is_active' in raw || 'full_name' in raw)) {\n row = raw;\n}\n\nconst normalizeEmail = (value) => String(value ?? '').trim().toLowerCase();\nconst rowEmail = normalizeEmail(row?.email);\nconst active = row?.is_active === true || String(row?.is_active ?? '').toLowerCase() === 'true';\nconst accessAllowed = Boolean(row && active && rowEmail === req.verifiedEmail);\n\nreturn [{\n json: {\n ...req,\n accessAllowed,\n fullName: row?.full_name || req.userName || req.verifiedEmail,\n canUseTarifario: accessAllowed && (row?.can_use_tarifario === true || String(row?.can_use_tarifario ?? '').toLowerCase() === 'true'),\n accessLookupOk: Boolean(row),\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1040, + 960 + ], + "id": "06aaa862-bfe8-4455-8634-8a319910ef4d", + "name": "Validar acceso" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "b01cd05b-234e-4d8d-a2f0-3f9b83cb857e", + "leftValue": "={{ $json.accessAllowed }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 1264, + 960 + ], + "id": "a23e7196-4260-463b-9a8a-309ee3bed74a", + "name": "Acceso permitido?" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ { ok:false, reply:'No tienes acceso activo al Cotizador Walmart Connect.' } }}", + "options": { + "responseCode": 403 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.4, + "position": [ + 1488, + 1248 + ], + "id": "d77bb6bb-4ed8-4823-bd9a-7409a1cc7b59", + "name": "Responder 403" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_get_my_agent_context", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $node[\"Validar acceso\"].json.auth }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({p_query:$node[\"Validar acceso\"].json.text,p_country_code:$node[\"Validar acceso\"].json.countryCode,p_limit:20}) }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1488, + 848 + ], + "id": "9613bcf5-900b-4c98-b122-e5d7709f4758", + "name": "Contexto Supabase" + }, + { + "parameters": { + "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cotizador_wmc_messages?select=role,content,attachments,created_at&conversation_id=eq.' + encodeURIComponent($node[\"Validar acceso\"].json.conversationId) + '&order=created_at.asc&limit=40' }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $node[\"Validar acceso\"].json.auth }}" + } + ] + }, + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1712, + 848 + ], + "id": "1eb8a5fb-b8b9-4bac-b264-671302a6d743", + "name": "Historial Supabase", + "continueOnFail": true + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_get_my_pending_actions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $node[\"Validar acceso\"].json.auth }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({p_limit:5}) }}", + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1936, + 848 + ], + "id": "31dcf9b8-a708-4c53-95c9-54c40d0ae67d", + "name": "Acciones pendientes", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const req = $node[\"Validar acceso\"].json;\nconst context = $node[\"Contexto Supabase\"].json ?? {};\nconst historyRaw = $node[\"Historial Supabase\"].json;\nconst pendingRaw = $node[\"Acciones pendientes\"].json;\n\nconst history = Array.isArray(historyRaw) ? historyRaw.slice(-30) : [];\nconst pending = Array.isArray(pendingRaw) ? pendingRaw : (Array.isArray(pendingRaw?.items) ? pendingRaw.items : []);\n\nconst system = `Eres el agente interno Cotizador Walmart Connect de GomezLee Marketing (GLM).\nTrabajas EXCLUSIVAMENTE con cotizaciones, presupuestos, tarifario, propuestas, ejecuciones y archivos relacionados con Walmart Connect/GLM. Si te piden algo fuera de este alcance, explica brevemente que no corresponde al Cotizador WMC.\n\nFUENTES Y REGLAS OBLIGATORIAS:\n1) El TARIFARIO SUPABASE incluido en CONTEXTO es la fuente oficial vigente. Nunca inventes una tarifa oficial.\n2) Los HISTÓRICOS son referencia para elementos específicos que no existan en el tarifario. Si debes estimar, usa referencias comparables de la forma más realista posible y marca isEstimated=true, estimationReason y confidence 0..1. Nunca presentes una estimación como tarifa oficial.\n3) Las cotizaciones Walmart Connect salen en USD y también incluyen moneda local usando el tipo de cambio entregado por CONTEXTO.\n4) Conserva la estructura funcional oficial WMC: Personal, Mobiliario - Equipo, Insumos de degustación y/o preparación, Logística e Instalación, Fee de agencia, total, condiciones comerciales, vigencia, observaciones/exclusiones y referencias visuales.\n5) Personal: total = unidades × días × costo unitario. Verifica TODA la matemática, subtotales, total USD y total local antes de responder.\n6) Respeta el país. No traslades precios de un país a otro salvo que el CONTEXTO lo permita. Referencias confirmadas para Guatemala: Impulsadora A capital USD 39/día; Supervisor capital USD 62/día; camisa polo/uniforme estándar USD 20/unidad.\n7) RTM/RTM+ suele implicar arcos, backpanels, mesas degustadoras, alfombras y montaje; cotizaciones de islas suelen concentrarse en impulso/personal, mobiliario y rebranding. No agregues partidas no solicitadas sin aclararlo.\n8) En CAM/multipaís, genera la combinación país × formato requerida; por ejemplo, 5 países × 3 formatos puede implicar 15 cotizaciones/hojas.\n9) Cuando Walmart haya dado el OK y pidan mensualización, usa action=generate_monthly_glm.\n10) Cambios de tarifario: JAMÁS los ejecutes silenciosamente. Primera solicitud -> tariff_change_proposal. Solo después de confirmación explícita del mismo usuario -> confirm_tariff_change con pendingActionId.\n11) Si permissions.canUseTarifario=false, NO propongas ni confirmes modificaciones del tarifario; explica que el usuario no tiene ese permiso.\n12) Si faltan datos esenciales para un archivo confiable, pregunta exactamente lo que falta y usa action=chat.\n13) Los adjuntos se entregan como metadatos y URLs firmadas/temporales. No inventes contenido de un adjunto que no esté disponible como texto en el contexto; si falta su contenido, indícalo claramente y solicita el dato mínimo necesario.\n14) Para referencias visuales o artes, genera prompts específicos, realistas y coherentes con el mobiliario/activación solicitada.\n15) Para propuestas en Google Slides, mantén estilo Walmart Connect: limpio, corporativo, azul marino profundo, títulos simples y visuales grandes, evitando inventar claims o especificaciones no entregadas.\n16) Cuando el usuario pida una imagen directamente, usa action=generate_image.\n17) Devuelve SOLO JSON válido, sin markdown ni texto fuera del JSON.\n\nESQUEMA DE RESPUESTA:\n{\n \"reply\":\"respuesta para el usuario\",\n \"action\":\"chat|generate_quote|generate_proposal|generate_image|generate_monthly_glm|tariff_change_proposal|confirm_tariff_change\",\n \"requiresConfirmation\":false,\n \"pendingAction\":null,\n \"pendingActionId\":null,\n \"quote\":null,\n \"artifact\":null,\n \"warnings\":[],\n \"confidence\":0.0\n}\n\nCuando action=generate_quote, quote debe incluir:\ncountryCode,country,brandSolution,project,advertiser,format,exchangeRate,currencyCode,totalUsd,totalLocal,status,items[],conditions,validity,observations,visualReferences.\nCada item: section,description,units,days,unitCostUsd,totalUsd,unitCostLocal,totalLocal,isEstimated,confidence,estimationReason,metadata.\nvisualReferences puede contener URLs existentes o objetos {title,imagePrompt,imageUrl}. Si el usuario pide una referencia visual nueva para la cotización, incluye un objeto con imagePrompt; el workflow generará esa imagen con el nodo nativo de Gemini y la insertará en Referencias visuales del Google Sheet.\n\nCuando action=generate_proposal:\nartifact={title,subtitle,slides:[{type:\"cover|section|visual|content|thankyou\",title,subtitle,body,imagePrompt,imageUrl}]}\n\nCuando action=generate_image:\nartifact={title,imagePrompt,aspectRatio}\n\nCuando action=generate_monthly_glm:\nartifact={title,sourceQuoteId,monthlyLines,metadata}\n\nCuando action=tariff_change_proposal:\npendingAction={operation:\"insert|update|deactivate\",tariffItemId,item,reason}.`;\n\nconst attachments = (req.attachments || []).map(a => ({\n filename: a.filename || '',\n mediaType: a.mediaType || '',\n url: a.url || '',\n storagePath: a.storagePath || null\n}));\n\nconst payload = {\n currentRequest: req.text,\n countryHint: req.countryCode,\n user: {name:req.fullName,email:req.verifiedEmail},\n permissions: {canUseTarifario:Boolean(req.canUseTarifario)},\n attachments,\n CONTEXTO: context,\n ACCIONES_PENDIENTES: pending,\n HISTORIAL: history.map(h => ({\n role:h.role,\n content:h.content,\n attachments:h.attachments || [],\n createdAt:h.created_at\n }))\n};\n\nconst attachmentLinks = attachments\n .filter(a => a.url)\n .map((a,i) => `Adjunto ${i+1}: ${a.filename || 'archivo'} (${a.mediaType || 'tipo no indicado'})\\nURL: ${a.url}`)\n .join('\\n\\n');\n\nconst geminiPrompt = [\n 'SOLICITUD Y CONTEXTO EN JSON:',\n JSON.stringify(payload),\n attachmentLinks ? '\\nENLACES DE ADJUNTOS PARA URL CONTEXT:\\n' + attachmentLinks : ''\n].join('\\n\\n');\n\nreturn [{json:{...req,system,geminiPrompt,model:\"gemini-3.7-flash\"}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2160, + 848 + ], + "id": "1827aa22-7d04-40a9-b0a3-97e6bfa800e5", + "name": "Construir Gemini" + }, + { + "parameters": { + "jsCode": "const req = $node[\"Construir Gemini\"].json;\nconst raw = $json ?? {};\n\nlet parsed = null;\nlet text = '';\n\n// El nodo nativo de Gemini puede devolver JSON ya simplificado o contenido en parts.\nif (raw && typeof raw === 'object' && typeof raw.action === 'string') {\n parsed = raw;\n try { text = JSON.stringify(raw); } catch {}\n}\n\nif (!parsed) {\n if (typeof raw.mergedResponse === 'string') {\n text = raw.mergedResponse;\n } else if (raw.content?.parts && Array.isArray(raw.content.parts)) {\n text = raw.content.parts.map(p => p?.text || '').join('');\n } else if (raw.candidates?.[0]?.content?.parts && Array.isArray(raw.candidates[0].content.parts)) {\n text = raw.candidates[0].content.parts.map(p => p?.text || '').join('');\n } else if (typeof raw.text === 'string') {\n text = raw.text;\n } else if (typeof raw.output === 'string') {\n text = raw.output;\n } else if (typeof raw.response === 'string') {\n text = raw.response;\n } else if (raw.output && typeof raw.output.text === 'string') {\n text = raw.output.text;\n }\n\n try {\n const cleaned = String(text || '').trim()\n .replace(/^```(?:json)?\\s*/i,'')\n .replace(/\\s*```$/,'');\n parsed = JSON.parse(cleaned);\n } catch {\n parsed = {\n reply:'No pude interpretar de forma segura la respuesta del modelo. Intenta nuevamente.',\n action:'chat',\n warnings:['La respuesta de Gemini no fue JSON válido.'],\n confidence:0\n };\n }\n}\n\nconst allowed = new Set([\n 'chat','generate_quote','generate_proposal','generate_image',\n 'generate_monthly_glm','tariff_change_proposal','confirm_tariff_change'\n]);\nlet action = allowed.has(parsed?.action) ? parsed.action : 'chat';\n\n// Seguridad adicional: usuarios sin permiso de Tarifario no pueden pedir mutaciones.\nif (!req.canUseTarifario && ['tariff_change_proposal','confirm_tariff_change'].includes(action)) {\n action = 'chat';\n parsed.reply = 'No tienes habilitado el permiso para modificar el Tarifario.';\n parsed.pendingAction = null;\n parsed.pendingActionId = null;\n parsed.requiresConfirmation = false;\n}\n\nconst normalized = {\n reply: String(parsed?.reply || '').trim() || 'Listo.',\n action,\n requiresConfirmation: Boolean(parsed?.requiresConfirmation || action === 'tariff_change_proposal'),\n pendingAction: parsed?.pendingAction ?? null,\n pendingActionId: parsed?.pendingActionId ?? null,\n quote: parsed?.quote ?? null,\n artifact: parsed?.artifact ?? null,\n warnings: Array.isArray(parsed?.warnings) ? parsed.warnings.map(String).slice(0,10) : [],\n confidence: Number.isFinite(Number(parsed?.confidence)) ? Math.max(0,Math.min(1,Number(parsed.confidence))) : null,\n model: 'gemini-3.7-flash'\n};\n\nreturn [{json:{...req,agent:normalized,rawModelText:text || JSON.stringify(parsed),durationMs:Date.now()-req.startedAt}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3008, + 816 + ], + "id": "c8f7bd9a-69a7-4a32-a216-2a78c999cd06", + "name": "Interpretar Gemini" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_process_my_agent_action", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({p_action:$json.agent,p_conversation_id:$json.conversationId,p_display_name:$json.fullName,p_session_id:$json.appSessionId}) }}", + "options": { + "timeout": 30000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3232, + 816 + ], + "id": "3fe0a765-1119-45a3-9f43-082d01b895a3", + "name": "Procesar acción sensible", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const x = $node[\"Interpretar Gemini\"].json;\nreturn [{json:{\n action:x.agent.action,\n quote:x.agent.quote,\n artifact:x.agent.artifact,\n user:{id:x.userId,email:x.verifiedEmail,name:x.fullName},\n conversationId:x.conversationId,\n appSessionId:x.appSessionId,\n model:x.agent.model,\n auth:x.auth\n}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3456, + 816 + ], + "id": "b3f12a97-75fa-4e33-afca-eb0801c55686", + "name": "Preparar artefacto" + }, + { + "parameters": { + "method": "POST", + "url": "https://agenteit.digitalcompass.agency/webhook/cotizador-wmc-artifacts", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({action:$json.action,quote:$json.quote,artifact:$json.artifact,user:$json.user,conversationId:$json.conversationId,appSessionId:$json.appSessionId,model:$json.model}) }}", + "options": { + "timeout": 180000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3664, + 816 + ], + "id": "ab187b90-2b3b-40d4-8aed-1ebdf2d7f000", + "name": "Generar artefacto", + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const x = $node[\"Interpretar Gemini\"].json;\nlet actionResult = {};\nlet artifactResult = {};\ntry { actionResult = $node[\"Procesar acción sensible\"].json ?? {}; } catch {}\ntry { artifactResult = $node[\"Generar artefacto\"].json ?? {}; } catch {}\nconst out = {...x.agent};\nif (actionResult.pendingActionId) out.pendingActionId = actionResult.pendingActionId;\nif (typeof actionResult.requiresConfirmation === 'boolean') out.requiresConfirmation = actionResult.requiresConfirmation;\nif (actionResult.ok === false && actionResult.error) {\n out.warnings = [...(out.warnings||[]), String(actionResult.error)];\n out.reply += `\\n\\n⚠️ ${actionResult.error}`;\n}\nif (artifactResult?.artifactUrl) {\n out.artifactUrl = artifactResult.artifactUrl;\n out.artifactType = artifactResult.artifactType || null;\n out.quoteId = artifactResult.quoteId || null;\n} else if (['generate_quote','generate_proposal','generate_image','generate_monthly_glm'].includes(out.action)) {\n const err = artifactResult?.error || artifactResult?.message || 'No se pudo generar el archivo solicitado.';\n out.warnings = [...(out.warnings||[]), String(err)];\n out.reply += `\\n\\n⚠️ ${err}`;\n}\nreturn [{json:{...x, final:{ok:true,...out}}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3888, + 816 + ], + "id": "556318da-08cf-43be-8f1e-436ffa0a0734", + "name": "Respuesta final" + }, + { + "parameters": { + "method": "POST", + "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_log_my_agent_run", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "apikey", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" + }, + { + "name": "Authorization", + "value": "={{ $json.auth }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({p_conversation_id:$json.conversationId,p_prompt:$json.text,p_response:$json.final.reply,p_action:$json.final.action,p_model:$json.final.model,p_duration_ms:$json.durationMs,p_status:'ok',p_metadata:{warnings:$json.final.warnings||[],confidence:$json.final.confidence,hasArtifact:!!$json.final.artifactUrl}}) }}", + "options": { + "timeout": 20000 + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 4112, + 816 + ], + "id": "541f05ec-2144-4e2f-ad15-d43ecada68cc", + "name": "Registrar ejecución", + "continueOnFail": true + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ $node[\"Respuesta final\"].json.final }}", + "options": { + "responseCode": 200 + } + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.4, + "position": [ + 4336, + 816 + ], + "id": "b4be408b-6ae1-417e-8503-0b39417f9ef7", + "name": "Responder al chat" + }, + { + "parameters": { + "jsCode": "// Configuración local del workflow: NO usa variables de entorno.\nreturn [{json:{\n ...$json,\n config:{\n supabaseUrl:\"https://dbit.digitalcompass.agency\",\n supabaseAnonKey:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE\",\n artifactsWebhookUrl:\"https://agenteit.digitalcompass.agency/webhook/cotizador-wmc-artifacts\"\n }\n}}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -704, + 1040 + ], + "id": "844cbb4c-45ba-43d1-8191-95b10d47b3f4", + "name": "Configuración - sin ENV" + }, + { + "parameters": { + "promptType": "define", + "text": "={{ $json.system + '\\n\\n' + $json.geminiPrompt }}", + "batching": {} + }, + "type": "@n8n/n8n-nodes-langchain.chainLlm", + "typeVersion": 1.7, + "position": [ + 2384, + 848 + ], + "id": "62fcafb8-2581-4d7b-9f8a-8ba809972fe2", + "name": "Gemini - Basic LLM Chain", + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 1500 + }, + { + "parameters": { + "modelName": "models/gemini-3.7-flash", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", + "typeVersion": 1.1, + "position": [ + 2384, + 1056 + ], + "id": "682eb3ff-4f3c-42df-989a-df33ac0b2f3b", + "name": "Gemini 3.7 Flash - Chat Model", + "credentials": { + "googlePalmApi": { + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" + } + } + } + ], + "connections": { + "Webhook - Chat": { + "main": [ + [ + { + "node": "Configuración - sin ENV", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalizar solicitud": { + "main": [ + [ + { + "node": "Supabase - validar JWT", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - validar JWT": { + "main": [ + [ + { + "node": "Validar identidad", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar identidad": { + "main": [ + [ + { + "node": "JWT válido?", + "type": "main", + "index": 0 + } + ] + ] + }, + "JWT válido?": { + "main": [ + [ + { + "node": "Supabase - consultar acceso", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Responder 401", + "type": "main", + "index": 0 + } + ] + ] + }, + "Supabase - consultar acceso": { + "main": [ + [ + { + "node": "Validar acceso", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validar acceso": { + "main": [ + [ + { + "node": "Acceso permitido?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Acceso permitido?": { + "main": [ + [ + { + "node": "Contexto Supabase", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Responder 403", + "type": "main", + "index": 0 + } + ] + ] + }, + "Contexto Supabase": { + "main": [ + [ + { + "node": "Historial Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, + "Historial Supabase": { + "main": [ + [ + { + "node": "Acciones pendientes", + "type": "main", + "index": 0 + } + ] + ] + }, + "Acciones pendientes": { + "main": [ + [ + { + "node": "Construir Gemini", + "type": "main", + "index": 0 + } + ] + ] + }, + "Construir Gemini": { + "main": [ + [ + { + "node": "Gemini - Basic LLM Chain", + "type": "main", + "index": 0 + } + ] + ] + }, + "Interpretar Gemini": { + "main": [ + [ + { + "node": "Procesar acción sensible", + "type": "main", + "index": 0 + } + ] + ] + }, + "Procesar acción sensible": { + "main": [ + [ + { + "node": "Preparar artefacto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Preparar artefacto": { + "main": [ + [ + { + "node": "Generar artefacto", + "type": "main", + "index": 0 + } + ] + ] + }, + "Generar artefacto": { + "main": [ + [ + { + "node": "Respuesta final", + "type": "main", + "index": 0 + } + ] + ] + }, + "Respuesta final": { + "main": [ + [ + { + "node": "Registrar ejecución", + "type": "main", + "index": 0 + } + ] + ] + }, + "Registrar ejecución": { + "main": [ + [ + { + "node": "Responder al chat", + "type": "main", + "index": 0 + } + ] + ] + }, + "Configuración - sin ENV": { + "main": [ + [ + { + "node": "Normalizar solicitud", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini - Basic LLM Chain": { + "main": [ + [ + { + "node": "Interpretar Gemini", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gemini 3.7 Flash - Chat Model": { + "ai_languageModel": [ + [ + { + "node": "Gemini - Basic LLM Chain", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + } + }, + "authors": "Isaac Aracena", + "name": "Version cf8ad34d", + "description": "", + "autosaved": true, + "workflowPublishHistory": [ + { + "createdAt": "2026-08-22T19:52:25.888Z", + "id": 5145, + "workflowId": "rtnO23gHlJfj45pA", + "versionId": "cf8ad34d-f107-4a19-a585-54d3a48e43b5", + "event": "activated", + "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" + } + ] + } +} \ No newline at end of file