{
  "name": "RAG Query - Document Q&A",
  "nodes": [
    {
      "parameters": {
        "content": "## RAG Query Workflow\n\nThis workflow handles document Q&A using vector similarity search.\n\n**Flow:**\n1. Receive question from Chat or Webhook\n2. Generate query embedding\n3. Search similar chunks in pgvector\n4. Build context from retrieved chunks\n5. Send to Ollama LLM with context\n6. Return answer",
        "height": 300,
        "width": 320
      },
      "id": "sticky-note",
      "name": "Instructions",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -200,
        -100
      ]
    },
    {
      "parameters": {
        "options": {
          "responseMode": "responseNode"
        }
      },
      "id": "chat-trigger",
      "name": "Chat Trigger",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        200
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "rag-query",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-trigger",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "// Get question from either Chat Trigger or Webhook\nlet question = '';\nlet sessionId = '';\n\ntry {\n  // Try Chat Trigger first\n  const chatData = $('Chat Trigger').first()?.json;\n  if (chatData?.chatInput) {\n    question = chatData.chatInput;\n    sessionId = chatData.sessionId || 'default';\n  }\n} catch (e) {\n  // Fall back to Webhook\n  const webhookData = $('Webhook Trigger').first()?.json;\n  if (webhookData) {\n    question = webhookData.question || webhookData.query || webhookData.message || '';\n    sessionId = webhookData.sessionId || webhookData.session_id || 'webhook';\n  }\n}\n\nif (!question) {\n  throw new Error('No question provided. Send {\"question\": \"your question\"}');\n}\n\nreturn [{\n  json: {\n    question,\n    sessionId\n  }\n}];"
      },
      "id": "extract-question",
      "name": "Extract Question",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://192.168.50.49:11434/api/embeddings",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'mxbai-embed-large:latest', prompt: $json.question }) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "query-embedding",
      "name": "Generate Query Embedding",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const embedding = $input.first().json.embedding;\nconst question = $('Extract Question').first().json.question;\nconst sessionId = $('Extract Question').first().json.sessionId;\n\nif (!embedding || !Array.isArray(embedding)) {\n  throw new Error('Invalid embedding response from Ollama');\n}\n\n// Format embedding as PostgreSQL vector string\nconst vectorString = '[' + embedding.join(',') + ']';\n\nreturn [{\n  json: {\n    question,\n    sessionId,\n    queryEmbedding: vectorString\n  }\n}];"
      },
      "id": "format-embedding",
      "name": "Format Query Embedding",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  id,\n  document_name,\n  chunk_index,\n  content,\n  1 - (embedding <=> '{{ $json.queryEmbedding }}') as similarity\nFROM document_chunks\nWHERE embedding IS NOT NULL\nORDER BY embedding <=> '{{ $json.queryEmbedding }}'\nLIMIT 5;",
        "options": {}
      },
      "id": "vector-search",
      "name": "Vector Similarity Search",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        880,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const chunks = $input.all();\nconst question = $('Extract Question').first().json.question;\nconst sessionId = $('Extract Question').first().json.sessionId;\n\nif (chunks.length === 0) {\n  return [{\n    json: {\n      question,\n      sessionId,\n      context: 'No relevant documents found in the knowledge base.',\n      hasContext: false\n    }\n  }];\n}\n\n// Build context from retrieved chunks\nlet context = '';\nconst sources = [];\n\nfor (const chunk of chunks) {\n  const data = chunk.json;\n  const similarity = (parseFloat(data.similarity) * 100).toFixed(1);\n  context += `\\n\\n--- From: ${data.document_name} (Chunk ${data.chunk_index}, Relevance: ${similarity}%) ---\\n${data.content}`;\n  \n  if (!sources.includes(data.document_name)) {\n    sources.push(data.document_name);\n  }\n}\n\nreturn [{\n  json: {\n    question,\n    sessionId,\n    context: context.trim(),\n    sources,\n    chunkCount: chunks.length,\n    hasContext: true\n  }\n}];"
      },
      "id": "build-context",
      "name": "Build Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://192.168.50.49:11434/api/generate",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  model: 'gpt-oss:120b',\n  prompt: `You are a helpful assistant that answers questions based on the provided document context. Respond in the same language as the user's question. If the context doesn't contain relevant information, say so.\n\n=== DOCUMENT CONTEXT ===\n${$json.context}\n=== END CONTEXT ===\n\nUser Question: ${$json.question}\n\nProvide a clear, accurate answer based on the context above:`,\n  stream: false\n}) }}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "ask-ollama",
      "name": "Ask Ollama LLM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const ollamaResponse = $input.first().json;\nconst contextData = $('Build Context').first().json;\n\nconst answer = ollamaResponse.response || 'Sorry, I could not generate an answer.';\n\n// Build response object\nconst response = {\n  answer,\n  question: contextData.question,\n  sources: contextData.sources || [],\n  chunksUsed: contextData.chunkCount || 0\n};\n\nreturn [{\n  json: response\n}];"
      },
      "id": "format-response",
      "name": "Format Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO chat_memory (session_id, role, content)\nVALUES \n  ('{{ $('Extract Question').first().json.sessionId }}', 'user', '{{ $('Extract Question').first().json.question.replace(/'/g, \"''\") }}'),\n  ('{{ $('Extract Question').first().json.sessionId }}', 'assistant', '{{ $json.answer.replace(/'/g, \"''\").substring(0, 10000) }}');",
        "options": {}
      },
      "id": "save-to-memory",
      "name": "Save to Chat Memory",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1760,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}"
      },
      "id": "respond-webhook",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1980,
        400
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $json.answer }}"
      },
      "id": "respond-chat",
      "name": "Respond to Chat",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1980,
        200
      ]
    }
  ],
  "connections": {
    "Chat Trigger": {
      "main": [
        [
          {
            "node": "Extract Question",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Extract Question",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Question": {
      "main": [
        [
          {
            "node": "Generate Query Embedding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Query Embedding": {
      "main": [
        [
          {
            "node": "Format Query Embedding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Query Embedding": {
      "main": [
        [
          {
            "node": "Vector Similarity Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vector Similarity Search": {
      "main": [
        [
          {
            "node": "Build Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Context": {
      "main": [
        [
          {
            "node": "Ask Ollama LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask Ollama LLM": {
      "main": [
        [
          {
            "node": "Format Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Response": {
      "main": [
        [
          {
            "node": "Save to Chat Memory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Chat Memory": {
      "main": [
        [
          {
            "node": "Respond to Chat",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    {
      "name": "RAG"
    },
    {
      "name": "Q&A"
    }
  ],
  "triggerCount": 2
}