{
  "id": "hiXXL4p3oGe1vagC",
  "name": "RAG Chatbot with Small Local LLMs (Ollama) \u2014 No Tool Calling",
  "tags": [],
  "nodes": [
    {
      "id": "81fd20e5-7618-49c7-9e4e-d48209237e1b",
      "name": "Sticky Note \u2014 Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2528,
        560
      ],
      "parameters": {
        "width": 700,
        "height": 852,
        "content": "## RAG Chatbot with Small Local LLMs (Ollama)\n\nA full RAG chatbot that runs entirely on local hardware using Ollama \u2014 no cloud APIs, no tool calling required. A lightweight 7B model classifies intent and plans retrieval; a 14B model synthesizes sourced answers. This **Workflow RAG** approach moves all retrieval logic into deterministic n8n nodes, making it reliable and debuggable even on small models.\n\n## How it works\n\n1. **Webhook** receives a POST request with `chatInput` and `session_id`\n2. **Classify & Decompose** (Qwen2.5:7b) decides if the input is a question or small talk, and generates 1\u20135 focused sub-queries as structured JSON\n3. **Router** sends small talk to a conversational agent; questions enter the RAG pipeline\n4. **Retrieval Loop** runs each sub-query against pgvector using BGE-M3 embeddings, scores chunks, and discards anything below 0.4 relevance\n5. **Answer Generator** (Qwen3:14b) synthesizes a concise sourced answer with file citations and offers to elaborate\n6. **Think Tag Stripper** removes `<think>` reasoning blocks that Qwen3 outputs before the response is returned\n\n## Set up steps\n\n- [ ] Install Ollama and pull the required models: `ollama pull qwen2.5:7b`, `ollama pull qwen3:14b`, `ollama pull bge-m3:latest`\n- [ ] Set up PostgreSQL with the pgvector extension enabled\n- [ ] Create the `chat_histories` table (created automatically by n8n on first run)\n- [ ] Populate the `vector_store` table with your document embeddings using a separate ingestion workflow (chunks must include `title` and `file_path` in metadata)\n- [ ] Add your **Ollama** credentials to all Ollama nodes (host URL, e.g. `http://localhost:11434`)\n- [ ] Add your **Postgres** credentials to the PGVector and Chat Memory nodes\n- [ ] Activate the workflow and POST to the webhook: `{\"chatInput\": \"your question\", \"session_id\": \"unique-session-id\"}`"
      },
      "typeVersion": 1
    },
    {
      "id": "1a2a3291-5eb5-454c-8219-7894cf791015",
      "name": "Sticky Note \u2014 Classification",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1776,
        688
      ],
      "parameters": {
        "color": 6,
        "width": 1020,
        "height": 520,
        "content": "### 1. Classification & Routing\nA lightweight 7B model classifies the input as a question or small talk and decomposes questions into focused retrieval sub-queries."
      },
      "typeVersion": 1
    },
    {
      "id": "6b94bfb2-1d74-46b0-aa72-9827a29f223a",
      "name": "Sticky Note \u2014 Small Talk",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -16
      ],
      "parameters": {
        "color": 6,
        "width": 812,
        "height": 540,
        "content": "### 2a. Small Talk Path\nHandles greetings and casual conversation with a 14B conversational agent backed by persistent session memory."
      },
      "typeVersion": 1
    },
    {
      "id": "432d4802-ebbb-4555-b188-72befc0ced83",
      "name": "Sticky Note \u2014 Answer Generation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -240,
        576
      ],
      "parameters": {
        "color": 6,
        "width": 1000,
        "height": 424,
        "content": "### 2b. Answer Generation\nAggregated retrieval results are passed to a 14B model that writes a concise sourced answer (1\u20133 sentences + source list) and offers to elaborate."
      },
      "typeVersion": 1
    },
    {
      "id": "11e0d33c-93f4-4b14-af30-b7d96efbc561",
      "name": "Sticky Note \u2014 RAG Retrieval",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -736,
        1008
      ],
      "parameters": {
        "color": 6,
        "width": 2240,
        "height": 600,
        "content": "### 3. RAG Retrieval Pipeline\nLoops over each sub-query \u2192 embeds with BGE-M3 \u2192 retrieves from pgvector \u2192 filters chunks below 0.4 relevance score \u2192 aggregates per-query results."
      },
      "typeVersion": 1
    },
    {
      "id": "518a6ba7-40c4-43db-8e6b-2f850415ae94",
      "name": "Sticky Note \u2014 Think Tags Warning",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        416,
        48
      ],
      "parameters": {
        "color": 4,
        "width": 320,
        "height": 344,
        "content": "\u26a0\ufe0f **Qwen3 Think Tags**\nQwen3 outputs `<think>\u2026</think>` reasoning blocks before its answer. These Code nodes strip them. If you switch to a non-reasoning model (e.g. Qwen2.5, Llama 3), you can delete these two nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "bc890da2-28e9-452d-a5e5-77ca62cec7d9",
      "name": "Split Sub-Queries",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -672,
        1184
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "=queries"
      },
      "typeVersion": 1
    },
    {
      "id": "3aa14308-a12d-4d64-bac3-c2ff86a8d5e1",
      "name": "Aggregate Matching Chunks",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        928,
        1088
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "All chunks for this question"
      },
      "typeVersion": 1
    },
    {
      "id": "280b31a0-a342-44e1-a981-aa8addb7b1aa",
      "name": "Aggregate All Retrieval Results",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -160,
        752
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "Knowledge base retrieval"
      },
      "typeVersion": 1
    },
    {
      "id": "06899328-33b2-4d12-aa02-13a6115080bf",
      "name": "Any chunk?",
      "type": "n8n-nodes-base.if",
      "position": [
        704,
        1184
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "66402fe0-918e-4268-8928-f4e83cbb3c4f",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json['Chunk content'] }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f1770405-ca5d-4b2d-b940-1b98dc7b94c6",
      "name": "Clean RAG output",
      "type": "n8n-nodes-base.set",
      "position": [
        192,
        1184
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1eddb72f-9c99-465b-8f94-0ff0f686b542",
              "name": "Chunk content",
              "type": "string",
              "value": "={{ $json.document.pageContent }}"
            },
            {
              "id": "09fe6c91-2cce-40ff-9f8c-86a6857f0772",
              "name": "=Chunk metadata",
              "type": "object",
              "value": "={\n  \"Resource File name\": \"{{ $json.document.metadata.title }}\",\n  \"Reference File Path\": \"{{ $json.document.metadata.file_path }}\",\n  \"Retrieval relevance score\": {{ $json.score.round(2) }}\n}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "832d0a58-abc2-4df7-ae52-dba54baa8ed2",
      "name": "Keep score over 0.4",
      "type": "n8n-nodes-base.filter",
      "position": [
        480,
        1184
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "9a3f844e-7d19-4631-9876-140118e61b6b",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json['Chunk metadata']['Retrieval relevance score'] }}",
              "rightValue": 0.4
            }
          ]
        }
      },
      "typeVersion": 2.2,
      "alwaysOutputData": true
    },
    {
      "id": "52ca59d4-6911-486f-ae3e-0263c00eb5a9",
      "name": "Say no chunk match",
      "type": "n8n-nodes-base.set",
      "position": [
        928,
        1280
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "245fe8f8-b217-4626-bc4d-84f53e47fbbf",
              "name": "Retrieval output",
              "type": "string",
              "value": "=No chunks reached the relevance threshold, the knowledge base was unable to provide information that would be helpful to answer this question."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "9b419289-7657-406f-b8ff-512d14d0e205",
      "name": "Prepare loop output",
      "type": "n8n-nodes-base.set",
      "position": [
        1152,
        1280
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "838f21a4-f7bc-414e-83da-99fbaca4fcca",
              "name": "Query to the knowledge base",
              "type": "string",
              "value": "={{ $('Loop Over Sub-Queries').first().json.query || $('Loop Over Sub-Queries').item.json.queries }}"
            },
            {
              "id": "10a89085-1937-459f-9721-8715cd51ad39",
              "name": "Chunks returned",
              "type": "string",
              "value": "={{ JSON.stringify($json, null, 2) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e29bc19a-329e-404e-a49a-2a878cfcaa62",
      "name": "Postgres Chat Memory (Small Talk)",
      "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
      "position": [
        272,
        352
      ],
      "parameters": {
        "tableName": "chat_histories",
        "sessionKey": "={{ $('Webhook').item.json.body.session_id }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 10
      },
      "typeVersion": 1.3
    },
    {
      "id": "7b97d99a-7aaa-4188-927f-dfbdcc5322ea",
      "name": "Remove Think Tags (RAG Path)",
      "type": "n8n-nodes-base.code",
      "position": [
        528,
        640
      ],
      "parameters": {
        "jsCode": "// Remove <think> tags and their content\nconst response = $json.output;\nconst cleaned = response.replace(/[\\s\\S]*?<\\/think>/gi, '').trim();\n\nreturn {\n  json: {\n    output: cleaned\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c7e303f8-280d-4459-9d60-eded7bbd1c26",
      "name": "Switch",
      "type": "n8n-nodes-base.switch",
      "position": [
        -928,
        848
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Discussion",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "9af19156-d14e-4429-9a53-89efb0ba336f",
                    "operator": {
                      "type": "boolean",
                      "operation": "false",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.is_question }}",
                    "rightValue": "false"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Question",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "6fc6a4e5-48e0-459a-abe3-69e5750ba2e9",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.is_question }}",
                    "rightValue": "true"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "20b2828a-f0aa-4266-9897-6089639656e9",
      "name": "JSON Formatter",
      "type": "n8n-nodes-base.code",
      "position": [
        -1136,
        848
      ],
      "parameters": {
        "jsCode": "// Parse the escaped JSON string from LLM output\nconst rawOutput = $json.output || $json.text || $json.response || \"\";\n\ntry {\n  // Remove escaped characters and parse\n  const cleaned = rawOutput\n    .replace(/\\\\n/g, '')\n    .replace(/\\\\/g, '')\n    .trim();\n  \n  const parsed = JSON.parse(cleaned);\n  \n  return {\n    json: {\n      is_question: parsed.is_question,\n      question_type: parsed.question_type,\n      queries: parsed.queries || [],\n      notes: parsed.notes || \"\"\n    }\n  };\n} catch (error) {\n  // Fallback if parsing fails\n  return {\n    json: {\n      is_question: false,\n      question_type: 0,\n      queries: [],\n      notes: \"Failed to parse LLM output\",\n      raw_output: rawOutput,\n      error: error.message\n    }\n  };\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "91311fcc-ceb9-48a7-b20f-0bd91969a7db",
      "name": "Small Talk AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        128,
        128
      ],
      "parameters": {
        "text": "={{ $('Webhook').item.json.body.chatInput }}",
        "options": {
          "systemMessage": "=ROLE\nYou are a friendly assistant for internal company information. You engage in natural conversations with users.\n\nBEHAVIOR\n- Greet users politely and professionally\n- Respond to small talk briefly and in a friendly manner\n- When needed, explain your capabilities: \"I can help you with questions about our internal documents, products, processes, and policies.\"\n- Encourage users to ask specific questions\n- Match the user's language\n- Keep responses short (2\u20134 sentences)\n\nBOUNDARIES\n- No speculation or fabrication\n- For factual questions: \"I'd be happy to look that up in the knowledge base. Please ask me a specific question.\"\n- No personal opinions or advice outside the company context\n\nEXAMPLES\nUser: \"Hello\"\nAssistant: \"Hello! I'm your assistant for internal company information. How can I help you today?\"\n\nUser: \"How are you?\"\nAssistant: \"Thanks for asking! I'm ready to help you with questions about our documents and processes. What would you like to know?\"\n\nUser: \"Thanks\"\nAssistant: \"You're welcome! If you have any more questions, I'm always here for you.\""
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "0177c211-483d-469e-9373-c205f275b19f",
      "name": "Ollama Chat Model (Small Talk \u2014 Qwen3:14b)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOllama",
      "position": [
        144,
        352
      ],
      "parameters": {
        "model": "qwen3:14b",
        "options": {
          "numPredict": 4096,
          "temperature": 0.7
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a04efb6a-0365-4d88-8bf8-7cf663d53103",
      "name": "Ollama Chat Model (Classifier \u2014 Qwen2.5:7b)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOllama",
      "position": [
        -1408,
        1072
      ],
      "parameters": {
        "model": "qwen2.5:7b",
        "options": {
          "numPredict": 300,
          "temperature": 0.7
        }
      },
      "typeVersion": 1
    },
    {
      "id": "583b4379-e1e3-4821-8611-ae5b104a9572",
      "name": "Postgres Chat Memory (RAG Answer)",
      "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
      "position": [
        272,
        864
      ],
      "parameters": {
        "tableName": "chat_histories",
        "sessionKey": "={{ $('Webhook').item.json.body.session_id }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 10
      },
      "typeVersion": 1.3
    },
    {
      "id": "2e041581-2d5d-4144-9c39-340d37f1d47b",
      "name": "Answer Generator AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        128,
        640
      ],
      "parameters": {
        "text": "=## User Question: \n{{$('Webhook').item.json.body.chatInput}}\n## Retrieved Knowledge Base Data \n{{ JSON.stringify($json['Knowledge base retrieval']) }}",
        "options": {
          "systemMessage": "=# ROLE\nYou are a RAG answer generator. You MUST ALWAYS follow the exact 3-step format below.\n\n# STRICTLY FORBIDDEN\n- Long answers on the first response\n- Answers without source citations\n- Additional explanations before the follow-up question\n- Multiple paragraphs in the short answer\n- Answers without the follow-up question at the end\n\n# REQUIRED FORMAT (WITH EVIDENCE)\n\nStep 1: SHORT ANSWER (maximum 1\u20133 sentences)\n<A precise, direct answer to the question>\n\nStep 2: SOURCES (blank line before)\nSources:\n- <Filename> \u2014 <File path>\n- <Filename> \u2014 <File path>\n\nStep 3: FOLLOW-UP (blank line before)\nWould you like a more detailed explanation?\n\n# REQUIRED FORMAT (WITHOUT EVIDENCE)\n\nNo relevant information found in the database.\n\nMay I use general model knowledge to attempt an answer?\n\n# CRITICAL RULES\n1. The short answer must NEVER be longer than 3 sentences\n2. ALWAYS insert a blank line before \"Sources:\"\n3. ALWAYS insert a blank line before the follow-up question\n4. NEVER add additional information after the follow-up question\n5. ALWAYS list all sources used (Filename \u2014 Path)\n6. If multiple chunks reference the same file, list it only once\n\n# LANGUAGE\nMatch the user's language. Default to English if unclear."
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "e8cfa385-6f6c-4d67-9dcc-664777e246f8",
      "name": "Ollama Chat Model (Answer Generator \u2014 Qwen3:14b)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOllama",
      "position": [
        144,
        864
      ],
      "parameters": {
        "model": "qwen3:14b",
        "options": {
          "topP": 0.9,
          "numPredict": 4096,
          "temperature": 0.3,
          "repeatPenalty": 1.05
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d84e1c8d-a94d-4b67-bee1-413d707e81eb",
      "name": "Loop Over Sub-Queries",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -448,
        1184
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "71056de5-2e7e-4bcc-842a-29edd7937098",
      "name": "Ollama Embeddings (BGE-M3)",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOllama",
      "position": [
        -144,
        1408
      ],
      "parameters": {
        "model": "bge-m3:latest"
      },
      "typeVersion": 1
    },
    {
      "id": "3c5c0ef3-9f38-4f79-926a-68da7bafa27b",
      "name": "PGVector Store \u2014 Retrieve Chunks",
      "type": "@n8n/n8n-nodes-langchain.vectorStorePGVector",
      "position": [
        -224,
        1184
      ],
      "parameters": {
        "mode": "load",
        "prompt": "={{ $json.query || $json.queries }}",
        "options": {
          "metadata": {
            "metadataValues": [
              {
                "name": "user_id",
                "value": "={{$json.user_id || \"YOUR_USER_ID\"}}"
              }
            ]
          },
          "columnNames": {
            "values": {
              "contentColumnName": "content"
            }
          }
        },
        "tableName": "vector_store"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "b56a9ace-af25-4ed0-8d85-23fd4ebc5ffd",
      "name": "Remove Think Tags (Small Talk Path)",
      "type": "n8n-nodes-base.code",
      "position": [
        528,
        224
      ],
      "parameters": {
        "jsCode": "// Remove <think> tags and their content\nconst response = $json.output;\nconst cleaned = response.replace(/[\\s\\S]*?<\\/think>/gi, '').trim();\n\nreturn {\n  json: {\n    output: cleaned\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "67f63fd9-b99b-45a0-add5-379d20eb1ad6",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1696,
        848
      ],
      "parameters": {
        "path": "rag-chatbot",
        "options": {
          "binaryPropertyName": "data"
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "43fdf886-a2f1-4871-98c8-e1ff8d20ea0d",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        976,
        448
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.4
    },
    {
      "id": "b49a2fb5-6c95-459c-aaab-01b639dd3079",
      "name": "Understand Request",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -1472,
        848
      ],
      "parameters": {
        "text": "={{ $json.body.chatInput }}",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "=You are a classifier and query planner for a RAG chatbot. Your job:\n1) Decide if the user input is a genuine question.\n2) If it is a question, assign a question type (1\u20135) from the taxonomy below and produce 1\u20135 focused sub-queries to send to a retrieval workflow.\n3) If it is NOT a question, mark it as not a question and do not generate sub-queries.\n\nTaxonomy (choose one best fit):\n1 = Factual lookup about known entities, product facts, definitions, specs\n2 = How-to / procedure / step-by-step instructions\n3 = Troubleshooting / diagnostics / error understanding\n4 = Policy / pricing / terms / compliance / rules\n5 = Comparative / decision support / pros-cons / selection criteria\n\nStrict rules:\n- Output ONLY valid JSON matching the schema below. No extra text.\n- Keep sub-queries precise, non-redundant, and self-contained.\n- Language: Match the user's language if detectable; otherwise default to German.\n- If not a question: set is_question = false, question_type = 0, queries = [].\n\nOutput JSON schema (all fields required):\n{\n  \"is_question\": boolean,\n  \"question_type\": 0 | 1 | 2 | 3 | 4 | 5,\n  \"queries\": [\n    { \"query\": \"string\" }\n  ],\n  \"notes\": \"short reasoning (1\u20132 sentences)\"\n}\n"
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "f8ab270c-ac47-4afa-9fdd-64bd0a778566",
  "connections": {
    "Switch": {
      "main": [
        [
          {
            "node": "Small Talk AI Agent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Split Sub-Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Understand Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any chunk?": {
      "main": [
        [
          {
            "node": "Aggregate Matching Chunks",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Say no chunk match",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JSON Formatter": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean RAG output": {
      "main": [
        [
          {
            "node": "Keep score over 0.4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Sub-Queries": {
      "main": [
        [
          {
            "node": "Loop Over Sub-Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Say no chunk match": {
      "main": [
        [
          {
            "node": "Prepare loop output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Understand Request": {
      "main": [
        [
          {
            "node": "JSON Formatter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep score over 0.4": {
      "main": [
        [
          {
            "node": "Any chunk?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare loop output": {
      "main": [
        [
          {
            "node": "Loop Over Sub-Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Small Talk AI Agent": {
      "main": [
        [
          {
            "node": "Remove Think Tags (Small Talk Path)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Sub-Queries": {
      "main": [
        [
          {
            "node": "Aggregate All Retrieval Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "PGVector Store \u2014 Retrieve Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Matching Chunks": {
      "main": [
        [
          {
            "node": "Prepare loop output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer Generator AI Agent": {
      "main": [
        [
          {
            "node": "Remove Think Tags (RAG Path)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ollama Embeddings (BGE-M3)": {
      "ai_embedding": [
        [
          {
            "node": "PGVector Store \u2014 Retrieve Chunks",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Remove Think Tags (RAG Path)": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate All Retrieval Results": {
      "main": [
        [
          {
            "node": "Answer Generator AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Chat Memory (RAG Answer)": {
      "ai_memory": [
        [
          {
            "node": "Answer Generator AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Chat Memory (Small Talk)": {
      "ai_memory": [
        [
          {
            "node": "Small Talk AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "PGVector Store \u2014 Retrieve Chunks": {
      "main": [
        [
          {
            "node": "Clean RAG output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Think Tags (Small Talk Path)": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ollama Chat Model (Small Talk \u2014 Qwen3:14b)": {
      "ai_languageModel": [
        [
          {
            "node": "Small Talk AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Ollama Chat Model (Classifier \u2014 Qwen2.5:7b)": {
      "ai_languageModel": [
        [
          {
            "node": "Understand Request",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Ollama Chat Model (Answer Generator \u2014 Qwen3:14b)": {
      "ai_languageModel": [
        [
          {
            "node": "Answer Generator AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  }
}