{
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "rag-chat",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      },
      "id": "ae899ed1-09f0-4723-a0fb-2d00f1f0f81f",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        48,
        -96
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {}
      },
      "id": "3cc9a8f0-213c-4f90-8dc3-44fe586e8919",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        3600,
        -192
      ]
    },
    {
      "parameters": {
        "jsCode": "// ====================================\n// QUERY ANALYZER - Intent Detection\n// ====================================\n\nconst body = $input.first().json.body || $input.first().json;\nconst query = body.query || body.chatInput || '';\n\n// Intent detection\nconst isGreeting = /^(hi|hello|hey|good morning|good afternoon)/i.test(query.trim());\nconst isListDocs = /\\b(list|show|what)\\s+(documents|docs|manuals)\\b/i.test(query);\n\n// Characteristic detection\nconst hasTechnicalTerms = /\\b(span|block|gpm|pump|valve|sensor|controller|wire|wiring|component|module|analog|digital|bacnet|niagara|kitcontrol|honeywell)\\b/i.test(query);\nconst wantsVisuals = /\\b(show|image|diagram|picture|visual|schematic|wiring|drawing)\\b/i.test(query) || /wir(e|ing)/i.test(query);\nconst wantsDetails = /\\b(detailed|spec|specification|table|parameter|configuration|breakdown)\\b/i.test(query);\n\n// Determine query type\nlet queryType = 'technical';\nif (isGreeting) queryType = 'greeting';\nelse if (isListDocs) queryType = 'list_documents';\n\n// Routing flags\nconst needsDocRouting = queryType === 'technical' && !body.doc_id;\nconst needsImages = wantsVisuals && queryType === 'technical';\nconst needsTables = wantsDetails && queryType === 'technical';\n\nreturn [{\n  json: {\n    query,\n    queryType,\n    needsDocRouting,\n    needsImages,\n    needsTables,\n    doc_id: body.doc_id || null,\n    top_k: wantsDetails ? 30 : 20,\n    fts_weight: hasTechnicalTerms ? 0.6 : 0.4,\n    vector_weight: hasTechnicalTerms ? 0.4 : 0.6,\n    user_id: body.userId || body.user_id || 'anonymous',\n    username: body.username || 'User'\n  }\n}];"
      },
      "id": "1fe1d8d3-d668-45e6-a4c1-fbde8e1570b8",
      "name": "1. Query Analyzer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        304,
        -96
      ]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.queryType }}",
              "value2": "greeting"
            }
          ]
        }
      },
      "id": "25f66f1d-e7e1-4402-9fec-c0bfa41a56d2",
      "name": "2a. Is Greeting?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        512,
        -96
      ]
    },
    {
      "parameters": {
        "jsCode": "const analyzer = $input.first().json;\n\nreturn [{\n  json: {\n    answer: \"Hello! I'm your technical documentation assistant. I can help you with information about pumps, valves, wiring, configurations, and more. What would you like to know?\",\n    citations: [],\n    images: [],\n    tables: [],\n    metadata: {\n      timestamp: new Date().toISOString(),\n      queryType: 'greeting',\n      query: analyzer.query,\n      user_id: analyzer.user_id,\n      username: analyzer.username\n    },\n    debug: {\n      pipeline: 'greeting-shortcut',\n      executionPath: 'direct'\n    }\n  }\n}];"
      },
      "id": "c5b356f4-7435-4d2d-b963-a2eb40025182",
      "name": "Greeting Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        704,
        -224
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.needsDocRouting }}",
              "value2": true
            }
          ]
        }
      },
      "id": "e4ddbfa0-046f-4895-a4ed-70c92ac6e35c",
      "name": "2b. Needs Doc Routing?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        704,
        32
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a document router for a technical documentation system.\n\nUser query: {{ $json.query }}\n\nYour task:\n1. Identify which document types would contain this information\n2. Enhance the query with technical terms and synonyms for better search\n\nReturn ONLY valid JSON (no markdown, no explanation):\n{\n  \"doc_ids\": [\"document_id_1\", \"document_id_2\"],\n  \"enhanced_query\": \"improved search query with technical terms\",\n  \"reasoning\": \"brief explanation\"\n}\n\nExamples:\nQuery: \"How to wire a pump?\"\nResponse: {\"doc_ids\": [\"kitcontrol_manual\"], \"enhanced_query\": \"pump wiring connection configuration analog output controller\", \"reasoning\": \"Wiring info typically in control system manuals\"}\n\nNow process this query and return JSON:"
      },
      "id": "df1d624f-b1cd-4656-86bc-fa4200af90ed",
      "name": "3. Document Router Chain",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.4,
      "position": [
        928,
        224
      ]
    },
    {
      "parameters": {
        "options": {
          "maxTokens": 500,
          "temperature": 0.3
        }
      },
      "id": "0d33a6c5-d23f-473a-8b23-fc508a78eeb6",
      "name": "OpenAI Router Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1,
      "position": [
        832,
        416
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// ====================================\n// PARSE ROUTER OUTPUT\n// ====================================\n\nconst routerOutput = $input.first().json;\nconst analyzer = $('1. Query Analyzer').first().json;\n\nlet parsed;\n\n// Handle different output formats\nif (routerOutput.response) {\n  // LLM Chain returns {response: \"...\"}\n  const responseText = routerOutput.response;\n  const jsonMatch = responseText.match(/\\{[\\s\\S]*\\}/);\n  parsed = jsonMatch ? JSON.parse(jsonMatch[0]) : { doc_ids: [], enhanced_query: analyzer.query };\n} else if (routerOutput.doc_ids) {\n  // Already parsed JSON\n  parsed = routerOutput;\n} else {\n  // Fallback\n  parsed = { doc_ids: [], enhanced_query: analyzer.query };\n}\n\nconst doc_ids = parsed.doc_ids || [];\nconst enhanced_query = parsed.enhanced_query || analyzer.query;\n\nreturn [{\n  json: {\n    query: enhanced_query,\n    doc_ids,\n    top_k: analyzer.top_k,\n    fts_weight: analyzer.fts_weight,\n    vector_weight: analyzer.vector_weight,\n    original_query: analyzer.query,\n    routing_reasoning: parsed.reasoning || 'No routing performed'\n  }\n}];"
      },
      "id": "f41b0b1f-7e98-4dff-bf07-197988c371eb",
      "name": "4. Parse Router Output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1216,
        224
      ]
    },
    {
      "parameters": {
        "jsCode": "// ====================================\n// PREPARE SEARCH (No Routing Path)\n// ====================================\n\nconst analyzer = $input.first().json;\n\nreturn [{\n  json: {\n    query: analyzer.query,\n    doc_ids: analyzer.doc_id ? [analyzer.doc_id] : null,\n    top_k: analyzer.top_k,\n    fts_weight: analyzer.fts_weight,\n    vector_weight: analyzer.vector_weight,\n    original_query: analyzer.query,\n    routing_reasoning: 'Direct search without routing'\n  }\n}];"
      },
      "id": "01989b7d-b0f8-44e4-bf3b-f5629dc985ce",
      "name": "4b. Prepare Direct Search",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        912,
        -96
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://dwisbglrutplhcotbehy.supabase.co/functions/v1/hybrid-search",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  query: $json.query,\n  doc_ids: $json.doc_ids,\n  top_k: $json.top_k,\n  fts_weight: $json.fts_weight,\n  vector_weight: $json.vector_weight\n}) }}",
        "options": {}
      },
      "id": "45d1f9e6-b94a-4780-bfd6-935d7647dcfc",
      "name": "5. Hybrid Search",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        1424,
        -96
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// ====================================\n// EXTRACT CHUNK IDS\n// ====================================\n\nconst searchResult = $input.first().json;\nconst chunks = searchResult.chunks || [];\n\nif (chunks.length === 0) {\n  return [{\n    json: {\n      error: 'No chunks found',\n      chunk_ids: [],\n      doc_id: null,\n      chunks: []\n    }\n  }];\n}\n\nconst chunk_ids = chunks.map(c => c.id);\nconst doc_id = chunks[0].doc_id;\n\nreturn [{\n  json: {\n    chunk_ids,\n    doc_id,\n    chunks_count: chunks.length,\n    chunks_preview: chunks.slice(0, 3).map(c => ({\n      id: c.id,\n      page: c.page_number,\n      preview: c.content.substring(0, 100)\n    }))\n  }\n}];"
      },
      "id": "2feb600c-cbb6-4dd0-b309-2b303c5bf2e3",
      "name": "6. Extract Chunk IDs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1616,
        -96
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://dwisbglrutplhcotbehy.supabase.co/functions/v1/context-expansion",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  chunk_ids: $json.chunk_ids,\n  doc_id: $json.doc_id,\n  token_budget: 6000,\n  expand_siblings: true,\n  expand_parents: true\n}) }}",
        "options": {}
      },
      "id": "41dfbbde-e284-48b3-970d-3648484e0d5f",
      "name": "7. Context Expansion",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        1792,
        -96
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $('1. Query Analyzer').first().json.needsImages }}",
              "value2": true
            }
          ]
        }
      },
      "id": "23a0798b-eba7-46da-8b98-f8c98af32e0d",
      "name": "8. Needs Images?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        1968,
        96
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://dwisbglrutplhcotbehy.supabase.co/functions/v1/retrieve-with-images",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  chunk_ids: $('6. Extract Chunk IDs').first().json.chunk_ids\n}) }}",
        "options": {}
      },
      "id": "84a3429b-ba43-4fd3-8fe5-d9f7b2dd1414",
      "name": "9a. Retrieve Images",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        2144,
        -96
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $('1. Query Analyzer').first().json.needsTables }}",
              "value2": true
            }
          ]
        }
      },
      "id": "2d52a50a-9b96-45ab-9899-dcf0c17c8600",
      "name": "9b. Needs Tables?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        2336,
        112
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://dwisbglrutplhcotbehy.supabase.co/functions/v1/retrieve-with-tables",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  chunk_ids: $('6. Extract Chunk IDs').first().json.chunk_ids\n}) }}",
        "options": {}
      },
      "id": "564f35be-f4c0-4d42-aaa2-823dcf34fcd2",
      "name": "10a. Retrieve Tables",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        2528,
        -96
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// ====================================\n// MERGE ALL RESULTS - FIXED\n// ====================================\n\nconst expansionResult = $('7. Context Expansion').first().json;\n\n// Safely get nodes - they may not have executed\nlet imagesNode = null;\nlet tablesNode = null;\n\ntry {\n  imagesNode = $('9a. Retrieve Images').first();\n} catch (e) {\n  // Node didn't execute, that's ok\n}\n\ntry {\n  tablesNode = $('10a. Retrieve Tables').first();\n} catch (e) {\n  // Node didn't execute, that's ok\n}\n\nconst expanded_chunks = expansionResult.expanded_chunks || [];\n\n// Extract images if retrieved\nconst images = (imagesNode && imagesNode.json && imagesNode.json.chunks)\n  ? imagesNode.json.chunks.flatMap(c => c.images || [])\n  : [];\n\n// Extract tables if retrieved\nconst tables = (tablesNode && tablesNode.json && tablesNode.json.chunks)\n  ? tablesNode.json.chunks.flatMap(c => c.tables || [])\n  : [];\n\n// Format context for LLM\nconst contextText = expanded_chunks\n  .map(chunk => `[Page ${chunk.page_number}]\\n${chunk.content}`)\n  .join('\\n\\n---\\n\\n');\n\nconst imagesText = images.length > 0\n  ? images\n      .map(img => `[Image - Page ${img.page_number}]: ${img.caption || img.summary || 'Diagram'}`)\n      .join('\\n')\n  : 'No images available';\n\nconst tablesText = tables.length > 0\n  ? tables\n      .map(tbl => `[Table - Page ${tbl.page_number}]:\\n${tbl.markdown}\\n${tbl.description || ''}`)\n      .join('\\n\\n')\n  : 'No tables available';\n\nreturn [\n  {\n    json: {\n      context: contextText,\n      images_description: imagesText,\n      tables_content: tablesText,\n      images_array: images,\n      tables_array: tables,\n      metadata: {\n        chunks_count: expanded_chunks.length,\n        images_count: images.length,\n        tables_count: tables.length,\n      },\n    },\n  },\n];\n"
      },
      "id": "131b3e5f-2916-4844-b826-0ed76c2d03e6",
      "name": "11. Merge Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2736,
        128
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a technical documentation assistant. Generate a comprehensive answer based on the retrieved context.\n\nUser Query: {{ $('1. Query Analyzer').first().json.query }}\n\n=== RETRIEVED CONTEXT ===\n{{ $json.context }}\n\n=== AVAILABLE IMAGES ===\n{{ $json.images_description }}\n\n=== AVAILABLE TABLES ===\n{{ $json.tables_content }}\n\n=== INSTRUCTIONS ===\n1. Answer the user's query comprehensively using the context provided\n2. Cite specific pages using [Page X] format after each fact\n3. If images are available and relevant, reference them: \"See the wiring diagram [Page 45]\"\n4. If tables are available, reference them: \"See specifications table [Page 46]\"\n5. Use clear formatting with headings and bullet points\n6. Be technical and accurate\n7. If context doesn't fully answer the query, acknowledge limitations\n\nGenerate your answer:"
      },
      "id": "99a4f4a5-9f8e-40fb-9921-3a105ca362b0",
      "name": "12. Answer Generator Chain",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.4,
      "position": [
        2960,
        128
      ]
    },
    {
      "parameters": {
        "options": {
          "maxTokens": 2000,
          "temperature": 0.3
        }
      },
      "id": "53f0f11f-4c37-4e6b-9d33-dfbe475270f5",
      "name": "OpenAI Answer Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1,
      "position": [
        2912,
        320
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// ====================================\n// FORMAT FINAL RESPONSE\n// ====================================\n\nconst answerOutput = $input.first().json;\nconst answerText = answerOutput.response || answerOutput.text || answerOutput.output || 'Error generating answer';\nconst mergedData = $('11. Merge Results').first().json;\nconst analyzer = $('1. Query Analyzer').first().json;\n\n// Extract citations\nconst citationRegex = /\\[Page\\s+(\\d+)\\]/gi;\nconst citations = [];\nlet match;\nwhile ((match = citationRegex.exec(answerText)) !== null) {\n  citations.push({ page: parseInt(match[1]) });\n}\n\n// Get unique citations\nconst uniqueCitations = [...new Set(citations.map(c => c.page))].map(page => ({ page }));\n\nreturn [{\n  json: {\n    answer: answerText,\n    citations: uniqueCitations,\n    images: mergedData.images_array,\n    tables: mergedData.tables_array,\n    metadata: {\n      timestamp: new Date().toISOString(),\n      query: analyzer.query,\n      queryType: analyzer.queryType,\n      chunks_retrieved: mergedData.metadata.chunks_count,\n      images_retrieved: mergedData.metadata.images_count,\n      tables_retrieved: mergedData.metadata.tables_count,\n      user_id: analyzer.user_id,\n      username: analyzer.username\n    },\n    debug: {\n      needsImages: analyzer.needsImages,\n      needsTables: analyzer.needsTables,\n      needsDocRouting: analyzer.needsDocRouting,\n      pipeline: 'deterministic-chains-v1'\n    }\n  }\n}];"
      },
      "id": "1b223ac8-9ba5-42e7-9beb-4a8e66aef0ef",
      "name": "13. Format Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3296,
        -32
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "1. Query Analyzer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Query Analyzer": {
      "main": [
        [
          {
            "node": "2a. Is Greeting?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2a. Is Greeting?": {
      "main": [
        [
          {
            "node": "Greeting Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "2b. Needs Doc Routing?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Greeting Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2b. Needs Doc Routing?": {
      "main": [
        [
          {
            "node": "3. Document Router Chain",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "4b. Prepare Direct Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Document Router Chain": {
      "main": [
        [
          {
            "node": "4. Parse Router Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Router Model": {
      "ai_languageModel": [
        [
          {
            "node": "3. Document Router Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "4. Parse Router Output": {
      "main": [
        [
          {
            "node": "5. Hybrid Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4b. Prepare Direct Search": {
      "main": [
        [
          {
            "node": "5. Hybrid Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Hybrid Search": {
      "main": [
        [
          {
            "node": "6. Extract Chunk IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. Extract Chunk IDs": {
      "main": [
        [
          {
            "node": "7. Context Expansion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7. Context Expansion": {
      "main": [
        [
          {
            "node": "8. Needs Images?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8. Needs Images?": {
      "main": [
        [
          {
            "node": "9a. Retrieve Images",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "9b. Needs Tables?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9a. Retrieve Images": {
      "main": [
        [
          {
            "node": "9b. Needs Tables?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9b. Needs Tables?": {
      "main": [
        [
          {
            "node": "10a. Retrieve Tables",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "11. Merge Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "10a. Retrieve Tables": {
      "main": [
        [
          {
            "node": "11. Merge Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "11. Merge Results": {
      "main": [
        [
          {
            "node": "12. Answer Generator Chain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "12. Answer Generator Chain": {
      "main": [
        [
          {
            "node": "13. Format Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Answer Model": {
      "ai_languageModel": [
        [
          {
            "node": "12. Answer Generator Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "13. Format Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "templateCredsSetupCompleted": true
  }
}