{
  "name": "PYROX — Agent B2B RAG (TelForceOne) · self-hosted",
  "meta": {
    "description": "Dwa przepływy w jednym pliku. A) INDEKSOWANIE: feed katalogu XML/CSV -> podział na fragmenty -> embeddingi (Ollama, lokalnie) -> Qdrant. B) OBSŁUGA: pytanie dystrybutora -> RAG na katalogu -> odpowiedź z cytatem ze źródła -> bramka pewności -> auto-odpowiedź albo eskalacja do człowieka. Zero zewnętrznych API — model i baza wektorowa stoją na Waszym serwerze."
  },
  "nodes": [
    {
      "parameters": {},
      "id": "ing-trigger-0001",
      "name": "A1 · Harmonogram: co noc",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [-620, -160]
    },
    {
      "parameters": {
        "url": "https://PODMIEN.example/feed/katalog.xml",
        "options": {}
      },
      "id": "ing-fetch-0002",
      "name": "A2 · Feed katalogu (macie XML/CSV)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-400, -160]
    },
    {
      "parameters": {
        "jsCode": "// Feed -> jeden rekord na SKU. Do wektorów idzie tekst, w metadanych zostaje to,\n// po czym potem filtrujemy (marka, rynek, indeks) i co pokazujemy jako źródło.\nconst produkty = $input.all().flatMap(i => i.json.produkty ?? [i.json]);\nreturn produkty.map(p => ({\n  json: {\n    tekst: [p.nazwa, p.opis, `Indeks: ${p.sku}`, `Marka: ${p.marka}`,\n            `Gwarancja: ${p.gwarancja}`, `Stan: ${p.stan}`, `Cena B2B: ${p.cena}`]\n           .filter(Boolean).join('\\n'),\n    sku: p.sku, marka: p.marka, rynek: p.rynek ?? 'PL'\n  }\n}));"
      },
      "id": "ing-map-0003",
      "name": "A3 · Mapowanie SKU + metadane",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-180, -160]
    },
    {
      "parameters": {
        "mode": "insert",
        "qdrantCollection": { "__rl": true, "value": "tfo_katalog", "mode": "list" },
        "options": {}
      },
      "id": "ing-qdrant-0004",
      "name": "A4 · Qdrant — indeks katalogu",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant",
      "typeVersion": 1,
      "position": [80, -160]
    },
    {
      "parameters": { "options": {} },
      "id": "ing-loader-0005",
      "name": "Loader dokumentów",
      "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
      "typeVersion": 1,
      "position": [140, 20]
    },
    {
      "parameters": { "chunkSize": 800, "chunkOverlap": 120, "options": {} },
      "id": "ing-split-0006",
      "name": "Podział na fragmenty",
      "type": "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
      "typeVersion": 1,
      "position": [140, 190]
    },
    {
      "parameters": { "model": "nomic-embed-text:latest" },
      "id": "emb-0007",
      "name": "Embeddingi (Ollama, lokalnie)",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOllama",
      "typeVersion": 1,
      "position": [-80, 20]
    },

    {
      "parameters": {
        "httpMethod": "POST",
        "path": "b2b-support",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "sup-hook-0010",
      "name": "B1 · Pytanie dystrybutora (webhook/mail/chat)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-620, 420]
    },
    {
      "parameters": {
        "jsCode": "// Jeden format dla wszystkich kanałów: mail, czat na stronie, formularz B2B, WhatsApp.\nconst b = $json.body ?? $json;\nreturn [{ json: {\n  pytanie: b.pytanie ?? b.message ?? b.text ?? '',\n  kanal:   b.kanal ?? 'web',\n  nadawca: b.email ?? b.from ?? 'nieznany',\n  jezyk:   b.jezyk ?? 'pl'\n}}];"
      },
      "id": "sup-norm-0011",
      "name": "B2 · Normalizacja kanału",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-400, 420]
    },
    {
      "parameters": {
        "query": "={{ $json.pytanie }}",
        "options": {
          "systemPromptTemplate": "Jesteś asystentem działu obsługi B2B TelForceOne. Odpowiadasz dystrybutorom wyłącznie na podstawie podanego kontekstu z katalogu.\nZASADY:\n1. Jeśli w kontekście nie ma odpowiedzi — napisz wprost, że nie masz tej informacji. Nigdy nie zgaduj indeksu, ceny, stanu magazynu ani warunków gwarancji.\n2. Zawsze podaj indeks (SKU) produktu, o którym mówisz.\n3. Odpowiadaj w języku pytania, krótko i konkretnie.\nKONTEKST:\n{context}\n\nPYTANIE: {question}"
        }
      },
      "id": "sup-rag-0012",
      "name": "B3 · RAG na katalogu (odpowiedź ze źródłem)",
      "type": "@n8n/n8n-nodes-langchain.chainRetrievalQa",
      "typeVersion": 1.7,
      "position": [-180, 420]
    },
    {
      "parameters": { "options": {} },
      "id": "sup-retr-0013",
      "name": "Retriever (top 4 fragmenty)",
      "type": "@n8n/n8n-nodes-langchain.retrieverVectorStore",
      "typeVersion": 1,
      "position": [-120, 620]
    },
    {
      "parameters": {
        "mode": "retrieve",
        "qdrantCollection": { "__rl": true, "value": "tfo_katalog", "mode": "list" },
        "options": {}
      },
      "id": "sup-qdrant-0014",
      "name": "Qdrant — katalog (odczyt)",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant",
      "typeVersion": 1,
      "position": [-120, 800]
    },
    {
      "parameters": { "model": "qwen3:235b", "options": { "temperature": 0.2 } },
      "id": "sup-llm-0015",
      "name": "LLM self-hosted (qwen @ Wasz serwer)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOllama",
      "typeVersion": 1,
      "position": [-360, 620]
    },
    {
      "parameters": {
        "jsCode": "// Bramka pewności. Model, który nie ma danych, ma to POWIEDZIEĆ — a nie zmyślić.\n// Dlatego liczymy sygnały ostrożności zamiast ufać samej długości odpowiedzi.\nconst o = $json.response ?? $json.text ?? '';\nconst brakDanych = /nie mam (tej )?informacj|brak danych|nie znalaz|nie jestem pewien/i.test(o);\nconst maSku      = /\\b[A-Z0-9]{4,}\\b/.test(o);\nconst zaKrotka   = o.trim().length < 40;\nconst pewnosc = brakDanych || zaKrotka ? 0.3 : (maSku ? 0.9 : 0.65);\nreturn [{ json: { ...$('B2 · Normalizacja kanału').first().json, odpowiedz: o, pewnosc,\n                  eskalacja: pewnosc < 0.7 } }];"
      },
      "id": "sup-conf-0016",
      "name": "B4 · Ocena pewności",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [80, 420]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 },
          "conditions": [
            {
              "id": "gate-1",
              "leftValue": "={{ $json.eskalacja }}",
              "rightValue": "true",
              "operator": { "type": "boolean", "operation": "false", "singleValue": true }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "sup-if-0017",
      "name": "B5 · Pewna odpowiedź?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [300, 420]
    },
    {
      "parameters": { "options": {} },
      "id": "sup-resp-0018",
      "name": "B6a · Odpowiedź automatyczna",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [540, 320]
    },
    {
      "parameters": {
        "jsCode": "// Eskalacja: człowiek dostaje pytanie RAZEM z draftem i źródłami — nie zaczyna od zera.\nreturn [{ json: {\n  do_zespolu: 'obsluga.b2b@telforceone.pl',\n  temat: `[B2B] ${$json.pytanie.slice(0,60)}`,\n  tresc: `Kanał: ${$json.kanal}\\nNadawca: ${$json.nadawca}\\nPewność AI: ${$json.pewnosc}\\n\\nSzkic odpowiedzi (do sprawdzenia):\\n${$json.odpowiedz}`,\n  ...$json\n}}];"
      },
      "id": "sup-esc-0019",
      "name": "B6b · Eskalacja do człowieka (z draftem)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [540, 520]
    },
    {
      "parameters": {
        "jsCode": "// Log: co pytają, ile obsłużone bez człowieka, gdzie brakuje danych w katalogu.\n// Ta tabela sama pokazuje, które opisy produktów trzeba uzupełnić.\nreturn [{ json: { data: $now.toISO(), kanal: $json.kanal, pytanie: $json.pytanie,\n                  pewnosc: $json.pewnosc, eskalacja: $json.eskalacja } }];"
      },
      "id": "sup-log-0020",
      "name": "B7 · Log do raportu",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [780, 420]
    }
  ],
  "connections": {
    "A1 · Harmonogram: co noc": { "main": [[{ "node": "A2 · Feed katalogu (macie XML/CSV)", "type": "main", "index": 0 }]] },
    "A2 · Feed katalogu (macie XML/CSV)": { "main": [[{ "node": "A3 · Mapowanie SKU + metadane", "type": "main", "index": 0 }]] },
    "A3 · Mapowanie SKU + metadane": { "main": [[{ "node": "A4 · Qdrant — indeks katalogu", "type": "main", "index": 0 }]] },
    "Loader dokumentów": { "ai_document": [[{ "node": "A4 · Qdrant — indeks katalogu", "type": "ai_document", "index": 0 }]] },
    "Podział na fragmenty": { "ai_textSplitter": [[{ "node": "Loader dokumentów", "type": "ai_textSplitter", "index": 0 }]] },
    "Embeddingi (Ollama, lokalnie)": { "ai_embedding": [[{ "node": "A4 · Qdrant — indeks katalogu", "type": "ai_embedding", "index": 0 }, { "node": "Qdrant — katalog (odczyt)", "type": "ai_embedding", "index": 0 }]] },

    "B1 · Pytanie dystrybutora (webhook/mail/chat)": { "main": [[{ "node": "B2 · Normalizacja kanału", "type": "main", "index": 0 }]] },
    "B2 · Normalizacja kanału": { "main": [[{ "node": "B3 · RAG na katalogu (odpowiedź ze źródłem)", "type": "main", "index": 0 }]] },
    "B3 · RAG na katalogu (odpowiedź ze źródłem)": { "main": [[{ "node": "B4 · Ocena pewności", "type": "main", "index": 0 }]] },
    "LLM self-hosted (qwen @ Wasz serwer)": { "ai_languageModel": [[{ "node": "B3 · RAG na katalogu (odpowiedź ze źródłem)", "type": "ai_languageModel", "index": 0 }]] },
    "Retriever (top 4 fragmenty)": { "ai_retriever": [[{ "node": "B3 · RAG na katalogu (odpowiedź ze źródłem)", "type": "ai_retriever", "index": 0 }]] },
    "Qdrant — katalog (odczyt)": { "ai_vectorStore": [[{ "node": "Retriever (top 4 fragmenty)", "type": "ai_vectorStore", "index": 0 }]] },
    "B4 · Ocena pewności": { "main": [[{ "node": "B5 · Pewna odpowiedź?", "type": "main", "index": 0 }]] },
    "B5 · Pewna odpowiedź?": { "main": [
      [{ "node": "B6a · Odpowiedź automatyczna", "type": "main", "index": 0 }],
      [{ "node": "B6b · Eskalacja do człowieka (z draftem)", "type": "main", "index": 0 }]
    ]},
    "B6a · Odpowiedź automatyczna": { "main": [[{ "node": "B7 · Log do raportu", "type": "main", "index": 0 }]] },
    "B6b · Eskalacja do człowieka (z draftem)": { "main": [[{ "node": "B7 · Log do raportu", "type": "main", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" },
  "pinData": {}
}
