AutomationFlowsWeb Scraping › Chat Ia

Chat Ia

Chat Ia. Uses httpRequest. Webhook trigger; 28 nodes.

Webhook trigger★★★★☆ complexity28 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 28 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "parameters": {
        "options": {}
      },
      "id": "b46fe309-6f9f-4a4e-a0b7-2d4086e48bc3",
      "name": "Responder",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        368,
        928
      ]
    },
    {
      "parameters": {
        "functionCode": "const ans = $json; const routed = $items(\"Enrutar contexto por intenci\u00f3n\", 0, 0)?.[0]?.json || {}; return [{ json: { ok: ans.ok, answer: ans.answer, model: ans.model, usage: ans.usage, intent: routed.intent, period: routed.period, context: routed.context } }];"
      },
      "id": "82f54fb7-80c0-4b78-895f-bb5440c16edf",
      "name": "Construir salida",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        160,
        928
      ]
    },
    {
      "parameters": {
        "functionCode": "try { const r = $json || {}; let answer = 'Sin respuesta'; if (Array.isArray(r.choices) && r.choices[0]?.message?.content) answer = r.choices[0].message.content; return [{ json: { ok: true, answer, model: r.model || null, usage: r.usage || null } }]; } catch (e) { return [{ json: { ok: false, answer: 'Error parseando respuesta IA', error: String(e) } }]; }"
      },
      "id": "4d56267d-ea2c-4803-aa1d-f007ca5d50c5",
      "name": "Extraer respuesta LLM",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -48,
        928
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "https://api.groq.com/openai/v1/chat/completions",
        "jsonParameters": true,
        "options": {
          "ignoreResponseCode": true
        },
        "bodyParametersJson": "={{ { \"model\": $json.model, \"messages\": $json.messages, \"temperature\": 0.2, \"max_tokens\": 900 } }}",
        "headerParametersJson": "{ \"Authorization\": \"<redacted-credential>\", \"Content-Type\": \"application/json\", \"Accept\": \"application/json\" }"
      },
      "id": "1ad05621-5ccb-4db4-8087-8c4118dc1c83",
      "name": "Groq Chat",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -256,
        928
      ]
    },
    {
      "parameters": {
        "functionCode": "const ctx = $json;\nconst sistema = [\n  'Eres un asistente en ESPA\u00d1OL. Te llamas Teodoro',\n  'Si la pregunta trata de VENTAS/PRODUCTOS/FINANZAS, responde EXCLUSIVAMENTE con el CONTEXTO proporcionado. No inventes cifras.',\n  'Tipos de contexto: ventas -> m\u00e9tricas de ingresos/unidades/cliente/producto; vapers -> inventario/stock/precios/marcas/sabores; finanzas -> ingresos/gastos/beneficio/margen/serie financiera.',\n  'Si falta contexto para el periodo solicitado, dilo y ofrece global, \u00faltimos 7/30/90 d\u00edas (si aplica).',\n  'Responde claro, breve y accionable: bullets, cifras, tendencias y outliers.'\n].join('\\n');\n\nreturn [{ json: {\n  model: 'llama-3.3-70b-versatile',\n  messages: [\n    { role: 'system', content: sistema },\n    { role: 'user', content: `INTENCI\u00d3N: ${ctx.intent}` },\n    { role: 'user', content: `USUARIO: ${ctx.prompt}` },\n    { role: 'user', content: `CONTEXTO(${ctx.intent.toUpperCase()}): ${JSON.stringify({ period: ctx.period, context: ctx.context, sample: ctx.sample })}` }\n  ]\n} }];"
      },
      "id": "a53b04e3-c7a5-4afb-a34d-3c6390a2f244",
      "name": "Construir mensajes Chat",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -448,
        928
      ]
    },
    {
      "parameters": {
        "functionCode": "const base = $items(\"Normalizar input (chat libre)\", 0, 0)[0]?.json || {};\nconst intent = $items(\"Detectar intenci\u00f3n\", 0, 0)[0]?.json?.intent || 'ventas';\nconst ventasCtx = $items(\"Contexto ventas (agregados)\", 0, 0)[0]?.json || {};\nconst vapersCtx = $items(\"Contexto vapers (agregados)\", 0, 0)[0]?.json || {};\nconst finCtx = $items(\"Contexto finanzas (agregados)\", 0, 0)[0]?.json || {};\n\nlet ctxType = intent;\nlet context = null;\nlet sample = null;\n\nif (intent === 'vapers' && vapersCtx?.vapersContext) {\n  context = vapersCtx.vapersContext; sample = vapersCtx?.muestra ?? null;\n} else if (intent === 'finanzas' && finCtx?.finanzasContext) {\n  context = finCtx.finanzasContext; sample = finCtx?.serieDiaria ?? null;\n} else if (ventasCtx?.contextVentas) {\n  ctxType = 'ventas';\n  context = ventasCtx.contextVentas; sample = ventasCtx?.ventasSample ?? null;\n}\n\nreturn [{ json: { prompt: base.prompt, period: base.period || null, intent: ctxType, context, sample } }];"
      },
      "id": "77b44eee-3c26-4261-bfd8-4905c41b5b4c",
      "name": "Enrutar contexto por intenci\u00f3n",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -784,
        928
      ]
    },
    {
      "parameters": {
        "mode": "passThrough"
      },
      "id": "3aea2cf6-5e9d-4025-a32e-1ed007bf4daf",
      "name": "Merge (Todo + Finanzas)",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        -992,
        928
      ]
    },
    {
      "parameters": {
        "mode": "passThrough"
      },
      "id": "fe064607-3dd6-421e-9606-3cb0b7029770",
      "name": "Merge (Ventas/Vapers)",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        -1152,
        848
      ]
    },
    {
      "parameters": {
        "mode": "passThrough"
      },
      "id": "d25b5d56-576c-48cf-b122-1ceb51b8515b",
      "name": "Merge Ventas + Productos",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        -848,
        448
      ]
    },
    {
      "parameters": {
        "functionCode": "// Agregados globales + ventanas + por periodo (sin $items; usa merge previo)\ntry {\n  // Tras el \"Merge ventas + productos (Combine)\", todo viene en $json\n  const ventas = Array.isArray($json.ventas) ? $json.ventas : [];\n  const prompt = $json.prompt || '';\n  const period = $json.period || null;\n\n  // \u00cdndice de productos ya embebido por el Merge\n  const productosIndex = ($json.productosIndex && typeof $json.productosIndex === 'object')\n    ? $json.productosIndex\n    : {};\n\n  const fechaKeys = ['fecha', 'createdAt', 'updatedAt', 'fechaVenta', 'date'];\n  const getFecha = (v) => {\n    for (const k of fechaKeys) {\n      if (v && v[k]) {\n        const d = new Date(v[k]);\n        if (!isNaN(d)) return d;\n      }\n    }\n    return null;\n  };\n\n  const clean = ventas.map(v => {\n    const fecha = getFecha(v);\n    const cantidad = Number(v?.cantidad ?? 1);\n    const pu = Number(v?.precio_unitario ?? v?.precio ?? 0);\n    const total = v?.total != null ? Number(v.total) : cantidad * pu;\n\n    // Resolver nombre: primero v.producto.nombre, luego \u00edndice, luego v.nombre, luego el id\n    const pid = v?.productId ?? v?.id_vaper ?? v?.idproducto ?? v?.producto_id ?? null;\n    const nombreResuelto =\n      (v?.producto?.nombre)\n      || (pid != null ? productosIndex[String(pid)] : null)\n      || v?.nombre\n      || (pid != null ? `Producto ${String(pid)}` : 'Desconocido');\n\n    const cliente = (v?.cliente ?? v?.usuario ?? 'Desconocido') + '';\n\n    return {\n      fecha,\n      cantidad,\n      precio_unitario: pu,\n      total,\n      cliente,\n      producto: String(nombreResuelto),\n      productId: pid\n    };\n  }).filter(x => x.fecha && !isNaN(x.fecha));\n\n  const now = new Date();\n  const dayMs = 24 * 60 * 60 * 1000;\n  const from7  = new Date(now.getTime() - 7  * dayMs);\n  const from30 = new Date(now.getTime() - 30 * dayMs);\n  const from90 = new Date(now.getTime() - 90 * dayMs);\n\n  const agg = (arr) => {\n    let ingresoTotal = 0, numVentas = arr.length, unidades = 0;\n    const cMap = new Map(), pMap = new Map(), byDay = new Map();\n\n    for (const x of arr) {\n      ingresoTotal += x.total; unidades += x.cantidad;\n\n      cMap.set(x.cliente, (cMap.get(x.cliente) || 0) + x.total);\n\n      const p = pMap.get(x.producto) || { unidades: 0, ingresos: 0 };\n      p.unidades += x.cantidad; p.ingresos += x.total;\n      pMap.set(x.producto, p);\n\n      const key = x.fecha.toISOString().slice(0,10);\n      const d = byDay.get(key) || { ingresos: 0, ventas: 0, unidades: 0 };\n      d.ingresos += x.total; d.ventas += 1; d.unidades += x.cantidad;\n      byDay.set(key, d);\n    }\n\n    const ticketMedio = numVentas > 0 ? ingresoTotal / numVentas : 0;\n    const topCliente = [...cMap.entries()].sort((a,b)=> b[1]-a[1])[0] || [null,0];\n    const topProductos = [...pMap.entries()]\n      .map(([producto, s]) => ({ producto, ...s }))\n      .sort((a,b)=> b.ingresos - a.ingresos)\n      .slice(0,5);\n    const serieDiaria = [...byDay.entries()]\n      .sort((a,b)=> (a[0] < b[0] ? -1 : 1))\n      .map(([fecha, val]) => ({ fecha, ...val }));\n\n    return {\n      metrics: { ingresoTotal, numVentas, unidades, ticketMedio },\n      resumen: { topCliente: { nombre: topCliente[0], gasto: topCliente[1] }, topProductos },\n      serieDiaria\n    };\n  };\n\n  const global = agg(clean);\n  const last7  = agg(clean.filter(x => x.fecha >= from7));\n  const last30 = agg(clean.filter(x => x.fecha >= from30));\n  const last90 = agg(clean.filter(x => x.fecha >= from90));\n\n  let byPeriod = null;\n  if (period?.from && period?.to) {\n    const f = new Date(period.from), t = new Date(period.to);\n    byPeriod = agg(clean.filter(x => x.fecha >= f && x.fecha <= t));\n  }\n\n  return [{\n    json: {\n      prompt,\n      period,\n      contextVentas: { global, last7, last30, last90, byPeriod },\n      ventasSample: clean.slice(0, 50)\n    }\n  }];\n} catch (e) {\n  return [{\n    json: {\n      prompt: $json.prompt || '',\n      period: $json.period || null,\n      contextVentas: null,\n      ventasSample: [],\n      _error: String(e)\n    }\n  }];\n}\n"
      },
      "id": "03ef67c4-406b-452c-ac4c-c5618936c589",
      "name": "Contexto ventas (agregados)",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -656,
        224
      ]
    },
    {
      "parameters": {
        "functionCode": "try {\n  // Parse respuesta de API de ventas\n  let raw = $json.data ?? $json.body ?? $json;\n  if (typeof raw === 'string') { try { raw = JSON.parse(raw); } catch { raw = []; } }\n  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n    const arrKey = Object.keys(raw).find(k => Array.isArray(raw[k]));\n    raw = arrKey ? raw[arrKey] : [];\n  }\n  if (!Array.isArray(raw)) raw = [];\n\n  // Recupera SIEMPRE prompt/period del origen\n  const baseNode = $items(\"Normalizar input (chat libre)\", 0, 0)[0];\n  const base = (baseNode && baseNode.json) ? baseNode.json : {};\n\n  // Conserva productId desde varios posibles campos\n  const ventas = raw.map(v => {\n    const productId = v?.productId ?? v?.id_vaper ?? v?.idproducto ?? v?.producto_id ?? null;\n    return { ...v, productId };\n  });\n\n  return [{ json: { ventas, prompt: base.prompt || '', period: base.period || null } }];\n} catch (e) {\n  const baseNode = $items(\"Normalizar input (chat libre)\", 0, 0)[0];\n  const base = (baseNode && baseNode.json) ? baseNode.json : {};\n  return [{ json: { ventas: [], prompt: base.prompt || '', period: base.period || null, _error: String(e) } }];\n}"
      },
      "id": "276536e1-c15d-4720-8dac-a5ef86b22afb",
      "name": "Parsear ventas",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -848,
        224
      ]
    },
    {
      "parameters": {
        "functionCode": "try {\n  const fin = Array.isArray($json.finanzas) ? $json.finanzas : [];\n  const byDay = new Map();\n  let ingresoTotal = 0, gastoTotal = 0, beneficioTotal = 0;\n  const byCategoria = new Map();\n  for (const x of fin) {\n    ingresoTotal += x.ingreso; gastoTotal += x.gasto; beneficioTotal += x.beneficio;\n    const day = x.fecha.toISOString().slice(0,10);\n    const d = byDay.get(day) || { ingreso:0, gasto:0, beneficio:0 };\n    d.ingreso += x.ingreso; d.gasto += x.gasto; d.beneficio += x.beneficio;\n    byDay.set(day, d);\n    if (x.categoria) byCategoria.set(x.categoria, (byCategoria.get(x.categoria)||0) + x.gasto);\n  }\n  const margen = ingresoTotal ? (beneficioTotal / ingresoTotal) : 0;\n  const serieDiaria = [...byDay.entries()].sort((a,b)=> a[0]<b[0]?-1:1).map(([fecha,val])=>({fecha, ...val}));\n  const gastosPorCategoria = [...byCategoria.entries()].sort((a,b)=> b[1]-a[1]).slice(0,8).map(([categoria, gasto])=>({categoria, gasto}));\n  return [{ json: { finanzasContext: {\n    metrics: { ingresoTotal, gastoTotal, beneficioTotal, margen },\n    resumen: { gastosPorCategoria },\n    serieDiaria\n  } } }];\n} catch (e) {\n  return [{ json: { finanzasContext: null, _error: String(e) } }];\n}"
      },
      "id": "1e345007-bfa1-453c-9d5c-939995adb472",
      "name": "Contexto finanzas (agregados)",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -1344,
        1008
      ]
    },
    {
      "parameters": {
        "functionCode": "try {\n  let raw = $json.data ?? $json.body ?? $json;\n  if (typeof raw === 'string') { try { raw = JSON.parse(raw); } catch { raw = []; } }\n  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n    const arrKey = Object.keys(raw).find(k => Array.isArray(raw[k]));\n    raw = arrKey ? raw[arrKey] : [];\n  }\n  if (!Array.isArray(raw)) raw = [];\n  const items = raw.map(r => {\n    const fecha = new Date(r.fecha ?? r.date ?? r.createdAt ?? r.updatedAt ?? Date.now());\n    const ingreso = Number(r.ingreso ?? r.ingresos ?? r.venta ?? r.revenue ?? 0);\n    const gasto = Number(r.gasto ?? r.gastos ?? r.costes ?? r.costo ?? r.expense ?? 0);\n    const beneficio = (r.beneficio ?? r.profit) != null ? Number(r.beneficio ?? r.profit) : (ingreso - gasto);\n    const categoria = r.categoria ?? r.category ?? null;\n    return { fecha, ingreso, gasto, beneficio, categoria };\n  }).filter(x => x.fecha && !isNaN(x.fecha));\n  return [{ json: { finanzas: items } }];\n} catch (e) {\n  return [{ json: { finanzas: [], _error: String(e) } }];\n}"
      },
      "id": "1286f119-7c99-480c-9b05-85cb36fab9a4",
      "name": "Parsear finanzas",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -1568,
        1008
      ]
    },
    {
      "parameters": {
        "url": "https://api-vapers.onrender.com/api/finanzas",
        "responseFormat": "string",
        "options": {
          "fullResponse": true
        }
      },
      "id": "8925ea80-0999-48d3-9248-e0c4d4d3949f",
      "name": "Obtener finanzas",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -1888,
        1008
      ]
    },
    {
      "parameters": {
        "functionCode": "try {\n  const vapers = Array.isArray($json.vapers) ? $json.vapers : [];\n  const totalSkus = vapers.length;\n  const stockTotal = vapers.reduce((a,b)=> a + (b.stock||0), 0);\n  const precioMedio = totalSkus ? vapers.reduce((a,b)=> a + (b.precio||0), 0)/totalSkus : 0;\n  const byMarca = new Map();\n  const bySabor = new Map();\n  for (const v of vapers) {\n    if (v.marca) byMarca.set(v.marca, (byMarca.get(v.marca)||0) + (v.stock||0));\n    if (v.sabor) bySabor.set(v.sabor, (bySabor.get(v.sabor)||0) + (v.stock||0));\n  }\n  const topStock = [...byMarca.entries()].sort((a,b)=> b[1]-a[1]).slice(0,5).map(([marca, stock])=>({marca, stock}));\n  const saboresTop = [...bySabor.entries()].sort((a,b)=> b[1]-a[1]).slice(0,5).map(([sabor, stock])=>({sabor, stock}));\n  return [{ json: { vapersContext: {\n    metrics: { totalSkus, stockTotal, precioMedio },\n    resumen: { topMarcasPorStock: topStock, topSaboresPorStock: saboresTop },\n    muestra: vapers.slice(0,50)\n  } } }];\n} catch (e) {\n  return [{ json: { vapersContext: null, _error: String(e) } }];\n}"
      },
      "id": "04554bf0-a859-4da0-ba8c-84f6dd48de29",
      "name": "Contexto vapers (agregados)",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -1344,
        768
      ]
    },
    {
      "parameters": {
        "functionCode": "try {\n  let raw = $json.data ?? $json.body ?? $json;\n  if (typeof raw === 'string') { try { raw = JSON.parse(raw); } catch { raw = []; } }\n  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n    const arrKey = Object.keys(raw).find(k => Array.isArray(raw[k]));\n    raw = arrKey ? raw[arrKey] : [];\n  }\n  if (!Array.isArray(raw)) raw = [];\n  // Normaliza campos habituales\n  const items = raw.map(v => {\n    const id = v.id_vaper ?? v.id ?? v.producto_id ?? v.idproducto ?? null;\n    const nombre = v.nombre ?? v.name ?? v.title ?? `Vaper ${id ?? ''}`;\n    const marca = v.marca ?? v.brand ?? null;\n    const sabor = v.sabor ?? v.flavor ?? null;\n    const stock = Number(v.stock ?? v.cantidad ?? v.unidades_disponibles ?? 0);\n    const precio = Number(v.precio ?? v.precio_unitario ?? v.pvp ?? 0);\n    const creado = v.createdAt ?? v.fecha_alta ?? v.fecha ?? v.date ?? null;\n    const actualizado = v.updatedAt ?? v.fecha_actualizacion ?? null;\n    const d = actualizado || creado || null;\n    const fecha = d ? new Date(d) : null;\n    return { id, nombre: String(nombre), marca, sabor, stock, precio, fecha };\n  }).filter(x => !x.fecha || !isNaN(x.fecha));\n  return [{ json: { vapers: items } }];\n} catch (e) {\n  return [{ json: { vapers: [], _error: String(e) } }];\n}"
      },
      "id": "3efcbb1a-6ef1-4236-b190-c43565028899",
      "name": "Parsear vapers",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -1568,
        768
      ]
    },
    {
      "parameters": {
        "url": "https://api-vapers.onrender.com/api/vapers",
        "responseFormat": "string",
        "options": {
          "fullResponse": true
        }
      },
      "id": "35307332-4f86-4053-be84-5a9dd278bf6d",
      "name": "Obtener vapers (dataset)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -1888,
        768
      ]
    },
    {
      "parameters": {
        "functionCode": "// \u00cdndice de productos {id -> nombre}\ntry {\n  let raw = $json.data ?? $json.body ?? $json;\n  if (typeof raw === 'string') { try { raw = JSON.parse(raw); } catch { raw = []; } }\n  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n    const k = Object.keys(raw).find(x => Array.isArray(raw[x]));\n    raw = k ? raw[k] : [];\n  }\n  const arr = Array.isArray(raw) ? raw : [];\n  const idx = {};\n  for (const p of arr) {\n    const id = p.id_vaper ?? p.idproducto ?? p.producto_id ?? p.id ?? p.sku ?? p.codigo ?? null;\n    const nombre = p.nombre ?? p.name ?? p.titulo ?? p.title ?? p.descripcion ?? p.descripcion_corta ?? null;\n    if (id != null) idx[String(id)] = (nombre && String(nombre).trim()) || String(id);\n  }\n  return [{ json: { productosIndex: idx } }];\n} catch (e) {\n  return [{ json: { productosIndex: {}, _errorProductos: String(e) } }];\n}"
      },
      "id": "199beb4a-d34a-43c2-929a-e49bd9e3132b",
      "name": "Indexar productos",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -1024,
        800
      ]
    },
    {
      "parameters": {
        "url": "https://api-vapers.onrender.com/api/vapers",
        "responseFormat": "string",
        "options": {
          "fullResponse": true
        }
      },
      "id": "5b9f3210-caff-4087-a252-7fbe2ca8b09c",
      "name": "Obtener productos (\u00edndice nombres)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -1536,
        800
      ]
    },
    {
      "parameters": {
        "mode": "passThrough"
      },
      "id": "3ded6905-19a3-4f96-bdca-8f759eee3d43",
      "name": "Merge Prompt Ventas2",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        -1056,
        320
      ]
    },
    {
      "parameters": {
        "url": "https://api-vapers.onrender.com/api/ventas",
        "responseFormat": "string",
        "options": {
          "fullResponse": true
        }
      },
      "id": "78eef6a1-4512-4350-a9cd-273f04e25223",
      "name": "Obtener ventas #2 (retry)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -1248,
        320
      ]
    },
    {
      "parameters": {},
      "id": "1b7ceb51-8669-403e-88e1-d9fa1b8085f6",
      "name": "Wait 5s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        -1472,
        320
      ]
    },
    {
      "parameters": {
        "conditions": {
          "number": [
            {
              "value1": "={{ $json.statusCode || $json.status || ($json.body?.statusCode ?? 0) }}",
              "operation": "equal",
              "value2": 200
            }
          ]
        }
      },
      "id": "9acd12dd-7503-4255-8c57-6628c3fc04f2",
      "name": "\u00bf200 OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        -1472,
        128
      ]
    },
    {
      "parameters": {
        "mode": "passThrough"
      },
      "id": "665eafce-133e-40b8-91f4-5c86678b5335",
      "name": "Merge Prompt Ventas1",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        -1664,
        128
      ]
    },
    {
      "parameters": {
        "url": "https://api-vapers.onrender.com/api/ventas",
        "responseFormat": "string",
        "options": {
          "fullResponse": true
        }
      },
      "id": "70aa2437-86ec-4578-808d-48e6d12411d7",
      "name": "Obtener ventas #1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        -1872,
        128
      ]
    },
    {
      "parameters": {
        "functionCode": "const { prompt } = $json;\nconst t = (prompt || '').toLowerCase();\n// Palabras clave para intenci\u00f3n\nconst isVentas = /(venta|ingreso|factur|ticket|cliente|unidades|top|tendenc|compar|hoy|ayer|semana|mes|a\u00f1o|periodo)/.test(t);\nconst isFinanzas = /(finanza|gasto|beneficio|margen|cash ?flow|tesor|balance|caja|coste|costos|costo)/.test(t);\nconst isVapers = /(stock|inventario|vaper|producto|sabor|marca|disponible|agotado|sku|precio)/.test(t);\nlet intent = 'ventas';\nif (isFinanzas && !isVentas && !isVapers) intent = 'finanzas';\nif (isVapers && !isFinanzas && !isVentas) intent = 'vapers';\n// Si menciona expl\u00edcitamente vapers/finanzas, prima sobre ventas por defecto\nif (/(^|\\b)(vapers?|productos?)\\b/.test(t)) intent = 'vapers';\nif (/(^|\\b)(finanzas?|gastos?|beneficio)\\b/.test(t)) intent = 'finanzas';\nreturn [{ json: { ...$json, intent } }];"
      },
      "id": "f13b5bec-ea69-4263-9fea-faf4dc7ca786",
      "name": "Detectar intenci\u00f3n",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -1872,
        368
      ]
    },
    {
      "parameters": {
        "functionCode": "const b = $json.body || $json || {};\n// Acepta prompt o message\nconst prompt = ((b.prompt ?? b.message ?? '') + '').trim();\nif (!prompt) {\n  return [{ json: { ok: false, error: 'Falta prompt/message' } }];\n}\n\n// Detecci\u00f3n opcional de periodo desde texto (no obligatorio)\nfunction iso(d){ return new Date(d).toISOString(); }\nconst now = new Date();\nconst today0 = new Date(now.getFullYear(), now.getMonth(), now.getDate());\nconst yday0 = new Date(today0.getTime() - 24*60*60*1000);\nlet period = null;\nconst txt = prompt.toLowerCase();\n\n// Rangos expl\u00edcitos: \"desde X hasta Y\"\nconst reRango = /(?:desde|de)\\s+(\\d{4}-\\d{2}-\\d{2}|\\d{1,2}[\\/.-]\\d{1,2}[\\/.-]\\d{2,4})\\s+(?:hasta|a)\\s+(\\d{4}-\\d{2}-\\d{2}|\\d{1,2}[\\/.-]\\d{1,2}[\\/.-]\\d{2,4})/;\nconst mR = txt.match(reRango);\nif (mR) {\n  const f = new Date(mR[1]);\n  const t = new Date(mR[2]);\n  if (!isNaN(f) && !isNaN(t)) period = { from: iso(f), to: iso(t) };\n}\n\nif (!period) {\n  if (/(hoy|today)/.test(txt)) period = { from: iso(today0), to: iso(now) };\n  else if (/(ayer|yesterday)/.test(txt)) period = { from: iso(yday0), to: iso(today0) };\n  else if (/(esta semana|\u00faltima semana|last week)/.test(txt)) { const f = new Date(now.getTime() - 7*24*60*60*1000); period = { from: iso(f), to: iso(now) }; }\n  else if (/(este mes|\u00faltimo mes|this month|last month)/.test(txt)) { const first = new Date(now.getFullYear(), now.getMonth(), 1); period = { from: iso(first), to: iso(now) }; }\n  else if (/(este trimestre|q\\d|quarter)/.test(txt)) { const q = Math.floor(now.getMonth()/3); const first = new Date(now.getFullYear(), q*3, 1); period = { from: iso(first), to: iso(now) }; }\n  else if (/(este a\u00f1o|this year|ytd)/.test(txt)) { const first = new Date(now.getFullYear(), 0, 1); period = { from: iso(first), to: iso(now) }; }\n}\n\nreturn [{ json: { prompt, period } }];"
      },
      "id": "95e3506d-a017-4a8e-b0ce-7d2fd90cb5dd",
      "name": "Normalizar input (chat libre)",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        -2064,
        224
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ventas-chat",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "a65b6751-27e3-46d3-9754-a25b4e3104a9",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [
        -2272,
        224
      ]
    }
  ],
  "connections": {
    "Construir salida": {
      "main": [
        [
          {
            "node": "Responder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extraer respuesta LLM": {
      "main": [
        [
          {
            "node": "Construir salida",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Chat": {
      "main": [
        [
          {
            "node": "Extraer respuesta LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construir mensajes Chat": {
      "main": [
        [
          {
            "node": "Groq Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enrutar contexto por intenci\u00f3n": {
      "main": [
        [
          {
            "node": "Construir mensajes Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge (Todo + Finanzas)": {
      "main": [
        [
          {
            "node": "Enrutar contexto por intenci\u00f3n",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge (Ventas/Vapers)": {
      "main": [
        [
          {
            "node": "Merge (Todo + Finanzas)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Ventas + Productos": {
      "main": [
        [
          {
            "node": "Contexto ventas (agregados)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Contexto ventas (agregados)": {
      "main": [
        [
          {
            "node": "Merge (Ventas/Vapers)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parsear ventas": {
      "main": [
        [
          {
            "node": "Merge Ventas + Productos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Contexto finanzas (agregados)": {
      "main": [
        [
          {
            "node": "Merge (Todo + Finanzas)",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Parsear finanzas": {
      "main": [
        [
          {
            "node": "Contexto finanzas (agregados)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Obtener finanzas": {
      "main": [
        [
          {
            "node": "Parsear finanzas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Contexto vapers (agregados)": {
      "main": [
        [
          {
            "node": "Merge (Ventas/Vapers)",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Parsear vapers": {
      "main": [
        [
          {
            "node": "Contexto vapers (agregados)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Obtener vapers (dataset)": {
      "main": [
        [
          {
            "node": "Parsear vapers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Indexar productos": {
      "main": [
        [
          {
            "node": "Merge Ventas + Productos",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Obtener productos (\u00edndice nombres)": {
      "main": [
        [
          {
            "node": "Indexar productos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Prompt Ventas2": {
      "main": [
        [
          {
            "node": "Parsear ventas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Obtener ventas #2 (retry)": {
      "main": [
        [
          {
            "node": "Merge Prompt Ventas2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 5s": {
      "main": [
        [
          {
            "node": "Obtener ventas #2 (retry)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u00bf200 OK?": {
      "main": [
        [
          {
            "node": "Parsear ventas",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 5s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Prompt Ventas1": {
      "main": [
        [
          {
            "node": "\u00bf200 OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Obtener ventas #1": {
      "main": [
        [
          {
            "node": "Merge Prompt Ventas1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalizar input (chat libre)": {
      "main": [
        [
          {
            "node": "Obtener finanzas",
            "type": "main",
            "index": 0
          },
          {
            "node": "Obtener vapers (dataset)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Obtener productos (\u00edndice nombres)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Prompt Ventas1",
            "type": "main",
            "index": 1
          },
          {
            "node": "Obtener ventas #1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Detectar intenci\u00f3n",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Normalizar input (chat libre)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Chat Ia. Uses httpRequest. Webhook trigger; 28 nodes.

Source: https://github.com/Rruubeenn23/Vapers/blob/53d90f273b2d0172d72c0351e38e6199d0a65d27/n8n/chat_ia.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di

n8n, Execute Workflow Trigger, HTTP Request +1
Web Scraping

This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .

HTTP Request, Ssh
Web Scraping

This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia

HTTP Request
Web Scraping

This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c

HTTP Request
Web Scraping

Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.

Execute Command, HTTP Request, Read Write File +1