{ "updatedAt": "2026-06-19T17:32:10.943Z", "createdAt": "2026-05-15T20:21:45.666Z", "id": "s688ICOeW8Y8Q8IW", "name": "FulgencioChat V3 - GLM", "description": null, "active": true, "isArchived": false, "nodes": [ { "parameters": { "httpMethod": "POST", "path": "googlechat", "responseMode": "responseNode", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ -688, 1104 ], "id": "6db3d13a-81ce-4fec-b956-ce0dfc1b1437", "name": "Webhook", "webhookId": "85f57925-7a74-4686-9b6b-1d022ec8bc37" }, { "parameters": { "respondWith": "json", "responseBody": "{\n \"text\": \"⏳ Procesando tu solicitud, dame un momento...\"\n}", "options": {} }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [ -480, 1104 ], "id": "b028e258-9418-4d9f-ba27-e69abacf5a64", "name": "Respond to Webhook" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const webhookData = $('Webhook').first().json || {};\n\n// En algunos casos Google Chat llega dentro de body.\n// En otros, puede venir directo en el objeto principal.\nconst body = webhookData.body || webhookData;\n\nconst message = body.message || {};\nconst user = body.user || message.sender || {};\nconst space = body.space || message.space || {};\nconst thread = body.thread || message.thread || {};\n\nconst texto =\n message.argumentText ||\n message.text ||\n body.text ||\n '';\n\nconst pregunta = String(texto || '')\n .replace(/^@\\S+\\s*/, '')\n .trim();\n\nreturn {\n json: {\n pregunta,\n usuario_nombre: user.displayName || message.sender?.displayName || '',\n usuario_email: user.email || message.sender?.email || '',\n space_name: space.name || message.space?.name || '',\n thread_name: thread.name || message.thread?.name || '',\n message_name: message.name || '',\n event_type: body.type || '',\n fecha_evento: body.eventTime || new Date().toISOString()\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -288, 1104 ], "id": "f32fa47b-c111-41a8-94a4-51c3346c4422", "name": "Code - Preparar mensaje" }, { "parameters": { "documentId": { "__rl": true, "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", "mode": "list", "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "propuestas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" }, "filtersUI": { "values": [] }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 112, 1104 ], "id": "e860e21b-9783-4036-a52e-c9f0650ec484", "name": "Sheets - Leer propuestas", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const rows = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst MAX_CANDIDATOS_DEFAULT = 25;\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nconst preguntaNorm = norm(pregunta);\n\nfunction detectarIntencionEspecial(texto) {\n const t = norm(texto);\n\n // IMPORTANTE:\n // No tratamos cualquier mención de \"brief\" como intención de crear un brief,\n // porque ahora Fulgencio también puede buscar briefs existentes en el Sheet de CDC Briefs.\n const pideCrearBrief =\n t.includes('CREAR BRIEF') ||\n t.includes('CREAR UN BRIEF') ||\n t.includes('CREAR UNA SOLICITUD') ||\n t.includes('CREAR SOLICITUD') ||\n t.includes('LLENAR BRIEF') ||\n t.includes('LLENAR UN BRIEF') ||\n t.includes('HACER BRIEF') ||\n t.includes('HACER UN BRIEF') ||\n t.includes('NUEVO BRIEF') ||\n t.includes('NUEVA SOLICITUD') ||\n t.includes('FORMULARIO DE BRIEF') ||\n t.includes('FORMULARIO PARA BRIEF') ||\n t.includes('SOLICITUD CREATIVA');\n\n const pideImagen =\n t.includes('CREAR IMAGEN') ||\n t.includes('GENERAR IMAGEN') ||\n t.includes('HAZME UNA IMAGEN') ||\n t.includes('DISEÑA UNA IMAGEN') ||\n t.includes('DISENA UNA IMAGEN') ||\n t.includes('IMAGEN NUEVA') ||\n t.includes('ARTE NUEVO');\n\n const pideAyuda =\n t.includes('QUE PUEDES HACER') ||\n t.includes('QUÉ PUEDES HACER') ||\n t.includes('AYUDA') ||\n t.includes('COMO TE USO') ||\n t.includes('CÓMO TE USO') ||\n t.includes('COMO FUNCIONAS') ||\n t.includes('CÓMO FUNCIONAS') ||\n t.includes('COMO FUNCIONA') ||\n t.includes('CÓMO FUNCIONA');\n\n if (pideCrearBrief) {\n return {\n tipo: 'brief',\n prompt: `\nEl usuario necesita crear o llenar un brief.\n\nResponde de forma breve indicando que puede crear la solicitud en este enlace:\nhttps://digitalcompass.agency/brief\n\nNo busques propuestas. No inventes información adicional.\n`\n };\n }\n\n if (pideImagen) {\n return {\n tipo: 'imagen_no_disponible',\n prompt: `\nEl usuario está pidiendo crear o generar una imagen.\n\nResponde claramente que la creación de imágenes no está disponible actualmente en Fulgencio.\nLuego ofrece ayudarle a buscar referencias, propuestas, campañas, briefs o ideas dentro de las fuentes disponibles.\n`\n };\n }\n\n if (pideAyuda) {\n return {\n tipo: 'ayuda',\n prompt: `\nEl usuario está pidiendo ayuda sobre cómo usar Fulgencio.\n\nExplica brevemente que Fulgencio puede ayudar a buscar información en:\n- banco de propuestas\n- briefs o solicitudes creativas de CDC Brief\n\nPuede buscar por:\n- cliente\n- marca\n- país\n- año\n- tipo de acción\n- estado de aprobación\n- canal\n- etiquetas o temas\n- brief ID\n- solicitante\n- tema del brief\n\nDa 6 ejemplos de preguntas útiles:\n1. Dame las 5 últimas propuestas de Motorola.\n2. Busca propuestas de Nestlé en República Dominicana.\n3. Dame propuestas aprobadas de activación en PDV.\n4. Tienes propuestas de Kit Kat en Costa Rica?\n5. Dame los últimos briefs creados.\n6. Busca briefs relacionados con Motorola.\n`\n };\n }\n\n return null;\n}\n\nfunction extraerCantidad(texto) {\n const t = norm(texto);\n\n const numero = t.match(/\\b(\\d{1,2})\\b/);\n if (numero) return Number(numero[1]);\n\n const palabras = {\n UNA: 1,\n UNO: 1,\n DOS: 2,\n TRES: 3,\n CUATRO: 4,\n CINCO: 5,\n SEIS: 6,\n SIETE: 7,\n OCHO: 8,\n NUEVE: 9,\n DIEZ: 10,\n };\n\n for (const [palabra, valor] of Object.entries(palabras)) {\n if (t.includes(palabra)) return valor;\n }\n\n return null;\n}\n\nfunction detectarUltimas(texto) {\n const t = norm(texto);\n\n return [\n 'ULTIMA',\n 'ULTIMAS',\n 'ULTIMO',\n 'ULTIMOS',\n 'RECIENTE',\n 'RECIENTES',\n 'NUEVA',\n 'NUEVAS',\n 'NUEVO',\n 'NUEVOS',\n ].some(x => t.includes(x));\n}\n\nfunction detectarSeguimiento(texto) {\n const t = norm(texto);\n\n return [\n 'DE ESAS',\n 'DE ESTAS',\n 'DE LOS ANTERIORES',\n 'DE LAS ANTERIORES',\n 'LA ANTERIOR',\n 'EL ANTERIOR',\n 'LA PRIMERA',\n 'LA SEGUNDA',\n 'LA TERCERA',\n 'ESAS',\n 'ESTAS',\n 'LOS QUE ME MOSTRASTE',\n 'LAS QUE ME MOSTRASTE',\n ].some(x => t.includes(x));\n}\n\nfunction detectarEstado(texto) {\n const t = norm(texto);\n\n if (t.includes('NO APROBADA') || t.includes('NO APROBADAS')) {\n return 'NO APROBADA';\n }\n\n if (t.includes('APROBADA') || t.includes('APROBADAS')) {\n return 'APROBADA';\n }\n\n if (\n t.includes('PENDIENTE') ||\n t.includes('PENDIENTES') ||\n t.includes('PENDIENTE DE APROBACION')\n ) {\n return 'PENDIENTE DE APROBACION';\n }\n\n return '';\n}\n\nfunction detectarAnios(texto) {\n const matches = norm(texto).match(/\\b(20\\d{2}|19\\d{2})\\b/g);\n return matches ? [...new Set(matches)] : [];\n}\n\nfunction detectarPaises(texto) {\n const t = norm(texto);\n\n const paises = [\n 'EL SALVADOR',\n 'PANAMA',\n 'REPUBLICA DOMINICANA',\n 'COLOMBIA',\n 'PUERTO RICO',\n 'HONDURAS',\n 'MEXICO',\n 'VENEZUELA',\n 'JAMAICA',\n 'TRINIDAD Y TOBAGO',\n 'COSTA RICA',\n 'NICARAGUA',\n 'GUATEMALA',\n ];\n\n const encontrados = [];\n\n for (const pais of paises) {\n if (t.includes(norm(pais))) {\n encontrados.push(pais);\n }\n }\n\n if (/\\bRD\\b/.test(t) || t.includes('REP DOM') || t.includes('DOMINICANA')) {\n if (!encontrados.includes('REPUBLICA DOMINICANA')) {\n encontrados.push('REPUBLICA DOMINICANA');\n }\n }\n\n return encontrados;\n}\n\nfunction detectarCanales(texto) {\n const t = norm(texto);\n\n const canales = [];\n\n if (t.includes('MODERNO')) canales.push('MODERNO');\n if (t.includes('TRADICIONAL')) canales.push('TRADICIONAL');\n if (t.includes('ONLINE') || t.includes('ON LINE')) canales.push('ONLINE');\n if (t.includes('PDV') || t.includes('PUNTO DE VENTA')) canales.push('PDV');\n if (t.includes('NO APLICA')) canales.push('NO APLICA');\n\n return canales;\n}\n\nconst intencionEspecial = detectarIntencionEspecial(pregunta);\n\nif (intencionEspecial) {\n return [\n {\n json: {\n source_type: 'propuestas',\n pregunta,\n intencion_especial: intencionEspecial.tipo,\n tokens: [],\n cantidad_solicitada: null,\n quiere_ultimas: false,\n es_seguimiento: false,\n estado_solicitado: '',\n paises_solicitados: [],\n anios_solicitados: [],\n canales_solicitados: [],\n total_propuestas: rows.length,\n total_candidatos: 0,\n sin_resultados: false,\n candidatos: [],\n contexto_propuestas: '',\n prompt_usuario: intencionEspecial.prompt\n }\n }\n ];\n}\n\nconst cantidadSolicitada = extraerCantidad(pregunta);\nconst quiereUltimas = detectarUltimas(pregunta);\nconst esSeguimiento = detectarSeguimiento(pregunta);\nconst estadoSolicitado = detectarEstado(pregunta);\nconst aniosSolicitados = detectarAnios(pregunta);\nconst paisesSolicitados = detectarPaises(pregunta);\nconst canalesSolicitados = detectarCanales(pregunta);\n\nconst stopwords = new Set([\n 'DAME', 'DARME', 'MUESTRA', 'MUESTRAME', 'BUSCA', 'BUSCAME',\n 'PROPUESTA', 'PROPUESTAS', 'IDEA', 'IDEAS', 'DOCUMENTO', 'DOCUMENTOS',\n 'ULTIMA', 'ULTIMAS', 'ULTIMO', 'ULTIMOS', 'RECIENTE', 'RECIENTES',\n 'NUEVA', 'NUEVAS', 'NUEVO', 'NUEVOS',\n 'APROBADA', 'APROBADAS', 'PENDIENTE', 'PENDIENTES',\n 'DE', 'DEL', 'LA', 'LAS', 'EL', 'LOS', 'PARA', 'POR', 'CON',\n 'EN', 'UN', 'UNA', 'Y', 'O', 'QUE', 'ME', 'TENGO', 'TIENES',\n 'ALGUNA', 'ALGUNAS', 'ALGUN', 'SOBRE', 'RELACIONADA', 'RELACIONADAS',\n 'ESAS', 'ESTAS', 'ESOS', 'ESTOS',\n]);\n\nconst tokens = preguntaNorm\n .split(/[^A-Z0-9Ñ]+/)\n .map(t => t.trim())\n .filter(t => t.length >= 3 && !stopwords.has(t))\n .filter(t => !aniosSolicitados.includes(t));\n\nfunction getYear(value) {\n const year = clean(value).match(/\\b(20\\d{2}|19\\d{2})\\b/);\n return year ? Number(year[1]) : 0;\n}\n\nfunction fieldContains(value, expected) {\n return norm(value).includes(norm(expected));\n}\n\nfunction matchesAny(value, expectedList) {\n if (!expectedList.length) return true;\n return expectedList.some(expected => fieldContains(value, expected));\n}\n\nfunction hardFilterPass(p) {\n if (paisesSolicitados.length && !matchesAny(p.PAIS, paisesSolicitados)) {\n return false;\n }\n\n if (aniosSolicitados.length && !aniosSolicitados.includes(String(p.ANIO))) {\n return false;\n }\n\n if (estadoSolicitado && norm(p.APROBADA) !== norm(estadoSolicitado)) {\n return false;\n }\n\n if (canalesSolicitados.length && !matchesAny(p.CANAL, canalesSolicitados)) {\n return false;\n }\n\n return true;\n}\n\nfunction scoreRow(row) {\n const camposGenerales = [\n row.NOMBRE,\n row.CLIENTE,\n row.MARCA,\n row.PAIS,\n row['TIPO DE ACCION'],\n row.CANAL,\n row['AMBIENTE DE COMPRA (RE)'],\n row['TÁCTICA PROMOCIONAL'],\n row.APROBADA,\n row.ETIQUETAS,\n row['AÑO'],\n row.Descripcion,\n row.nombre_archivo,\n ];\n\n const camposFuertes = [\n row.NOMBRE,\n row.CLIENTE,\n row.MARCA,\n row.PAIS,\n row['TIPO DE ACCION'],\n row.ETIQUETAS,\n row['AÑO'],\n ];\n\n const textoGeneral = norm(camposGenerales.join(' '));\n const textoFuerte = norm(camposFuertes.join(' '));\n\n let score = 0;\n\n for (const token of tokens) {\n if (textoGeneral.includes(token)) score += 1;\n if (textoFuerte.includes(token)) score += 2;\n }\n\n if (paisesSolicitados.length && matchesAny(row.PAIS, paisesSolicitados)) {\n score += 8;\n }\n\n if (aniosSolicitados.length && aniosSolicitados.includes(clean(row['AÑO']))) {\n score += 8;\n }\n\n if (estadoSolicitado && norm(row.APROBADA) === norm(estadoSolicitado)) {\n score += 8;\n }\n\n if (canalesSolicitados.length && matchesAny(row.CANAL, canalesSolicitados)) {\n score += 5;\n }\n\n return score;\n}\n\nconst propuestas = rows\n .map((item, index) => {\n const row = item.json || {};\n\n const fila =\n row.row_number ||\n row.__row_number ||\n row.__rowNumber ||\n row.rowNumber ||\n index + 2;\n\n const anio = clean(row['AÑO']);\n\n return {\n fila,\n score: scoreRow(row),\n\n NOMBRE: clean(row.NOMBRE),\n CLIENTE: clean(row.CLIENTE),\n MARCA: clean(row.MARCA),\n PAIS: clean(row.PAIS),\n TIPO_DE_ACCION: clean(row['TIPO DE ACCION']),\n CANAL: clean(row.CANAL),\n AMBIENTE_DE_COMPRA: clean(row['AMBIENTE DE COMPRA (RE)']),\n TACTICA_PROMOCIONAL: clean(row['TÁCTICA PROMOCIONAL']),\n APROBADA: clean(row.APROBADA),\n ETIQUETAS: clean(row.ETIQUETAS),\n ANIO: anio,\n ANIO_NUM: getYear(anio),\n ENLACE: clean(row['Enlace a la propuesta']),\n DESCRIPCION: clean(row.Descripcion),\n };\n })\n .filter(p => p.NOMBRE || p.DESCRIPCION || p.ENLACE);\n\nlet candidatos = propuestas\n .filter(p => hardFilterPass(p))\n .filter(p => {\n if (tokens.length > 0) return p.score > 0;\n\n if (\n paisesSolicitados.length ||\n aniosSolicitados.length ||\n estadoSolicitado ||\n canalesSolicitados.length\n ) {\n return true;\n }\n\n return false;\n });\n\nif (candidatos.length === 0 && tokens.length === 0 && !esSeguimiento) {\n candidatos = [...propuestas];\n}\n\nif (quiereUltimas) {\n candidatos = candidatos.sort((a, b) => b.fila - a.fila);\n} else {\n candidatos = candidatos.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.fila - a.fila;\n });\n}\n\nconst limiteCandidatos = cantidadSolicitada\n ? Math.min(Math.max(cantidadSolicitada, 1), 25)\n : MAX_CANDIDATOS_DEFAULT;\n\ncandidatos = candidatos.slice(0, limiteCandidatos);\n\nconst contexto = candidatos.map((p, index) => {\n return [\n `RESULTADO ${index + 1}`,\n `Fila: ${p.fila}`,\n `Nombre: ${p.NOMBRE}`,\n `Cliente: ${p.CLIENTE}`,\n `Marca: ${p.MARCA}`,\n `País: ${p.PAIS}`,\n `Tipo de acción: ${p.TIPO_DE_ACCION}`,\n `Canal: ${p.CANAL}`,\n `Ambiente de compra: ${p.AMBIENTE_DE_COMPRA}`,\n `Táctica promocional: ${p.TACTICA_PROMOCIONAL}`,\n `Estado aprobación: ${p.APROBADA}`,\n `Etiquetas: ${p.ETIQUETAS}`,\n `Año: ${p.ANIO}`,\n `Descripción: ${p.DESCRIPCION}`,\n `Enlace: ${p.ENLACE}`,\n ].join('\\n');\n}).join('\\n\\n---\\n\\n');\n\nconst sinResultados = candidatos.length === 0;\n\nconst promptUsuario = `\nPregunta del usuario:\n${pregunta}\n\nInstrucción de búsqueda detectada:\n- Cantidad solicitada: ${cantidadSolicitada || 'No especificada'}\n- Quiere últimas/recientes: ${quiereUltimas ? 'Sí' : 'No'}\n- Es pregunta de seguimiento: ${esSeguimiento ? 'Sí' : 'No'}\n- Estado solicitado: ${estadoSolicitado || 'No especificado'}\n- Países solicitados: ${paisesSolicitados.length ? paisesSolicitados.join(', ') : 'No especificado'}\n- Años solicitados: ${aniosSolicitados.length ? aniosSolicitados.join(', ') : 'No especificado'}\n- Canales solicitados: ${canalesSolicitados.length ? canalesSolicitados.join(', ') : 'No especificado'}\n\nResultado de búsqueda:\n${sinResultados ? 'No se encontraron propuestas candidatas con los filtros detectados.' : 'Se encontraron propuestas candidatas.'}\n\nContexto encontrado en el banco de propuestas:\n${contexto || 'No se encontraron propuestas candidatas con la búsqueda.'}\n\nInstrucción para responder:\n${sinResultados\n ? 'Indica que no encontraste coincidencias. No inventes propuestas. Sugiere probar con otro cliente, marca, país, año, canal o tipo de acción.'\n : 'Responde usando únicamente las propuestas del contexto.'}\n`;\n\nreturn [\n {\n json: {\n source_type: 'propuestas',\n pregunta,\n tokens,\n cantidad_solicitada: cantidadSolicitada,\n quiere_ultimas: quiereUltimas,\n es_seguimiento: esSeguimiento,\n estado_solicitado: estadoSolicitado,\n paises_solicitados: paisesSolicitados,\n anios_solicitados: aniosSolicitados,\n canales_solicitados: canalesSolicitados,\n total_propuestas: propuestas.length,\n total_candidatos: candidatos.length,\n sin_resultados: sinResultados,\n intencion_especial: '',\n candidatos,\n contexto_propuestas: contexto,\n prompt_usuario: promptUsuario,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 336, 1104 ], "id": "7773a0d7-596e-4deb-8f2e-9775ae06b07a", "name": "Code - Filtrar propuestas relevantes" }, { "parameters": { "promptType": "define", "text": "={{ $json.prompt_usuario }}", "options": { "systemMessage": "=Eres Fulgencio Fumado, asistente interno de creatividad, briefs y propuestas de GomezLee Marketing.\n\nTu función es responder preguntas usando únicamente el contexto entregado por el sistema.\n\nActualmente puedes recibir contexto de dos fuentes:\n\n1. Banco de propuestas:\nContiene propuestas, presentaciones, activaciones, casos, campañas y referencias internas de GomezLee Marketing.\n\n2. CDC Briefs:\nContiene briefs, solicitudes creativas y requerimientos generados desde la app CDC Brief.\n\nNo confundas ambas fuentes:\n- Una propuesta es un caso, presentación, activación o referencia interna del banco de propuestas.\n- Un brief es una solicitud o requerimiento creativo.\n- Si el usuario pregunta por propuestas, casos, activaciones, presentaciones o campañas, prioriza el banco de propuestas.\n- Si el usuario pregunta por briefs, solicitudes, requerimientos creativos, pedidos o temas registrados en CDC Brief, prioriza CDC Briefs.\n- Si hay información útil en ambas fuentes, separa la respuesta por secciones:\n Banco de propuestas\n CDC Briefs\n\nReglas obligatorias:\n1. No inventes propuestas, briefs, enlaces, países, clientes, marcas, años, estados, fechas ni solicitantes.\n2. Si no hay información suficiente en el contexto, dilo claramente.\n3. No digas que puedes crear imágenes. La creación de imágenes no está disponible actualmente en Fulgencio.\n4. No menciones n8n, workflows, Google Sheets, nodos, prompts, memoria técnica ni procesos internos.\n5. Responde en español, con tono profesional, cercano, directo y útil. Evita sonar demasiado formal o robótico.\n6. Cuando incluyas enlaces, ponlos completos.\n7. Si algún dato aparece como PENDIENTE, vacío o no disponible, no lo inventes; indica que ese dato está pendiente o no disponible.\n8. Usa únicamente la información entregada en el contexto o en la memoria reciente cuando sea una pregunta de seguimiento.\n9. No prometas enviar archivos, crear documentos, generar imágenes, modificar propuestas, actualizar información o hacer cambios en sistemas. Solo puedes consultar, resumir, comparar y sugerir usando el contexto disponible.\n10. Si el usuario combina varios filtros y hay pocos o ningún resultado, explica qué filtros se aplicaron y sugiere quitar o cambiar algún filtro.\n\nReglas para propuestas:\n- Si el usuario pide propuestas, entrega una lista clara con:\n - Nombre\n - Cliente/Marca\n - País\n - Año si está disponible\n - Tipo de acción si está disponible\n - Breve descripción\n - Enlace\n- Si una propuesta tiene enlace disponible, inclúyelo siempre.\n- Si no tiene enlace, indica “Enlace no disponible”.\n- Si el usuario pide una cantidad específica, respeta esa cantidad cuando el contexto la tenga disponible.\n- Si el usuario pide las últimas o más recientes, respeta el orden entregado en el contexto.\n- Cuando la instrucción de búsqueda detectada indique país, año, canal, estado o cantidad solicitada, respeta esos filtros estrictamente.\n- Si no hay candidatos de propuestas en el contexto, no inventes resultados. Indica que no encontraste coincidencias y sugiere ajustar la búsqueda por cliente, marca, país, año, canal, estado o tipo de acción.\n\nReglas para CDC Briefs:\n- Si el usuario pide briefs, solicitudes, requerimientos o pedidos creativos existentes, responde usando la sección CDC Briefs del contexto.\n- Cuando respondas sobre briefs, incluye cuando esté disponible:\n - Brief ID\n - Fecha de creación\n - Solicitante\n - Correo\n - Estado\n - Fecha de entrega\n - Resumen\n - Link del documento brief\n - Link de carpeta Drive\n- No trates un brief como si fuera una propuesta.\n- No trates una propuesta como si fuera un brief.\n- Si el usuario pide los últimos briefs o briefs recientes, respeta el orden entregado en el contexto.\n- Si no hay briefs candidatos en el contexto, indica que no encontraste briefs relacionados y sugiere buscar por brief ID, solicitante, tema, estado o fecha.\n- Si el usuario pide crear un brief, llenar un brief, hacer una nueva solicitud o abrir el formulario de brief, comparte este enlace:\nhttps://digitalcompass.agency/brief\n- Si el usuario solo pregunta por briefs existentes, no respondas únicamente con el enlace del formulario. Busca y responde con los briefs del contexto.\n\nFormato recomendado:\n- Resumen breve al inicio.\n- Lista numerada cuando aplique.\n- Si usas ambas fuentes, separa en secciones:\n Banco de propuestas\n CDC Briefs\n- Cierre breve ofreciendo filtrar por país, marca, cliente, año, estado, tipo de acción, brief ID, solicitante o tema.\n\nFormato especial:\n- Si el usuario pide “tabla”, “formato tabla” o “en una tabla”, responde directamente en tabla Markdown.\n- Para propuestas en tabla, usa columnas útiles como:\n Nombre, Cliente/Marca, País, Año, Tipo de acción y Enlace.\n- Para briefs en tabla, usa columnas útiles como:\n Brief ID, Fecha, Solicitante, Estado, Fecha de entrega, Resumen y Enlace.\n- Si el usuario pide bullets, responde en bullets.\n\nAnálisis, patrones e ideas:\n- Si el usuario pide análisis, patrones, insights o ideas creativas, analiza únicamente la información entregada en el contexto.\n- Resume patrones repetidos, tipos de activación, mecánicas, formatos, canales, oportunidades y temas relevantes sin inventar datos fuera del contexto.\n- Si el usuario pide ideas nuevas, usa el contexto como referencia.\n- Aclara que son ideas inspiradas en propuestas o briefs existentes y no propuestas ya aprobadas, salvo que el contexto indique lo contrario.\n\nComparaciones:\n- Si el usuario pide comparar propuestas, organiza la respuesta por diferencias y similitudes:\n - Tipo de acción\n - País\n - Marca\n - Canal\n - Objetivo\n - Elementos creativos\n- Si el usuario pide comparar briefs, organiza la respuesta por:\n - Solicitante\n - Estado\n - Fecha de entrega\n - Objetivo o tema\n - Materiales o insumos disponibles\n - Diferencias principales\n- Usa solo el contexto disponible.\n\nSeguimiento y memoria:\n- Si el usuario dice “de esas”, “la segunda”, “la anterior”, “dame los enlaces”, “resume esas”, “de esos briefs” o “las que me mostraste”, usa la memoria reciente para entender la referencia.\n- Responde solo con información que aparezca en el contexto o memoria entregada.\n- No mezcles resultados nuevos con resultados de memoria si el usuario claramente se refiere a resultados ya mostrados.\n- Si no puedes identificar a qué se refiere, pide una aclaración breve.\n\nMensajes ambiguos:\n- Si el usuario envía un mensaje vacío, muy corto o ambiguo como “hola”, “buenas”, “ayuda” o “qué puedes hacer”, responde explicando brevemente cómo puede usar Fulgencio y da ejemplos de preguntas útiles.\n- Ejemplos de preguntas útiles:\n 1. Dame las 5 últimas propuestas de Motorola.\n 2. Busca propuestas de Nestlé en República Dominicana.\n 3. Dame propuestas aprobadas de activación en PDV.\n 4. Tienes propuestas de Kit Kat en Costa Rica?\n 5. Dame los últimos briefs creados.\n 6. Busca briefs relacionados con Motorola.\n 7. Necesito crear un brief.\n 8. Necesito acceder al Tablero CDC.\n 9. ¿Dónde registro un proyecto creativo?\n\nImágenes:\n- Si el usuario solicita crear o generar imágenes, indica que la creación de imágenes no está disponible actualmente en Fulgencio.\n- Ofrece buscar referencias, propuestas, briefs o ideas existentes dentro del contexto disponible.\n\nBrief nuevo:\n- Si el usuario pide crear un brief, llenar un brief, hacer una solicitud creativa o abrir el formulario, comparte este enlace:\nhttps://digitalcompass.agency/brief\n\nTablero CDC / Aprobaciones Proyectos:\n- Si el usuario pide acceder al Tablero CDC, abrir Aprobaciones Proyectos, entrar al tablero de proyectos o registrar un proyecto creativo, comparte este enlace:\nhttps://digitalcompass.agency/tablero-cdc/\n- Indica brevemente que corresponde al Tablero CDC / Aprobaciones Proyectos.\n- No afirmes que puedes crear, modificar, aprobar, eliminar o consultar proyectos dentro del Tablero CDC desde el chat.\n- No confundas el Tablero CDC con CDC Brief ni con el Banco de Propuestas.\n\nRestricción final:\nSi el contexto indica que no se encontraron candidatos, no inventes resultados. Responde que no encontraste coincidencias y sugiere ajustar la búsqueda según la fuente:\n- Para propuestas: cliente, marca, país, año, canal, estado o tipo de acción.\n- Para briefs: brief ID, solicitante, estado, fecha, tema o palabras clave del brief." } }, "type": "@n8n/n8n-nodes-langchain.agent", "typeVersion": 2, "position": [ 1136, 1088 ], "id": "9eaa8c05-9abd-493f-8b3d-f9ba212fcc81", "name": "AI Agent - Gemini Fulgencio" }, { "parameters": { "modelName": "models/gemini-3.1-flash-lite", "options": {} }, "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", "typeVersion": 1, "position": [ 1136, 1312 ], "id": "6e26bca2-35f2-41ba-b73a-d7a718fe3471", "name": "Google Gemini Chat Model", "credentials": { "googlePalmApi": { "id": "6Jdcd9FONxG35YuJ", "name": "Isaac - Gemini Api Gratis" } } }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const data = $json || {};\n\nlet respuesta =\n data.output ||\n data.text ||\n data.response ||\n data.content ||\n data.message ||\n '';\n\nrespuesta = String(respuesta || '').trim();\n\nif (!respuesta) {\n respuesta = 'No pude generar una respuesta con la información disponible.';\n}\n\nconst MAX_CHARS = 7000;\n\nif (respuesta.length > MAX_CHARS) {\n respuesta =\n respuesta.slice(0, MAX_CHARS) +\n '\\n\\nRespuesta recortada por longitud. Puedes pedirme un filtro más específico por cliente, marca, país, año, estado o tipo de acción.';\n}\n\nreturn {\n json: {\n respuesta\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1488, 1088 ], "id": "709bd05c-3649-4441-9ac1-3db31d681639", "name": "Code - Formatear respuesta Chat" }, { "parameters": { "authentication": "oAuth2", "spaceId": "={{ $('Code - Preparar mensaje').first().json.space_name }}", "messageUi": { "text": "={{ $json.respuesta }}" }, "additionalFields": {} }, "type": "n8n-nodes-base.googleChat", "typeVersion": 1, "position": [ 1744, 1088 ], "id": "02fa53e1-d7db-4adf-8656-65cc4470624e", "name": "Chat - Enviar respuesta", "webhookId": "bc07f187-75cb-40aa-a145-75bfe88d9d5e", "credentials": { "googleChatOAuth2Api": { "id": "V4eVQvEoy5QcBvUu", "name": "Chat account" } } }, { "parameters": { "sessionIdType": "customKey", "sessionKey": "={{ $('Code - Preparar mensaje').first().json.thread_name || ($('Code - Preparar mensaje').first().json.space_name + '::' + $('Code - Preparar mensaje').first().json.usuario_email) }}", "contextWindowLength": 6 }, "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", "typeVersion": 1.3, "position": [ 1280, 1312 ], "id": "0c3cce29-08b6-4708-a7df-c85272c4fb25", "name": "Simple Memory" }, { "parameters": { "content": "💬 Entrada Google Chat\n\nRecibe el mensaje del usuario desde Google Chat, responde rápido con “procesando” y prepara los datos de la conversación: pregunta, usuario, espacio, hilo y session key.", "height": 448, "width": 608, "color": 2 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -736, 928 ], "id": "36e88459-cdd1-4fea-ba12-855daefb5d66", "name": "Sticky Note" }, { "parameters": { "content": "🔎 Búsqueda y contexto de Fulgencio\n\nLee el banco de propuestas y el Sheet de CDC Briefs.\n\nFiltra candidatos relevantes antes de enviarlos a Gemini:\n- propuestas, casos, activaciones y campañas\n- briefs, solicitudes creativas y requerimientos\n\nLuego une ambas fuentes en un solo contexto, manteniéndolas separadas para no mezclar propuestas con briefs.", "height": 544, "width": 960, "color": 4 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 0, 912 ], "id": "e4f025cd-9b7c-4de8-bfbd-51a163a56f74", "name": "Sticky Note1" }, { "parameters": { "content": "🤖 Respuesta con Gemini\n\nGemini responde usando solo el contexto filtrado y combinado.\n\nPuede responder sobre propuestas y CDC Briefs, manteniendo cada fuente separada cuando aplique.\n\nNo crea imágenes, no inventa datos y conserva enlaces reales cuando estén disponibles.", "height": 544, "width": 864, "color": 5 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 1072, 944 ], "id": "10906b1f-a3d0-476e-81f5-e2d3b5c4467d", "name": "Sticky Note2" }, { "parameters": { "documentId": { "__rl": true, "value": "1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU", "mode": "list", "cachedResultName": "CDC Brief - Base para Fulgencio", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "Briefs", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU/edit#gid=0" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 80, 1296 ], "id": "b3f61adb-199d-4f90-8187-9efb92f72378", "name": "Sheets - Leer briefs CDC", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const rows = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst MAX_BRIEFS_DEFAULT = 10;\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nconst preguntaNorm = norm(pregunta);\n\nfunction extraerCantidad(texto) {\n const t = norm(texto);\n\n const numero = t.match(/\\b(\\d{1,2})\\b/);\n if (numero) return Number(numero[1]);\n\n const palabras = {\n UNA: 1,\n UNO: 1,\n DOS: 2,\n TRES: 3,\n CUATRO: 4,\n CINCO: 5,\n SEIS: 6,\n SIETE: 7,\n OCHO: 8,\n NUEVE: 9,\n DIEZ: 10,\n };\n\n for (const [palabra, valor] of Object.entries(palabras)) {\n if (t.includes(palabra)) return valor;\n }\n\n return null;\n}\n\nfunction detectarUltimas(texto) {\n const t = norm(texto);\n\n return [\n 'ULTIMA',\n 'ULTIMAS',\n 'ULTIMO',\n 'ULTIMOS',\n 'RECIENTE',\n 'RECIENTES',\n 'NUEVA',\n 'NUEVAS',\n 'NUEVO',\n 'NUEVOS',\n ].some(x => t.includes(x));\n}\n\nfunction detectarInteresBriefs(texto) {\n const t = norm(texto);\n\n return [\n 'BRIEF',\n 'BRIEFS',\n 'SOLICITUD',\n 'SOLICITUDES',\n 'REQUERIMIENTO',\n 'REQUERIMIENTOS',\n 'PEDIDO CREATIVO',\n 'CDC',\n 'FORMULARIO',\n ].some(x => t.includes(x));\n}\n\nfunction detectarInteresPropuestas(texto) {\n const t = norm(texto);\n\n return [\n 'PROPUESTA',\n 'PROPUESTAS',\n 'BANCO DE PROPUESTAS',\n 'CASO',\n 'CASOS',\n 'ACTIVACION',\n 'ACTIVACIONES',\n ].some(x => t.includes(x));\n}\n\nconst cantidadSolicitada = extraerCantidad(pregunta);\nconst quiereUltimas = detectarUltimas(pregunta);\nconst mencionaBriefs = detectarInteresBriefs(pregunta);\nconst mencionaPropuestas = detectarInteresPropuestas(pregunta);\n\nconst stopwords = new Set([\n 'DAME', 'DARME', 'MUESTRA', 'MUESTRAME', 'BUSCA', 'BUSCAME',\n 'BRIEF', 'BRIEFS', 'SOLICITUD', 'SOLICITUDES', 'REQUERIMIENTO', 'REQUERIMIENTOS',\n 'PROPUESTA', 'PROPUESTAS', 'DOCUMENTO', 'DOCUMENTOS',\n 'ULTIMA', 'ULTIMAS', 'ULTIMO', 'ULTIMOS', 'RECIENTE', 'RECIENTES',\n 'NUEVA', 'NUEVAS', 'NUEVO', 'NUEVOS',\n 'DE', 'DEL', 'LA', 'LAS', 'EL', 'LOS', 'PARA', 'POR', 'CON',\n 'EN', 'UN', 'UNA', 'Y', 'O', 'QUE', 'ME', 'TENGO', 'TIENES',\n 'ALGUNA', 'ALGUNAS', 'ALGUN', 'SOBRE', 'RELACIONADA', 'RELACIONADAS',\n 'CDC', 'FORMULARIO',\n]);\n\nconst tokens = preguntaNorm\n .split(/[^A-Z0-9Ñ]+/)\n .map(t => t.trim())\n .filter(t => t.length >= 3 && !stopwords.has(t));\n\nfunction getDateValue(value) {\n const raw = clean(value);\n const d = new Date(raw);\n return Number.isNaN(d.getTime()) ? 0 : d.getTime();\n}\n\nfunction scoreBrief(row) {\n const camposFuertes = [\n row.brief_id,\n row.nombre_carpeta,\n row.enviado_por,\n row.correo,\n row.estado,\n ];\n\n const camposGenerales = [\n row.brief_id,\n row.nombre_carpeta,\n row.enviado_por,\n row.correo,\n row.estado,\n row.resumen_chatbot,\n row.texto_brief,\n row.texto_busqueda,\n row.fecha_entrega,\n ];\n\n const textoFuerte = norm(camposFuertes.join(' '));\n const textoGeneral = norm(camposGenerales.join(' '));\n\n let score = 0;\n\n for (const token of tokens) {\n if (textoGeneral.includes(token)) score += 1;\n if (textoFuerte.includes(token)) score += 2;\n }\n\n if (mencionaBriefs) score += 3;\n\n return score;\n}\n\nconst debeBuscarBriefs = mencionaBriefs || (!mencionaPropuestas && tokens.length > 0);\n\nlet briefs = rows\n .map((item, index) => {\n const row = item.json || {};\n\n return {\n fila: row.row_number || row.__row_number || row.__rowNumber || row.rowNumber || index + 2,\n score: scoreBrief(row),\n\n fecha_creacion: clean(row.fecha_creacion),\n brief_id: clean(row.brief_id),\n nombre_carpeta: clean(row.nombre_carpeta),\n enviado_por: clean(row.enviado_por),\n correo: clean(row.correo),\n link_carpeta_drive: clean(row.link_carpeta_drive),\n link_documento_brief: clean(row.link_documento_brief),\n estado: clean(row.estado),\n cantidad_audios: clean(row.cantidad_audios),\n cantidad_imagenes: clean(row.cantidad_imagenes),\n cantidad_documentos: clean(row.cantidad_documentos),\n cantidad_links: clean(row.cantidad_links),\n resumen_chatbot: clean(row.resumen_chatbot),\n texto_brief: clean(row.texto_brief),\n texto_busqueda: clean(row.texto_busqueda),\n fecha_entrega: clean(row.fecha_entrega),\n\n fecha_sort: getDateValue(row.fecha_creacion),\n };\n })\n .filter(b => b.brief_id || b.nombre_carpeta || b.texto_brief || b.resumen_chatbot);\n\nlet candidatos = [];\n\nif (debeBuscarBriefs) {\n candidatos = briefs.filter(b => {\n if (tokens.length > 0) return b.score > 0;\n if (mencionaBriefs) return true;\n return false;\n });\n}\n\nif (quiereUltimas) {\n candidatos = candidatos.sort((a, b) => {\n if (b.fecha_sort !== a.fecha_sort) return b.fecha_sort - a.fecha_sort;\n return b.fila - a.fila;\n });\n} else {\n candidatos = candidatos.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n if (b.fecha_sort !== a.fecha_sort) return b.fecha_sort - a.fecha_sort;\n return b.fila - a.fila;\n });\n}\n\nconst limite = cantidadSolicitada\n ? Math.min(Math.max(cantidadSolicitada, 1), 25)\n : MAX_BRIEFS_DEFAULT;\n\ncandidatos = candidatos.slice(0, limite);\n\nconst contextoBriefs = candidatos.map((b, index) => {\n return [\n `BRIEF ${index + 1}`,\n `Fila: ${b.fila}`,\n `Fecha creación: ${b.fecha_creacion}`,\n `Brief ID: ${b.brief_id}`,\n `Nombre carpeta: ${b.nombre_carpeta}`,\n `Enviado por: ${b.enviado_por}`,\n `Correo: ${b.correo}`,\n `Estado: ${b.estado}`,\n `Fecha entrega: ${b.fecha_entrega}`,\n `Cantidad audios: ${b.cantidad_audios}`,\n `Cantidad imágenes: ${b.cantidad_imagenes}`,\n `Cantidad documentos: ${b.cantidad_documentos}`,\n `Cantidad links: ${b.cantidad_links}`,\n `Resumen chatbot: ${b.resumen_chatbot}`,\n `Texto brief: ${b.texto_brief}`,\n `Texto búsqueda: ${b.texto_busqueda}`,\n `Link carpeta Drive: ${b.link_carpeta_drive}`,\n `Link documento brief: ${b.link_documento_brief}`,\n ].join('\\n');\n}).join('\\n\\n---\\n\\n');\n\nreturn [\n {\n json: {\n source_type: 'briefs_cdc',\n pregunta,\n tokens,\n menciona_briefs: mencionaBriefs,\n menciona_propuestas: mencionaPropuestas,\n cantidad_solicitada: cantidadSolicitada,\n quiere_ultimas: quiereUltimas,\n total_briefs: briefs.length,\n total_briefs_candidatos: candidatos.length,\n briefs_candidatos: candidatos,\n contexto_briefs: contextoBriefs,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 288, 1296 ], "id": "a18ef7c8-078c-4d9f-a132-a4518191ba43", "name": "Code - Filtrar briefs CDC" }, { "parameters": {}, "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ 592, 1136 ], "id": "6e4765b5-9cd2-4bb9-b702-95df5b4af5bc", "name": "Merge - Unir fuentes Fulgencio" }, { "parameters": { "jsCode": "const items = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst propuestasItem =\n items.find(i => i.json.source_type === 'propuestas')?.json || {};\n\nconst briefsItem =\n items.find(i => i.json.source_type === 'briefs_cdc')?.json || {};\n\nif (propuestasItem.intencion_especial) {\n return [\n {\n json: {\n ...propuestasItem,\n prompt_usuario: propuestasItem.prompt_usuario,\n total_fuentes: 1,\n fuentes_usadas: ['intencion_especial'],\n },\n },\n ];\n}\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction construirContextoPropuestas(candidatos = []) {\n if (!Array.isArray(candidatos) || candidatos.length === 0) return '';\n\n return candidatos.map((p, index) => {\n return [\n `PROPUESTA ${index + 1}`,\n `Fila: ${clean(p.fila)}`,\n `Nombre: ${clean(p.NOMBRE)}`,\n `Cliente: ${clean(p.CLIENTE)}`,\n `Marca: ${clean(p.MARCA)}`,\n `País: ${clean(p.PAIS)}`,\n `Tipo de acción: ${clean(p.TIPO_DE_ACCION)}`,\n `Canal: ${clean(p.CANAL)}`,\n `Ambiente de compra: ${clean(p.AMBIENTE_DE_COMPRA)}`,\n `Táctica promocional: ${clean(p.TACTICA_PROMOCIONAL)}`,\n `Estado aprobación: ${clean(p.APROBADA)}`,\n `Etiquetas: ${clean(p.ETIQUETAS)}`,\n `Año: ${clean(p.ANIO)}`,\n `Descripción: ${clean(p.DESCRIPCION)}`,\n `Enlace: ${clean(p.ENLACE)}`,\n ].join('\\n');\n }).join('\\n\\n---\\n\\n');\n}\n\nconst contextoPropuestas =\n clean(propuestasItem.contexto_propuestas) ||\n construirContextoPropuestas(propuestasItem.candidatos || []);\n\nconst contextoBriefs = clean(briefsItem.contexto_briefs);\n\nconst totalPropuestas = Number(propuestasItem.total_candidatos || 0);\nconst totalBriefs = Number(briefsItem.total_briefs_candidatos || 0);\n\nconst sinResultados = totalPropuestas === 0 && totalBriefs === 0;\n\nconst promptUsuario = `\nPregunta del usuario:\n${pregunta}\n\nFuentes disponibles para responder:\n1. Banco de propuestas:\n - Contiene propuestas, casos, presentaciones, activaciones y referencias internas de GomezLee Marketing.\n - Total de candidatos encontrados: ${totalPropuestas}\n\n2. CDC Briefs:\n - Contiene briefs y solicitudes creativas generadas desde la app CDC Brief.\n - Total de candidatos encontrados: ${totalBriefs}\n\nReglas para elegir fuente:\n- Si el usuario pregunta por propuestas, casos, activaciones o presentaciones, prioriza el Banco de propuestas.\n- Si el usuario pregunta por briefs, solicitudes, requerimientos creativos, pedidos o formularios CDC, prioriza CDC Briefs.\n- Si la pregunta es general y hay información relevante en ambas fuentes, puedes mencionar ambas separadas por sección.\n- No mezcles un brief con una propuesta como si fueran lo mismo.\n- Un brief es una solicitud o requerimiento creativo.\n- Una propuesta es una presentación/caso del banco de propuestas.\n- Usa únicamente la información entregada abajo.\n- No inventes enlaces ni datos.\n\nResultado de búsqueda:\n${sinResultados ? 'No se encontraron candidatos en ninguna fuente.' : 'Se encontraron candidatos en una o más fuentes.'}\n\nContexto del Banco de propuestas:\n${contextoPropuestas || 'No se encontraron propuestas candidatas.'}\n\nContexto de CDC Briefs:\n${contextoBriefs || 'No se encontraron briefs candidatos.'}\n\nInstrucción para responder:\n${sinResultados\n ? 'Indica que no encontraste coincidencias. Sugiere buscar por cliente, marca, país, año, tipo de acción, estado, brief ID, solicitante o tema del brief.'\n : 'Responde usando únicamente el contexto anterior. Si usas ambas fuentes, separa la respuesta por secciones.'}\n`;\n\nreturn [\n {\n json: {\n pregunta,\n\n source_type: 'contexto_fulgencio_combinado',\n\n total_propuestas_candidatas: totalPropuestas,\n total_briefs_candidatos: totalBriefs,\n sin_resultados: sinResultados,\n\n propuestas: propuestasItem.candidatos || [],\n briefs: briefsItem.briefs_candidatos || [],\n\n contexto_propuestas: contextoPropuestas,\n contexto_briefs: contextoBriefs,\n\n prompt_usuario: promptUsuario,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 800, 1136 ], "id": "7c046244-6f10-4d74-a374-099f211ac26b", "name": "Code - Construir contexto Fulgencio" } ], "connections": { "Webhook": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] }, "Respond to Webhook": { "main": [ [ { "node": "Code - Preparar mensaje", "type": "main", "index": 0 } ] ] }, "Code - Preparar mensaje": { "main": [ [ { "node": "Sheets - Leer propuestas", "type": "main", "index": 0 }, { "node": "Sheets - Leer briefs CDC", "type": "main", "index": 0 } ] ] }, "Sheets - Leer propuestas": { "main": [ [ { "node": "Code - Filtrar propuestas relevantes", "type": "main", "index": 0 } ] ] }, "Code - Filtrar propuestas relevantes": { "main": [ [ { "node": "Merge - Unir fuentes Fulgencio", "type": "main", "index": 0 } ] ] }, "AI Agent - Gemini Fulgencio": { "main": [ [ { "node": "Code - Formatear respuesta Chat", "type": "main", "index": 0 } ] ] }, "Google Gemini Chat Model": { "ai_languageModel": [ [ { "node": "AI Agent - Gemini Fulgencio", "type": "ai_languageModel", "index": 0 } ] ] }, "Code - Formatear respuesta Chat": { "main": [ [ { "node": "Chat - Enviar respuesta", "type": "main", "index": 0 } ] ] }, "Chat - Enviar respuesta": { "main": [ [] ] }, "Simple Memory": { "ai_memory": [ [ { "node": "AI Agent - Gemini Fulgencio", "type": "ai_memory", "index": 0 } ] ] }, "Sheets - Leer briefs CDC": { "main": [ [ { "node": "Code - Filtrar briefs CDC", "type": "main", "index": 0 } ] ] }, "Code - Filtrar briefs CDC": { "main": [ [ { "node": "Merge - Unir fuentes Fulgencio", "type": "main", "index": 1 } ] ] }, "Merge - Unir fuentes Fulgencio": { "main": [ [ { "node": "Code - Construir contexto Fulgencio", "type": "main", "index": 0 } ] ] }, "Code - Construir contexto Fulgencio": { "main": [ [ { "node": "AI Agent - Gemini Fulgencio", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "binaryMode": "separate" }, "staticData": null, "meta": { "templateCredsSetupCompleted": true }, "versionId": "a60ee2c1-eb12-46e1-9dc6-3e9402087051", "activeVersionId": "a60ee2c1-eb12-46e1-9dc6-3e9402087051", "versionCounter": 225, "triggerCount": 1, "shared": [ { "updatedAt": "2026-05-15T20:21:45.671Z", "createdAt": "2026-05-15T20:21:45.671Z", "role": "workflow:owner", "workflowId": "s688ICOeW8Y8Q8IW", "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-06-19T17:30:42.000Z", "createdAt": "2026-06-19T17:30:31.300Z", "versionId": "a60ee2c1-eb12-46e1-9dc6-3e9402087051", "workflowId": "s688ICOeW8Y8Q8IW", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "googlechat", "responseMode": "responseNode", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ -688, 1104 ], "id": "6db3d13a-81ce-4fec-b956-ce0dfc1b1437", "name": "Webhook", "webhookId": "85f57925-7a74-4686-9b6b-1d022ec8bc37" }, { "parameters": { "respondWith": "json", "responseBody": "{\n \"text\": \"⏳ Procesando tu solicitud, dame un momento...\"\n}", "options": {} }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [ -480, 1104 ], "id": "b028e258-9418-4d9f-ba27-e69abacf5a64", "name": "Respond to Webhook" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const webhookData = $('Webhook').first().json || {};\n\n// En algunos casos Google Chat llega dentro de body.\n// En otros, puede venir directo en el objeto principal.\nconst body = webhookData.body || webhookData;\n\nconst message = body.message || {};\nconst user = body.user || message.sender || {};\nconst space = body.space || message.space || {};\nconst thread = body.thread || message.thread || {};\n\nconst texto =\n message.argumentText ||\n message.text ||\n body.text ||\n '';\n\nconst pregunta = String(texto || '')\n .replace(/^@\\S+\\s*/, '')\n .trim();\n\nreturn {\n json: {\n pregunta,\n usuario_nombre: user.displayName || message.sender?.displayName || '',\n usuario_email: user.email || message.sender?.email || '',\n space_name: space.name || message.space?.name || '',\n thread_name: thread.name || message.thread?.name || '',\n message_name: message.name || '',\n event_type: body.type || '',\n fecha_evento: body.eventTime || new Date().toISOString()\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -288, 1104 ], "id": "f32fa47b-c111-41a8-94a4-51c3346c4422", "name": "Code - Preparar mensaje" }, { "parameters": { "documentId": { "__rl": true, "value": "1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng", "mode": "list", "cachedResultName": "BANCO DE PROPUESTAS DE CDC PARA FULGENCIO FUMADO", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "propuestas", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1iy3lC3DS3ZfShb7d26iQOrPjx9s_3B-PC07XfYew7ng/edit#gid=0" }, "filtersUI": { "values": [] }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 112, 1104 ], "id": "e860e21b-9783-4036-a52e-c9f0650ec484", "name": "Sheets - Leer propuestas", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const rows = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst MAX_CANDIDATOS_DEFAULT = 25;\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nconst preguntaNorm = norm(pregunta);\n\nfunction detectarIntencionEspecial(texto) {\n const t = norm(texto);\n\n // IMPORTANTE:\n // No tratamos cualquier mención de \"brief\" como intención de crear un brief,\n // porque ahora Fulgencio también puede buscar briefs existentes en el Sheet de CDC Briefs.\n const pideCrearBrief =\n t.includes('CREAR BRIEF') ||\n t.includes('CREAR UN BRIEF') ||\n t.includes('CREAR UNA SOLICITUD') ||\n t.includes('CREAR SOLICITUD') ||\n t.includes('LLENAR BRIEF') ||\n t.includes('LLENAR UN BRIEF') ||\n t.includes('HACER BRIEF') ||\n t.includes('HACER UN BRIEF') ||\n t.includes('NUEVO BRIEF') ||\n t.includes('NUEVA SOLICITUD') ||\n t.includes('FORMULARIO DE BRIEF') ||\n t.includes('FORMULARIO PARA BRIEF') ||\n t.includes('SOLICITUD CREATIVA');\n\n const pideImagen =\n t.includes('CREAR IMAGEN') ||\n t.includes('GENERAR IMAGEN') ||\n t.includes('HAZME UNA IMAGEN') ||\n t.includes('DISEÑA UNA IMAGEN') ||\n t.includes('DISENA UNA IMAGEN') ||\n t.includes('IMAGEN NUEVA') ||\n t.includes('ARTE NUEVO');\n\n const pideAyuda =\n t.includes('QUE PUEDES HACER') ||\n t.includes('QUÉ PUEDES HACER') ||\n t.includes('AYUDA') ||\n t.includes('COMO TE USO') ||\n t.includes('CÓMO TE USO') ||\n t.includes('COMO FUNCIONAS') ||\n t.includes('CÓMO FUNCIONAS') ||\n t.includes('COMO FUNCIONA') ||\n t.includes('CÓMO FUNCIONA');\n\n if (pideCrearBrief) {\n return {\n tipo: 'brief',\n prompt: `\nEl usuario necesita crear o llenar un brief.\n\nResponde de forma breve indicando que puede crear la solicitud en este enlace:\nhttps://digitalcompass.agency/brief\n\nNo busques propuestas. No inventes información adicional.\n`\n };\n }\n\n if (pideImagen) {\n return {\n tipo: 'imagen_no_disponible',\n prompt: `\nEl usuario está pidiendo crear o generar una imagen.\n\nResponde claramente que la creación de imágenes no está disponible actualmente en Fulgencio.\nLuego ofrece ayudarle a buscar referencias, propuestas, campañas, briefs o ideas dentro de las fuentes disponibles.\n`\n };\n }\n\n if (pideAyuda) {\n return {\n tipo: 'ayuda',\n prompt: `\nEl usuario está pidiendo ayuda sobre cómo usar Fulgencio.\n\nExplica brevemente que Fulgencio puede ayudar a buscar información en:\n- banco de propuestas\n- briefs o solicitudes creativas de CDC Brief\n\nPuede buscar por:\n- cliente\n- marca\n- país\n- año\n- tipo de acción\n- estado de aprobación\n- canal\n- etiquetas o temas\n- brief ID\n- solicitante\n- tema del brief\n\nDa 6 ejemplos de preguntas útiles:\n1. Dame las 5 últimas propuestas de Motorola.\n2. Busca propuestas de Nestlé en República Dominicana.\n3. Dame propuestas aprobadas de activación en PDV.\n4. Tienes propuestas de Kit Kat en Costa Rica?\n5. Dame los últimos briefs creados.\n6. Busca briefs relacionados con Motorola.\n`\n };\n }\n\n return null;\n}\n\nfunction extraerCantidad(texto) {\n const t = norm(texto);\n\n const numero = t.match(/\\b(\\d{1,2})\\b/);\n if (numero) return Number(numero[1]);\n\n const palabras = {\n UNA: 1,\n UNO: 1,\n DOS: 2,\n TRES: 3,\n CUATRO: 4,\n CINCO: 5,\n SEIS: 6,\n SIETE: 7,\n OCHO: 8,\n NUEVE: 9,\n DIEZ: 10,\n };\n\n for (const [palabra, valor] of Object.entries(palabras)) {\n if (t.includes(palabra)) return valor;\n }\n\n return null;\n}\n\nfunction detectarUltimas(texto) {\n const t = norm(texto);\n\n return [\n 'ULTIMA',\n 'ULTIMAS',\n 'ULTIMO',\n 'ULTIMOS',\n 'RECIENTE',\n 'RECIENTES',\n 'NUEVA',\n 'NUEVAS',\n 'NUEVO',\n 'NUEVOS',\n ].some(x => t.includes(x));\n}\n\nfunction detectarSeguimiento(texto) {\n const t = norm(texto);\n\n return [\n 'DE ESAS',\n 'DE ESTAS',\n 'DE LOS ANTERIORES',\n 'DE LAS ANTERIORES',\n 'LA ANTERIOR',\n 'EL ANTERIOR',\n 'LA PRIMERA',\n 'LA SEGUNDA',\n 'LA TERCERA',\n 'ESAS',\n 'ESTAS',\n 'LOS QUE ME MOSTRASTE',\n 'LAS QUE ME MOSTRASTE',\n ].some(x => t.includes(x));\n}\n\nfunction detectarEstado(texto) {\n const t = norm(texto);\n\n if (t.includes('NO APROBADA') || t.includes('NO APROBADAS')) {\n return 'NO APROBADA';\n }\n\n if (t.includes('APROBADA') || t.includes('APROBADAS')) {\n return 'APROBADA';\n }\n\n if (\n t.includes('PENDIENTE') ||\n t.includes('PENDIENTES') ||\n t.includes('PENDIENTE DE APROBACION')\n ) {\n return 'PENDIENTE DE APROBACION';\n }\n\n return '';\n}\n\nfunction detectarAnios(texto) {\n const matches = norm(texto).match(/\\b(20\\d{2}|19\\d{2})\\b/g);\n return matches ? [...new Set(matches)] : [];\n}\n\nfunction detectarPaises(texto) {\n const t = norm(texto);\n\n const paises = [\n 'EL SALVADOR',\n 'PANAMA',\n 'REPUBLICA DOMINICANA',\n 'COLOMBIA',\n 'PUERTO RICO',\n 'HONDURAS',\n 'MEXICO',\n 'VENEZUELA',\n 'JAMAICA',\n 'TRINIDAD Y TOBAGO',\n 'COSTA RICA',\n 'NICARAGUA',\n 'GUATEMALA',\n ];\n\n const encontrados = [];\n\n for (const pais of paises) {\n if (t.includes(norm(pais))) {\n encontrados.push(pais);\n }\n }\n\n if (/\\bRD\\b/.test(t) || t.includes('REP DOM') || t.includes('DOMINICANA')) {\n if (!encontrados.includes('REPUBLICA DOMINICANA')) {\n encontrados.push('REPUBLICA DOMINICANA');\n }\n }\n\n return encontrados;\n}\n\nfunction detectarCanales(texto) {\n const t = norm(texto);\n\n const canales = [];\n\n if (t.includes('MODERNO')) canales.push('MODERNO');\n if (t.includes('TRADICIONAL')) canales.push('TRADICIONAL');\n if (t.includes('ONLINE') || t.includes('ON LINE')) canales.push('ONLINE');\n if (t.includes('PDV') || t.includes('PUNTO DE VENTA')) canales.push('PDV');\n if (t.includes('NO APLICA')) canales.push('NO APLICA');\n\n return canales;\n}\n\nconst intencionEspecial = detectarIntencionEspecial(pregunta);\n\nif (intencionEspecial) {\n return [\n {\n json: {\n source_type: 'propuestas',\n pregunta,\n intencion_especial: intencionEspecial.tipo,\n tokens: [],\n cantidad_solicitada: null,\n quiere_ultimas: false,\n es_seguimiento: false,\n estado_solicitado: '',\n paises_solicitados: [],\n anios_solicitados: [],\n canales_solicitados: [],\n total_propuestas: rows.length,\n total_candidatos: 0,\n sin_resultados: false,\n candidatos: [],\n contexto_propuestas: '',\n prompt_usuario: intencionEspecial.prompt\n }\n }\n ];\n}\n\nconst cantidadSolicitada = extraerCantidad(pregunta);\nconst quiereUltimas = detectarUltimas(pregunta);\nconst esSeguimiento = detectarSeguimiento(pregunta);\nconst estadoSolicitado = detectarEstado(pregunta);\nconst aniosSolicitados = detectarAnios(pregunta);\nconst paisesSolicitados = detectarPaises(pregunta);\nconst canalesSolicitados = detectarCanales(pregunta);\n\nconst stopwords = new Set([\n 'DAME', 'DARME', 'MUESTRA', 'MUESTRAME', 'BUSCA', 'BUSCAME',\n 'PROPUESTA', 'PROPUESTAS', 'IDEA', 'IDEAS', 'DOCUMENTO', 'DOCUMENTOS',\n 'ULTIMA', 'ULTIMAS', 'ULTIMO', 'ULTIMOS', 'RECIENTE', 'RECIENTES',\n 'NUEVA', 'NUEVAS', 'NUEVO', 'NUEVOS',\n 'APROBADA', 'APROBADAS', 'PENDIENTE', 'PENDIENTES',\n 'DE', 'DEL', 'LA', 'LAS', 'EL', 'LOS', 'PARA', 'POR', 'CON',\n 'EN', 'UN', 'UNA', 'Y', 'O', 'QUE', 'ME', 'TENGO', 'TIENES',\n 'ALGUNA', 'ALGUNAS', 'ALGUN', 'SOBRE', 'RELACIONADA', 'RELACIONADAS',\n 'ESAS', 'ESTAS', 'ESOS', 'ESTOS',\n]);\n\nconst tokens = preguntaNorm\n .split(/[^A-Z0-9Ñ]+/)\n .map(t => t.trim())\n .filter(t => t.length >= 3 && !stopwords.has(t))\n .filter(t => !aniosSolicitados.includes(t));\n\nfunction getYear(value) {\n const year = clean(value).match(/\\b(20\\d{2}|19\\d{2})\\b/);\n return year ? Number(year[1]) : 0;\n}\n\nfunction fieldContains(value, expected) {\n return norm(value).includes(norm(expected));\n}\n\nfunction matchesAny(value, expectedList) {\n if (!expectedList.length) return true;\n return expectedList.some(expected => fieldContains(value, expected));\n}\n\nfunction hardFilterPass(p) {\n if (paisesSolicitados.length && !matchesAny(p.PAIS, paisesSolicitados)) {\n return false;\n }\n\n if (aniosSolicitados.length && !aniosSolicitados.includes(String(p.ANIO))) {\n return false;\n }\n\n if (estadoSolicitado && norm(p.APROBADA) !== norm(estadoSolicitado)) {\n return false;\n }\n\n if (canalesSolicitados.length && !matchesAny(p.CANAL, canalesSolicitados)) {\n return false;\n }\n\n return true;\n}\n\nfunction scoreRow(row) {\n const camposGenerales = [\n row.NOMBRE,\n row.CLIENTE,\n row.MARCA,\n row.PAIS,\n row['TIPO DE ACCION'],\n row.CANAL,\n row['AMBIENTE DE COMPRA (RE)'],\n row['TÁCTICA PROMOCIONAL'],\n row.APROBADA,\n row.ETIQUETAS,\n row['AÑO'],\n row.Descripcion,\n row.nombre_archivo,\n ];\n\n const camposFuertes = [\n row.NOMBRE,\n row.CLIENTE,\n row.MARCA,\n row.PAIS,\n row['TIPO DE ACCION'],\n row.ETIQUETAS,\n row['AÑO'],\n ];\n\n const textoGeneral = norm(camposGenerales.join(' '));\n const textoFuerte = norm(camposFuertes.join(' '));\n\n let score = 0;\n\n for (const token of tokens) {\n if (textoGeneral.includes(token)) score += 1;\n if (textoFuerte.includes(token)) score += 2;\n }\n\n if (paisesSolicitados.length && matchesAny(row.PAIS, paisesSolicitados)) {\n score += 8;\n }\n\n if (aniosSolicitados.length && aniosSolicitados.includes(clean(row['AÑO']))) {\n score += 8;\n }\n\n if (estadoSolicitado && norm(row.APROBADA) === norm(estadoSolicitado)) {\n score += 8;\n }\n\n if (canalesSolicitados.length && matchesAny(row.CANAL, canalesSolicitados)) {\n score += 5;\n }\n\n return score;\n}\n\nconst propuestas = rows\n .map((item, index) => {\n const row = item.json || {};\n\n const fila =\n row.row_number ||\n row.__row_number ||\n row.__rowNumber ||\n row.rowNumber ||\n index + 2;\n\n const anio = clean(row['AÑO']);\n\n return {\n fila,\n score: scoreRow(row),\n\n NOMBRE: clean(row.NOMBRE),\n CLIENTE: clean(row.CLIENTE),\n MARCA: clean(row.MARCA),\n PAIS: clean(row.PAIS),\n TIPO_DE_ACCION: clean(row['TIPO DE ACCION']),\n CANAL: clean(row.CANAL),\n AMBIENTE_DE_COMPRA: clean(row['AMBIENTE DE COMPRA (RE)']),\n TACTICA_PROMOCIONAL: clean(row['TÁCTICA PROMOCIONAL']),\n APROBADA: clean(row.APROBADA),\n ETIQUETAS: clean(row.ETIQUETAS),\n ANIO: anio,\n ANIO_NUM: getYear(anio),\n ENLACE: clean(row['Enlace a la propuesta']),\n DESCRIPCION: clean(row.Descripcion),\n };\n })\n .filter(p => p.NOMBRE || p.DESCRIPCION || p.ENLACE);\n\nlet candidatos = propuestas\n .filter(p => hardFilterPass(p))\n .filter(p => {\n if (tokens.length > 0) return p.score > 0;\n\n if (\n paisesSolicitados.length ||\n aniosSolicitados.length ||\n estadoSolicitado ||\n canalesSolicitados.length\n ) {\n return true;\n }\n\n return false;\n });\n\nif (candidatos.length === 0 && tokens.length === 0 && !esSeguimiento) {\n candidatos = [...propuestas];\n}\n\nif (quiereUltimas) {\n candidatos = candidatos.sort((a, b) => b.fila - a.fila);\n} else {\n candidatos = candidatos.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.fila - a.fila;\n });\n}\n\nconst limiteCandidatos = cantidadSolicitada\n ? Math.min(Math.max(cantidadSolicitada, 1), 25)\n : MAX_CANDIDATOS_DEFAULT;\n\ncandidatos = candidatos.slice(0, limiteCandidatos);\n\nconst contexto = candidatos.map((p, index) => {\n return [\n `RESULTADO ${index + 1}`,\n `Fila: ${p.fila}`,\n `Nombre: ${p.NOMBRE}`,\n `Cliente: ${p.CLIENTE}`,\n `Marca: ${p.MARCA}`,\n `País: ${p.PAIS}`,\n `Tipo de acción: ${p.TIPO_DE_ACCION}`,\n `Canal: ${p.CANAL}`,\n `Ambiente de compra: ${p.AMBIENTE_DE_COMPRA}`,\n `Táctica promocional: ${p.TACTICA_PROMOCIONAL}`,\n `Estado aprobación: ${p.APROBADA}`,\n `Etiquetas: ${p.ETIQUETAS}`,\n `Año: ${p.ANIO}`,\n `Descripción: ${p.DESCRIPCION}`,\n `Enlace: ${p.ENLACE}`,\n ].join('\\n');\n}).join('\\n\\n---\\n\\n');\n\nconst sinResultados = candidatos.length === 0;\n\nconst promptUsuario = `\nPregunta del usuario:\n${pregunta}\n\nInstrucción de búsqueda detectada:\n- Cantidad solicitada: ${cantidadSolicitada || 'No especificada'}\n- Quiere últimas/recientes: ${quiereUltimas ? 'Sí' : 'No'}\n- Es pregunta de seguimiento: ${esSeguimiento ? 'Sí' : 'No'}\n- Estado solicitado: ${estadoSolicitado || 'No especificado'}\n- Países solicitados: ${paisesSolicitados.length ? paisesSolicitados.join(', ') : 'No especificado'}\n- Años solicitados: ${aniosSolicitados.length ? aniosSolicitados.join(', ') : 'No especificado'}\n- Canales solicitados: ${canalesSolicitados.length ? canalesSolicitados.join(', ') : 'No especificado'}\n\nResultado de búsqueda:\n${sinResultados ? 'No se encontraron propuestas candidatas con los filtros detectados.' : 'Se encontraron propuestas candidatas.'}\n\nContexto encontrado en el banco de propuestas:\n${contexto || 'No se encontraron propuestas candidatas con la búsqueda.'}\n\nInstrucción para responder:\n${sinResultados\n ? 'Indica que no encontraste coincidencias. No inventes propuestas. Sugiere probar con otro cliente, marca, país, año, canal o tipo de acción.'\n : 'Responde usando únicamente las propuestas del contexto.'}\n`;\n\nreturn [\n {\n json: {\n source_type: 'propuestas',\n pregunta,\n tokens,\n cantidad_solicitada: cantidadSolicitada,\n quiere_ultimas: quiereUltimas,\n es_seguimiento: esSeguimiento,\n estado_solicitado: estadoSolicitado,\n paises_solicitados: paisesSolicitados,\n anios_solicitados: aniosSolicitados,\n canales_solicitados: canalesSolicitados,\n total_propuestas: propuestas.length,\n total_candidatos: candidatos.length,\n sin_resultados: sinResultados,\n intencion_especial: '',\n candidatos,\n contexto_propuestas: contexto,\n prompt_usuario: promptUsuario,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 336, 1104 ], "id": "7773a0d7-596e-4deb-8f2e-9775ae06b07a", "name": "Code - Filtrar propuestas relevantes" }, { "parameters": { "promptType": "define", "text": "={{ $json.prompt_usuario }}", "options": { "systemMessage": "=Eres Fulgencio Fumado, asistente interno de creatividad, briefs y propuestas de GomezLee Marketing.\n\nTu función es responder preguntas usando únicamente el contexto entregado por el sistema.\n\nActualmente puedes recibir contexto de dos fuentes:\n\n1. Banco de propuestas:\nContiene propuestas, presentaciones, activaciones, casos, campañas y referencias internas de GomezLee Marketing.\n\n2. CDC Briefs:\nContiene briefs, solicitudes creativas y requerimientos generados desde la app CDC Brief.\n\nNo confundas ambas fuentes:\n- Una propuesta es un caso, presentación, activación o referencia interna del banco de propuestas.\n- Un brief es una solicitud o requerimiento creativo.\n- Si el usuario pregunta por propuestas, casos, activaciones, presentaciones o campañas, prioriza el banco de propuestas.\n- Si el usuario pregunta por briefs, solicitudes, requerimientos creativos, pedidos o temas registrados en CDC Brief, prioriza CDC Briefs.\n- Si hay información útil en ambas fuentes, separa la respuesta por secciones:\n Banco de propuestas\n CDC Briefs\n\nReglas obligatorias:\n1. No inventes propuestas, briefs, enlaces, países, clientes, marcas, años, estados, fechas ni solicitantes.\n2. Si no hay información suficiente en el contexto, dilo claramente.\n3. No digas que puedes crear imágenes. La creación de imágenes no está disponible actualmente en Fulgencio.\n4. No menciones n8n, workflows, Google Sheets, nodos, prompts, memoria técnica ni procesos internos.\n5. Responde en español, con tono profesional, cercano, directo y útil. Evita sonar demasiado formal o robótico.\n6. Cuando incluyas enlaces, ponlos completos.\n7. Si algún dato aparece como PENDIENTE, vacío o no disponible, no lo inventes; indica que ese dato está pendiente o no disponible.\n8. Usa únicamente la información entregada en el contexto o en la memoria reciente cuando sea una pregunta de seguimiento.\n9. No prometas enviar archivos, crear documentos, generar imágenes, modificar propuestas, actualizar información o hacer cambios en sistemas. Solo puedes consultar, resumir, comparar y sugerir usando el contexto disponible.\n10. Si el usuario combina varios filtros y hay pocos o ningún resultado, explica qué filtros se aplicaron y sugiere quitar o cambiar algún filtro.\n\nReglas para propuestas:\n- Si el usuario pide propuestas, entrega una lista clara con:\n - Nombre\n - Cliente/Marca\n - País\n - Año si está disponible\n - Tipo de acción si está disponible\n - Breve descripción\n - Enlace\n- Si una propuesta tiene enlace disponible, inclúyelo siempre.\n- Si no tiene enlace, indica “Enlace no disponible”.\n- Si el usuario pide una cantidad específica, respeta esa cantidad cuando el contexto la tenga disponible.\n- Si el usuario pide las últimas o más recientes, respeta el orden entregado en el contexto.\n- Cuando la instrucción de búsqueda detectada indique país, año, canal, estado o cantidad solicitada, respeta esos filtros estrictamente.\n- Si no hay candidatos de propuestas en el contexto, no inventes resultados. Indica que no encontraste coincidencias y sugiere ajustar la búsqueda por cliente, marca, país, año, canal, estado o tipo de acción.\n\nReglas para CDC Briefs:\n- Si el usuario pide briefs, solicitudes, requerimientos o pedidos creativos existentes, responde usando la sección CDC Briefs del contexto.\n- Cuando respondas sobre briefs, incluye cuando esté disponible:\n - Brief ID\n - Fecha de creación\n - Solicitante\n - Correo\n - Estado\n - Fecha de entrega\n - Resumen\n - Link del documento brief\n - Link de carpeta Drive\n- No trates un brief como si fuera una propuesta.\n- No trates una propuesta como si fuera un brief.\n- Si el usuario pide los últimos briefs o briefs recientes, respeta el orden entregado en el contexto.\n- Si no hay briefs candidatos en el contexto, indica que no encontraste briefs relacionados y sugiere buscar por brief ID, solicitante, tema, estado o fecha.\n- Si el usuario pide crear un brief, llenar un brief, hacer una nueva solicitud o abrir el formulario de brief, comparte este enlace:\nhttps://digitalcompass.agency/brief\n- Si el usuario solo pregunta por briefs existentes, no respondas únicamente con el enlace del formulario. Busca y responde con los briefs del contexto.\n\nFormato recomendado:\n- Resumen breve al inicio.\n- Lista numerada cuando aplique.\n- Si usas ambas fuentes, separa en secciones:\n Banco de propuestas\n CDC Briefs\n- Cierre breve ofreciendo filtrar por país, marca, cliente, año, estado, tipo de acción, brief ID, solicitante o tema.\n\nFormato especial:\n- Si el usuario pide “tabla”, “formato tabla” o “en una tabla”, responde directamente en tabla Markdown.\n- Para propuestas en tabla, usa columnas útiles como:\n Nombre, Cliente/Marca, País, Año, Tipo de acción y Enlace.\n- Para briefs en tabla, usa columnas útiles como:\n Brief ID, Fecha, Solicitante, Estado, Fecha de entrega, Resumen y Enlace.\n- Si el usuario pide bullets, responde en bullets.\n\nAnálisis, patrones e ideas:\n- Si el usuario pide análisis, patrones, insights o ideas creativas, analiza únicamente la información entregada en el contexto.\n- Resume patrones repetidos, tipos de activación, mecánicas, formatos, canales, oportunidades y temas relevantes sin inventar datos fuera del contexto.\n- Si el usuario pide ideas nuevas, usa el contexto como referencia.\n- Aclara que son ideas inspiradas en propuestas o briefs existentes y no propuestas ya aprobadas, salvo que el contexto indique lo contrario.\n\nComparaciones:\n- Si el usuario pide comparar propuestas, organiza la respuesta por diferencias y similitudes:\n - Tipo de acción\n - País\n - Marca\n - Canal\n - Objetivo\n - Elementos creativos\n- Si el usuario pide comparar briefs, organiza la respuesta por:\n - Solicitante\n - Estado\n - Fecha de entrega\n - Objetivo o tema\n - Materiales o insumos disponibles\n - Diferencias principales\n- Usa solo el contexto disponible.\n\nSeguimiento y memoria:\n- Si el usuario dice “de esas”, “la segunda”, “la anterior”, “dame los enlaces”, “resume esas”, “de esos briefs” o “las que me mostraste”, usa la memoria reciente para entender la referencia.\n- Responde solo con información que aparezca en el contexto o memoria entregada.\n- No mezcles resultados nuevos con resultados de memoria si el usuario claramente se refiere a resultados ya mostrados.\n- Si no puedes identificar a qué se refiere, pide una aclaración breve.\n\nMensajes ambiguos:\n- Si el usuario envía un mensaje vacío, muy corto o ambiguo como “hola”, “buenas”, “ayuda” o “qué puedes hacer”, responde explicando brevemente cómo puede usar Fulgencio y da ejemplos de preguntas útiles.\n- Ejemplos de preguntas útiles:\n 1. Dame las 5 últimas propuestas de Motorola.\n 2. Busca propuestas de Nestlé en República Dominicana.\n 3. Dame propuestas aprobadas de activación en PDV.\n 4. Tienes propuestas de Kit Kat en Costa Rica?\n 5. Dame los últimos briefs creados.\n 6. Busca briefs relacionados con Motorola.\n 7. Necesito crear un brief.\n 8. Necesito acceder al Tablero CDC.\n 9. ¿Dónde registro un proyecto creativo?\n\nImágenes:\n- Si el usuario solicita crear o generar imágenes, indica que la creación de imágenes no está disponible actualmente en Fulgencio.\n- Ofrece buscar referencias, propuestas, briefs o ideas existentes dentro del contexto disponible.\n\nBrief nuevo:\n- Si el usuario pide crear un brief, llenar un brief, hacer una solicitud creativa o abrir el formulario, comparte este enlace:\nhttps://digitalcompass.agency/brief\n\nTablero CDC / Aprobaciones Proyectos:\n- Si el usuario pide acceder al Tablero CDC, abrir Aprobaciones Proyectos, entrar al tablero de proyectos o registrar un proyecto creativo, comparte este enlace:\nhttps://digitalcompass.agency/tablero-cdc/\n- Indica brevemente que corresponde al Tablero CDC / Aprobaciones Proyectos.\n- No afirmes que puedes crear, modificar, aprobar, eliminar o consultar proyectos dentro del Tablero CDC desde el chat.\n- No confundas el Tablero CDC con CDC Brief ni con el Banco de Propuestas.\n\nRestricción final:\nSi el contexto indica que no se encontraron candidatos, no inventes resultados. Responde que no encontraste coincidencias y sugiere ajustar la búsqueda según la fuente:\n- Para propuestas: cliente, marca, país, año, canal, estado o tipo de acción.\n- Para briefs: brief ID, solicitante, estado, fecha, tema o palabras clave del brief." } }, "type": "@n8n/n8n-nodes-langchain.agent", "typeVersion": 2, "position": [ 1136, 1088 ], "id": "9eaa8c05-9abd-493f-8b3d-f9ba212fcc81", "name": "AI Agent - Gemini Fulgencio" }, { "parameters": { "modelName": "models/gemini-3.1-flash-lite", "options": {} }, "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", "typeVersion": 1, "position": [ 1136, 1312 ], "id": "6e26bca2-35f2-41ba-b73a-d7a718fe3471", "name": "Google Gemini Chat Model", "credentials": { "googlePalmApi": { "id": "6Jdcd9FONxG35YuJ", "name": "Isaac - Gemini Api Gratis" } } }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const data = $json || {};\n\nlet respuesta =\n data.output ||\n data.text ||\n data.response ||\n data.content ||\n data.message ||\n '';\n\nrespuesta = String(respuesta || '').trim();\n\nif (!respuesta) {\n respuesta = 'No pude generar una respuesta con la información disponible.';\n}\n\nconst MAX_CHARS = 7000;\n\nif (respuesta.length > MAX_CHARS) {\n respuesta =\n respuesta.slice(0, MAX_CHARS) +\n '\\n\\nRespuesta recortada por longitud. Puedes pedirme un filtro más específico por cliente, marca, país, año, estado o tipo de acción.';\n}\n\nreturn {\n json: {\n respuesta\n }\n};" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1488, 1088 ], "id": "709bd05c-3649-4441-9ac1-3db31d681639", "name": "Code - Formatear respuesta Chat" }, { "parameters": { "authentication": "oAuth2", "spaceId": "={{ $('Code - Preparar mensaje').first().json.space_name }}", "messageUi": { "text": "={{ $json.respuesta }}" }, "additionalFields": {} }, "type": "n8n-nodes-base.googleChat", "typeVersion": 1, "position": [ 1744, 1088 ], "id": "02fa53e1-d7db-4adf-8656-65cc4470624e", "name": "Chat - Enviar respuesta", "webhookId": "bc07f187-75cb-40aa-a145-75bfe88d9d5e", "credentials": { "googleChatOAuth2Api": { "id": "V4eVQvEoy5QcBvUu", "name": "Chat account" } } }, { "parameters": { "sessionIdType": "customKey", "sessionKey": "={{ $('Code - Preparar mensaje').first().json.thread_name || ($('Code - Preparar mensaje').first().json.space_name + '::' + $('Code - Preparar mensaje').first().json.usuario_email) }}", "contextWindowLength": 6 }, "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", "typeVersion": 1.3, "position": [ 1280, 1312 ], "id": "0c3cce29-08b6-4708-a7df-c85272c4fb25", "name": "Simple Memory" }, { "parameters": { "content": "💬 Entrada Google Chat\n\nRecibe el mensaje del usuario desde Google Chat, responde rápido con “procesando” y prepara los datos de la conversación: pregunta, usuario, espacio, hilo y session key.", "height": 448, "width": 608, "color": 2 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -736, 928 ], "id": "36e88459-cdd1-4fea-ba12-855daefb5d66", "name": "Sticky Note" }, { "parameters": { "content": "🔎 Búsqueda y contexto de Fulgencio\n\nLee el banco de propuestas y el Sheet de CDC Briefs.\n\nFiltra candidatos relevantes antes de enviarlos a Gemini:\n- propuestas, casos, activaciones y campañas\n- briefs, solicitudes creativas y requerimientos\n\nLuego une ambas fuentes en un solo contexto, manteniéndolas separadas para no mezclar propuestas con briefs.", "height": 544, "width": 960, "color": 4 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 0, 912 ], "id": "e4f025cd-9b7c-4de8-bfbd-51a163a56f74", "name": "Sticky Note1" }, { "parameters": { "content": "🤖 Respuesta con Gemini\n\nGemini responde usando solo el contexto filtrado y combinado.\n\nPuede responder sobre propuestas y CDC Briefs, manteniendo cada fuente separada cuando aplique.\n\nNo crea imágenes, no inventa datos y conserva enlaces reales cuando estén disponibles.", "height": 544, "width": 864, "color": 5 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 1072, 944 ], "id": "10906b1f-a3d0-476e-81f5-e2d3b5c4467d", "name": "Sticky Note2" }, { "parameters": { "documentId": { "__rl": true, "value": "1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU", "mode": "list", "cachedResultName": "CDC Brief - Base para Fulgencio", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU/edit?usp=drivesdk" }, "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "Briefs", "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1h_d4l4FYkLvyy_Zp7bEQFKpy5hx6pVj9xZ98mjMHHSU/edit#gid=0" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 80, 1296 ], "id": "b3f61adb-199d-4f90-8187-9efb92f72378", "name": "Sheets - Leer briefs CDC", "credentials": { "googleSheetsOAuth2Api": { "id": "K0hDZh3a85MpOHCs", "name": "Google Sheets account 2" } } }, { "parameters": { "jsCode": "const rows = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst MAX_BRIEFS_DEFAULT = 10;\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction norm(value) {\n return clean(value)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase();\n}\n\nconst preguntaNorm = norm(pregunta);\n\nfunction extraerCantidad(texto) {\n const t = norm(texto);\n\n const numero = t.match(/\\b(\\d{1,2})\\b/);\n if (numero) return Number(numero[1]);\n\n const palabras = {\n UNA: 1,\n UNO: 1,\n DOS: 2,\n TRES: 3,\n CUATRO: 4,\n CINCO: 5,\n SEIS: 6,\n SIETE: 7,\n OCHO: 8,\n NUEVE: 9,\n DIEZ: 10,\n };\n\n for (const [palabra, valor] of Object.entries(palabras)) {\n if (t.includes(palabra)) return valor;\n }\n\n return null;\n}\n\nfunction detectarUltimas(texto) {\n const t = norm(texto);\n\n return [\n 'ULTIMA',\n 'ULTIMAS',\n 'ULTIMO',\n 'ULTIMOS',\n 'RECIENTE',\n 'RECIENTES',\n 'NUEVA',\n 'NUEVAS',\n 'NUEVO',\n 'NUEVOS',\n ].some(x => t.includes(x));\n}\n\nfunction detectarInteresBriefs(texto) {\n const t = norm(texto);\n\n return [\n 'BRIEF',\n 'BRIEFS',\n 'SOLICITUD',\n 'SOLICITUDES',\n 'REQUERIMIENTO',\n 'REQUERIMIENTOS',\n 'PEDIDO CREATIVO',\n 'CDC',\n 'FORMULARIO',\n ].some(x => t.includes(x));\n}\n\nfunction detectarInteresPropuestas(texto) {\n const t = norm(texto);\n\n return [\n 'PROPUESTA',\n 'PROPUESTAS',\n 'BANCO DE PROPUESTAS',\n 'CASO',\n 'CASOS',\n 'ACTIVACION',\n 'ACTIVACIONES',\n ].some(x => t.includes(x));\n}\n\nconst cantidadSolicitada = extraerCantidad(pregunta);\nconst quiereUltimas = detectarUltimas(pregunta);\nconst mencionaBriefs = detectarInteresBriefs(pregunta);\nconst mencionaPropuestas = detectarInteresPropuestas(pregunta);\n\nconst stopwords = new Set([\n 'DAME', 'DARME', 'MUESTRA', 'MUESTRAME', 'BUSCA', 'BUSCAME',\n 'BRIEF', 'BRIEFS', 'SOLICITUD', 'SOLICITUDES', 'REQUERIMIENTO', 'REQUERIMIENTOS',\n 'PROPUESTA', 'PROPUESTAS', 'DOCUMENTO', 'DOCUMENTOS',\n 'ULTIMA', 'ULTIMAS', 'ULTIMO', 'ULTIMOS', 'RECIENTE', 'RECIENTES',\n 'NUEVA', 'NUEVAS', 'NUEVO', 'NUEVOS',\n 'DE', 'DEL', 'LA', 'LAS', 'EL', 'LOS', 'PARA', 'POR', 'CON',\n 'EN', 'UN', 'UNA', 'Y', 'O', 'QUE', 'ME', 'TENGO', 'TIENES',\n 'ALGUNA', 'ALGUNAS', 'ALGUN', 'SOBRE', 'RELACIONADA', 'RELACIONADAS',\n 'CDC', 'FORMULARIO',\n]);\n\nconst tokens = preguntaNorm\n .split(/[^A-Z0-9Ñ]+/)\n .map(t => t.trim())\n .filter(t => t.length >= 3 && !stopwords.has(t));\n\nfunction getDateValue(value) {\n const raw = clean(value);\n const d = new Date(raw);\n return Number.isNaN(d.getTime()) ? 0 : d.getTime();\n}\n\nfunction scoreBrief(row) {\n const camposFuertes = [\n row.brief_id,\n row.nombre_carpeta,\n row.enviado_por,\n row.correo,\n row.estado,\n ];\n\n const camposGenerales = [\n row.brief_id,\n row.nombre_carpeta,\n row.enviado_por,\n row.correo,\n row.estado,\n row.resumen_chatbot,\n row.texto_brief,\n row.texto_busqueda,\n row.fecha_entrega,\n ];\n\n const textoFuerte = norm(camposFuertes.join(' '));\n const textoGeneral = norm(camposGenerales.join(' '));\n\n let score = 0;\n\n for (const token of tokens) {\n if (textoGeneral.includes(token)) score += 1;\n if (textoFuerte.includes(token)) score += 2;\n }\n\n if (mencionaBriefs) score += 3;\n\n return score;\n}\n\nconst debeBuscarBriefs = mencionaBriefs || (!mencionaPropuestas && tokens.length > 0);\n\nlet briefs = rows\n .map((item, index) => {\n const row = item.json || {};\n\n return {\n fila: row.row_number || row.__row_number || row.__rowNumber || row.rowNumber || index + 2,\n score: scoreBrief(row),\n\n fecha_creacion: clean(row.fecha_creacion),\n brief_id: clean(row.brief_id),\n nombre_carpeta: clean(row.nombre_carpeta),\n enviado_por: clean(row.enviado_por),\n correo: clean(row.correo),\n link_carpeta_drive: clean(row.link_carpeta_drive),\n link_documento_brief: clean(row.link_documento_brief),\n estado: clean(row.estado),\n cantidad_audios: clean(row.cantidad_audios),\n cantidad_imagenes: clean(row.cantidad_imagenes),\n cantidad_documentos: clean(row.cantidad_documentos),\n cantidad_links: clean(row.cantidad_links),\n resumen_chatbot: clean(row.resumen_chatbot),\n texto_brief: clean(row.texto_brief),\n texto_busqueda: clean(row.texto_busqueda),\n fecha_entrega: clean(row.fecha_entrega),\n\n fecha_sort: getDateValue(row.fecha_creacion),\n };\n })\n .filter(b => b.brief_id || b.nombre_carpeta || b.texto_brief || b.resumen_chatbot);\n\nlet candidatos = [];\n\nif (debeBuscarBriefs) {\n candidatos = briefs.filter(b => {\n if (tokens.length > 0) return b.score > 0;\n if (mencionaBriefs) return true;\n return false;\n });\n}\n\nif (quiereUltimas) {\n candidatos = candidatos.sort((a, b) => {\n if (b.fecha_sort !== a.fecha_sort) return b.fecha_sort - a.fecha_sort;\n return b.fila - a.fila;\n });\n} else {\n candidatos = candidatos.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n if (b.fecha_sort !== a.fecha_sort) return b.fecha_sort - a.fecha_sort;\n return b.fila - a.fila;\n });\n}\n\nconst limite = cantidadSolicitada\n ? Math.min(Math.max(cantidadSolicitada, 1), 25)\n : MAX_BRIEFS_DEFAULT;\n\ncandidatos = candidatos.slice(0, limite);\n\nconst contextoBriefs = candidatos.map((b, index) => {\n return [\n `BRIEF ${index + 1}`,\n `Fila: ${b.fila}`,\n `Fecha creación: ${b.fecha_creacion}`,\n `Brief ID: ${b.brief_id}`,\n `Nombre carpeta: ${b.nombre_carpeta}`,\n `Enviado por: ${b.enviado_por}`,\n `Correo: ${b.correo}`,\n `Estado: ${b.estado}`,\n `Fecha entrega: ${b.fecha_entrega}`,\n `Cantidad audios: ${b.cantidad_audios}`,\n `Cantidad imágenes: ${b.cantidad_imagenes}`,\n `Cantidad documentos: ${b.cantidad_documentos}`,\n `Cantidad links: ${b.cantidad_links}`,\n `Resumen chatbot: ${b.resumen_chatbot}`,\n `Texto brief: ${b.texto_brief}`,\n `Texto búsqueda: ${b.texto_busqueda}`,\n `Link carpeta Drive: ${b.link_carpeta_drive}`,\n `Link documento brief: ${b.link_documento_brief}`,\n ].join('\\n');\n}).join('\\n\\n---\\n\\n');\n\nreturn [\n {\n json: {\n source_type: 'briefs_cdc',\n pregunta,\n tokens,\n menciona_briefs: mencionaBriefs,\n menciona_propuestas: mencionaPropuestas,\n cantidad_solicitada: cantidadSolicitada,\n quiere_ultimas: quiereUltimas,\n total_briefs: briefs.length,\n total_briefs_candidatos: candidatos.length,\n briefs_candidatos: candidatos,\n contexto_briefs: contextoBriefs,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 288, 1296 ], "id": "a18ef7c8-078c-4d9f-a132-a4518191ba43", "name": "Code - Filtrar briefs CDC" }, { "parameters": {}, "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ 592, 1136 ], "id": "6e4765b5-9cd2-4bb9-b702-95df5b4af5bc", "name": "Merge - Unir fuentes Fulgencio" }, { "parameters": { "jsCode": "const items = $input.all();\n\nconst pregunta = $('Code - Preparar mensaje').first().json.pregunta || '';\n\nconst propuestasItem =\n items.find(i => i.json.source_type === 'propuestas')?.json || {};\n\nconst briefsItem =\n items.find(i => i.json.source_type === 'briefs_cdc')?.json || {};\n\nif (propuestasItem.intencion_especial) {\n return [\n {\n json: {\n ...propuestasItem,\n prompt_usuario: propuestasItem.prompt_usuario,\n total_fuentes: 1,\n fuentes_usadas: ['intencion_especial'],\n },\n },\n ];\n}\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction construirContextoPropuestas(candidatos = []) {\n if (!Array.isArray(candidatos) || candidatos.length === 0) return '';\n\n return candidatos.map((p, index) => {\n return [\n `PROPUESTA ${index + 1}`,\n `Fila: ${clean(p.fila)}`,\n `Nombre: ${clean(p.NOMBRE)}`,\n `Cliente: ${clean(p.CLIENTE)}`,\n `Marca: ${clean(p.MARCA)}`,\n `País: ${clean(p.PAIS)}`,\n `Tipo de acción: ${clean(p.TIPO_DE_ACCION)}`,\n `Canal: ${clean(p.CANAL)}`,\n `Ambiente de compra: ${clean(p.AMBIENTE_DE_COMPRA)}`,\n `Táctica promocional: ${clean(p.TACTICA_PROMOCIONAL)}`,\n `Estado aprobación: ${clean(p.APROBADA)}`,\n `Etiquetas: ${clean(p.ETIQUETAS)}`,\n `Año: ${clean(p.ANIO)}`,\n `Descripción: ${clean(p.DESCRIPCION)}`,\n `Enlace: ${clean(p.ENLACE)}`,\n ].join('\\n');\n }).join('\\n\\n---\\n\\n');\n}\n\nconst contextoPropuestas =\n clean(propuestasItem.contexto_propuestas) ||\n construirContextoPropuestas(propuestasItem.candidatos || []);\n\nconst contextoBriefs = clean(briefsItem.contexto_briefs);\n\nconst totalPropuestas = Number(propuestasItem.total_candidatos || 0);\nconst totalBriefs = Number(briefsItem.total_briefs_candidatos || 0);\n\nconst sinResultados = totalPropuestas === 0 && totalBriefs === 0;\n\nconst promptUsuario = `\nPregunta del usuario:\n${pregunta}\n\nFuentes disponibles para responder:\n1. Banco de propuestas:\n - Contiene propuestas, casos, presentaciones, activaciones y referencias internas de GomezLee Marketing.\n - Total de candidatos encontrados: ${totalPropuestas}\n\n2. CDC Briefs:\n - Contiene briefs y solicitudes creativas generadas desde la app CDC Brief.\n - Total de candidatos encontrados: ${totalBriefs}\n\nReglas para elegir fuente:\n- Si el usuario pregunta por propuestas, casos, activaciones o presentaciones, prioriza el Banco de propuestas.\n- Si el usuario pregunta por briefs, solicitudes, requerimientos creativos, pedidos o formularios CDC, prioriza CDC Briefs.\n- Si la pregunta es general y hay información relevante en ambas fuentes, puedes mencionar ambas separadas por sección.\n- No mezcles un brief con una propuesta como si fueran lo mismo.\n- Un brief es una solicitud o requerimiento creativo.\n- Una propuesta es una presentación/caso del banco de propuestas.\n- Usa únicamente la información entregada abajo.\n- No inventes enlaces ni datos.\n\nResultado de búsqueda:\n${sinResultados ? 'No se encontraron candidatos en ninguna fuente.' : 'Se encontraron candidatos en una o más fuentes.'}\n\nContexto del Banco de propuestas:\n${contextoPropuestas || 'No se encontraron propuestas candidatas.'}\n\nContexto de CDC Briefs:\n${contextoBriefs || 'No se encontraron briefs candidatos.'}\n\nInstrucción para responder:\n${sinResultados\n ? 'Indica que no encontraste coincidencias. Sugiere buscar por cliente, marca, país, año, tipo de acción, estado, brief ID, solicitante o tema del brief.'\n : 'Responde usando únicamente el contexto anterior. Si usas ambas fuentes, separa la respuesta por secciones.'}\n`;\n\nreturn [\n {\n json: {\n pregunta,\n\n source_type: 'contexto_fulgencio_combinado',\n\n total_propuestas_candidatas: totalPropuestas,\n total_briefs_candidatos: totalBriefs,\n sin_resultados: sinResultados,\n\n propuestas: propuestasItem.candidatos || [],\n briefs: briefsItem.briefs_candidatos || [],\n\n contexto_propuestas: contextoPropuestas,\n contexto_briefs: contextoBriefs,\n\n prompt_usuario: promptUsuario,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 800, 1136 ], "id": "7c046244-6f10-4d74-a374-099f211ac26b", "name": "Code - Construir contexto Fulgencio" } ], "connections": { "Webhook": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] }, "Respond to Webhook": { "main": [ [ { "node": "Code - Preparar mensaje", "type": "main", "index": 0 } ] ] }, "Code - Preparar mensaje": { "main": [ [ { "node": "Sheets - Leer propuestas", "type": "main", "index": 0 }, { "node": "Sheets - Leer briefs CDC", "type": "main", "index": 0 } ] ] }, "Sheets - Leer propuestas": { "main": [ [ { "node": "Code - Filtrar propuestas relevantes", "type": "main", "index": 0 } ] ] }, "Code - Filtrar propuestas relevantes": { "main": [ [ { "node": "Merge - Unir fuentes Fulgencio", "type": "main", "index": 0 } ] ] }, "AI Agent - Gemini Fulgencio": { "main": [ [ { "node": "Code - Formatear respuesta Chat", "type": "main", "index": 0 } ] ] }, "Google Gemini Chat Model": { "ai_languageModel": [ [ { "node": "AI Agent - Gemini Fulgencio", "type": "ai_languageModel", "index": 0 } ] ] }, "Code - Formatear respuesta Chat": { "main": [ [ { "node": "Chat - Enviar respuesta", "type": "main", "index": 0 } ] ] }, "Chat - Enviar respuesta": { "main": [ [] ] }, "Simple Memory": { "ai_memory": [ [ { "node": "AI Agent - Gemini Fulgencio", "type": "ai_memory", "index": 0 } ] ] }, "Sheets - Leer briefs CDC": { "main": [ [ { "node": "Code - Filtrar briefs CDC", "type": "main", "index": 0 } ] ] }, "Code - Filtrar briefs CDC": { "main": [ [ { "node": "Merge - Unir fuentes Fulgencio", "type": "main", "index": 1 } ] ] }, "Merge - Unir fuentes Fulgencio": { "main": [ [ { "node": "Code - Construir contexto Fulgencio", "type": "main", "index": 0 } ] ] }, "Code - Construir contexto Fulgencio": { "main": [ [ { "node": "AI Agent - Gemini Fulgencio", "type": "main", "index": 0 } ] ] } }, "authors": "Isaac Aracena", "name": "Version a60ee2c1", "description": "", "autosaved": true, "workflowPublishHistory": [ { "createdAt": "2026-06-19T17:30:42.387Z", "id": 1633, "workflowId": "s688ICOeW8Y8Q8IW", "versionId": "a60ee2c1-eb12-46e1-9dc6-3e9402087051", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" } ] } }