{
  "name": "Idempotency Demo",
  "nodes": [
    {
      "parameters": {
        "mode": "insert",
        "memoryKey": "rag_knowledge_base"
      },
      "id": "bc151bfc-6585-4728-b7ab-589f9e6960ba",
      "name": "Simple Vector Store (Insert)",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreInMemory",
      "position": [
        736,
        -448
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "modelName": "models/gemini-embedding-001"
      },
      "id": "48cde7bc-e83b-42b6-832b-51eb61038b4b",
      "name": "Embeddings Google Gemini",
      "type": "@n8n/n8n-nodes-langchain.embeddingsGoogleGemini",
      "position": [
        592,
        -80
      ],
      "typeVersion": 1,
      "credentials": {
        "googlePalmApi": {
          "id": "2NCNQNCI7N9cWxgE",
          "name": "Google Gemini(PaLM) Api account"
        }
      }
    },
    {
      "parameters": {
        "dataType": "binary",
        "options": {
          "metadata": {
            "metadataValues": [
              {
                "name": "source",
                "value": "={{ $json.name }}"
              }
            ]
          }
        }
      },
      "id": "725ea61a-1523-4cc5-86a4-fd349357bfb1",
      "name": "Default Data Loader",
      "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
      "position": [
        768,
        -224
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "chunkSize": 1500,
        "chunkOverlap": 200,
        "options": {}
      },
      "id": "41695038-06d9-4f8d-84f2-cee25086c6d0",
      "name": "Recursive Character Text Splitter",
      "type": "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
      "position": [
        832,
        16
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "name": "search_documents",
        "description": "Use this tool to search and retrieve information from the uploaded documents. Always use this tool for ANY question about course content, definitions, concepts, or topics. This is your primary source of information."
      },
      "id": "d4e9762a-6e74-430d-b152-c6f20c9b0d41",
      "name": "Vector Store Tool",
      "type": "@n8n/n8n-nodes-langchain.toolVectorStore",
      "position": [
        1008,
        528
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "memoryKey": "rag_knowledge_base"
      },
      "id": "83abae37-84aa-4b6f-af3c-13d3e1e463ab",
      "name": "Simple Vector Store (Retrieval)",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreInMemory",
      "position": [
        880,
        720
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "modelName": "models/gemini-embedding-001"
      },
      "id": "ae6a3f22-46eb-42da-bf5d-e72fa03418c5",
      "name": "Embeddings Google Gemini (retrieval)",
      "type": "@n8n/n8n-nodes-langchain.embeddingsGoogleGemini",
      "position": [
        896,
        896
      ],
      "typeVersion": 1,
      "credentials": {
        "googlePalmApi": {
          "id": "2NCNQNCI7N9cWxgE",
          "name": "Google Gemini(PaLM) Api account"
        }
      }
    },
    {
      "parameters": {
        "operation": "download",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.id }}"
        },
        "options": {
          "fileName": "={{ $json.name }}"
        }
      },
      "id": "b7cbcfdc-2659-410f-872b-c45c8613a61e",
      "name": "Download File From Google Drive",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        304,
        -448
      ],
      "typeVersion": 3,
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "29Az9xaq2Tj747c1",
          "name": "Google Drive account"
        }
      }
    },
    {
      "parameters": {},
      "id": "f020c427-9611-4152-83e4-458a19d5d873",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -176,
        -448
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "resource": "fileFolder",
        "filter": {
          "folderId": {
            "__rl": true,
            "value": "1ti3PBAC7VMVFMB06JLdJasJCh6qTIISj",
            "mode": "list",
            "cachedResultName": "n8n",
            "cachedResultUrl": "https://drive.google.com/drive/folders/1ti3PBAC7VMVFMB06JLdJasJCh6qTIISj"
          }
        },
        "options": {}
      },
      "id": "3787bdc3-fb23-4a65-aedd-cfcc5325b66f",
      "name": "Google Drive - Search Files",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        64,
        -448
      ],
      "typeVersion": 3,
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "29Az9xaq2Tj747c1",
          "name": "Google Drive account"
        }
      }
    },
    {
      "parameters": {
        "contextWindowLength": 10
      },
      "id": "eb11c070-4d49-4073-a459-6c4e0e7558f7",
      "name": "Window Buffer Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        832,
        496
      ],
      "typeVersion": 1.3
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "b4b57a3e-5458-4007-86a8-16239a7647c4",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        -448,
        608
      ],
      "webhookId": "91c96331-064c-414e-b0c8-186fdb7c0a33",
      "typeVersion": 1.1
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "05013ab2-0955-4c39-84a3-e7514a1cd6fc",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        656,
        528
      ],
      "typeVersion": 1,
      "credentials": {
        "googlePalmApi": {
          "id": "2NCNQNCI7N9cWxgE",
          "name": "Google Gemini(PaLM) Api account"
        }
      }
    },
    {
      "parameters": {
        "modelName": "models/gemini-2.0-flash",
        "options": {}
      },
      "id": "3c8f2cd5-94f7-4e38-9749-b0fa12ceed22",
      "name": "Google Gemini Chat Model (retrieval)",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        1200,
        736
      ],
      "typeVersion": 1,
      "retryOnFail": true,
      "waitBetweenTries": 5000,
      "credentials": {
        "googlePalmApi": {
          "id": "2NCNQNCI7N9cWxgE",
          "name": "Google Gemini(PaLM) Api account"
        }
      }
    },
    {
      "parameters": {
        "content": "## RAG Document Retrieval Agent",
        "height": 1048,
        "width": 3512,
        "color": 6
      },
      "id": "aa94fb7c-dc0f-4be8-9680-7ca81928d1d2",
      "name": "Sticky Note - Chat",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -544,
        208
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU",
          "mode": "list",
          "cachedResultName": "Agent Memory",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "memory",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit#gid=0"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -240,
        608
      ],
      "id": "a64ff17d-4f09-4630-bca4-cc525b734b73",
      "name": "Google Sheets - Read Memory",
      "alwaysOutputData": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aRx38zJntw4usFjZ",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU",
          "mode": "list",
          "cachedResultName": "Agent Memory",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "memory",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit#gid=0"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "query": "={{ $json.query }}",
            "response_summary": "={{ $('AI Agent').first().json.output.substring(0, 300) }}",
            "documents_used": "={{ $('AI Agent').first().json.output.match(/Source: .+/g)?.join(', ') || 'None cited' }}\n",
            "timestamp": "={{ $now.toISO() }}",
            "expires_at": "={{  $now.plus({days: 90}).toISO() }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "query",
              "displayName": "query",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "response_summary",
              "displayName": "response_summary",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "documents_used",
              "displayName": "documents_used",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "timestamp",
              "displayName": "timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "expires_at",
              "displayName": "expires_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2192,
        656
      ],
      "id": "8f4e28c4-ee2a-40f2-9659-8b0934203610",
      "name": "Google Sheets - Write Memory",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aRx38zJntw4usFjZ",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "content": "## Document Retrieval",
        "height": 688,
        "width": 1408,
        "color": 4
      },
      "id": "fd1bc31c-24f6-4b4a-91b3-f924c913c087",
      "name": "Sticky Note - Ingestion",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -256,
        -528
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU",
          "mode": "list",
          "cachedResultName": "Agent Memory",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 796874551,
          "mode": "list",
          "cachedResultName": "loaded_documents",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit#gid=796874551"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "document_name": "={{ $json.name }}",
            "loaded_at": "={{ $now.toISO() }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "document_name",
              "displayName": "document_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "loaded_at",
              "displayName": "loaded_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        496,
        -336
      ],
      "id": "750842e8-7cfa-4661-a6d3-09bdc7c67a81",
      "name": "Google Sheets - Doc Names",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aRx38zJntw4usFjZ",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU",
          "mode": "list",
          "cachedResultName": "Agent Memory",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 796874551,
          "mode": "list",
          "cachedResultName": "loaded_documents",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit#gid=796874551"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -16,
        608
      ],
      "id": "c47f3f8e-c067-443a-9d74-3544eda3e60e",
      "name": "Google Sheets - Read Docs",
      "alwaysOutputData": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aRx38zJntw4usFjZ",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const agentOutput = $input.last().json.output || '';\nconst chatInput = $('When chat message received').first().json.chatInput || '';\n\n// Extract parts\nconst answerMatch = agentOutput.match(/Answer:\\s*([\\s\\S]*?)(?=\\nSource:|$)/);\nconst sourceMatch = agentOutput.match(/Source:\\s*(.+)/);\nconst confidenceMatch = agentOutput.match(/Confidence:\\s*(.+)/);\n\nlet answer = answerMatch ? answerMatch[1].trim() : agentOutput;\n\n// Remove any leftover Source/Confidence lines from the answer text\nanswer = answer.replace(/Source:.*$/gm, '').replace(/Confidence:.*$/gm, '').trim();\n\nconst source = sourceMatch ? sourceMatch[1].trim() : 'Unknown';\nconst confidence = confidenceMatch ? confidenceMatch[1].trim() : 'Unknown';\n\nreturn {\n  output: `${answer}\\n\\nSource: ${source}\\nConfidence: ${confidence}`,\n  query: chatInput,\n  response_summary: agentOutput.substring(0, 300),\n  documents_used: source\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1408,
        224
      ],
      "id": "701f003f-7792-47a2-aa2f-95acfb05affe",
      "name": "Code in JavaScript"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $('When chat message received').first().json.chatInput }}",
        "options": {
          "systemMessage": "=You are a research assistant specializing in document retrieval.\nYou MUST use the search_documents tool for EVERY question before responding. NEVER answer from memory or document names alone.\n\nLOADED DOCUMENTS:\n{{ $('Google Sheets - Read Docs').all().map(r => '- ' + r.json.document_name).join('\\n') || 'No documents loaded.' }}\n\nRULES:\n- Use ONLY information from the provided documents. Do NOT infer or assume.\n- Identify which document the answer came from by matching content to the LOADED DOCUMENTS list.\n- Include a confidence level (high/medium/low) with every answer. Use \"low\" if the retrieved text only mentions the term briefly.\n- Empty results: \"The knowledge base is currently empty. Please run the document ingestion workflow first.\"\n- Answer not found: \"The provided documents do not contain sufficient information to answer this question.\"\n- Conflicting information: present both sources and let the user decide.\n- If unsure which document, state your best match and set confidence to medium.\n\nOUTPUT FORMAT:\nAnswer: [your answer]\nSource: [document name]\nConfidence: [high/medium/low]\n\nMEMORY CONTEXT:\n{{ $('Google Sheets - Read Memory').all().map(r => 'Q: ' + r.json.query + ' → A: ' + r.json.response_summary).join('\\n') || 'No prior interactions found.' }}\nReference past queries for follow-up continuity.\n\nUse tools directly. Do NOT write code, print statements, or use print()/default_api.\nWhen multiple chunks are returned, prefer the one with the most substantive explanation. If unsure, call search_documents again with a rephrased query.",
          "passthroughBinaryImages": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        656,
        240
      ],
      "id": "26000e57-6eba-4940-9a08-ef1b4b38c314",
      "name": "AI Agent",
      "retryOnFail": true,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "try {\n  const codeOutput = $('Code in JavaScript').first().json;\n  return { output: codeOutput.output };\n} catch (e) {\n  const guardOutput = $('Output Guard Response').first().json;\n  return { output: guardOutput.output };\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2800,
        240
      ],
      "id": "4cb9b0fb-ccd3-4fdf-a6f1-742080374254",
      "name": "Final Output"
    },
    {
      "parameters": {
        "jsCode": "// INPUT GUARD - Checks before AI Agent runs\nconst input = $('When chat message received').first().json.chatInput || '';\nconst inputLower = input.toLowerCase();\nconst inputCleaned = inputLower.replace(/[-_.\\/\\\\*#@!]/g, '');\n\n// --- HARD BLOCK: Prompt Injection ---\nconst injectionPatterns = [\n  /ignore\\s+(your|previous|all|these|my)?\\s*(instructions|rules|prompt)/,\n  /disregard\\s+your\\s+(training|rules|instructions)/,\n  /pretend\\s+you\\s+are/,\n  /you\\s+are\\s+now/,\n  /reveal\\s+your\\s+(system\\s+)?prompt/,\n  /override\\s+(safety|guardrail|system)/,\n  /forget\\s+(everything|your\\s+rules)/,\n  /tell\\s+me\\s+your\\s+(system\\s+)?prompt/,\n  /show\\s+me\\s+your\\s+(instructions|rules|prompt)/,\n  /what\\s+(are|is)\\s+your\\s+(system\\s+)?(prompt|instructions)/,\n];\n\nfor (const pattern of injectionPatterns) {\n  if (pattern.test(inputCleaned)) {\n    return {\n      action: \"BLOCK\",\n      rule_id: \"INP001\",\n      rule_name: \"Prompt Injection\",\n      response: \"I'm a document retrieval assistant. I can help you find information from your uploaded course materials. What topic would you like to look up?\"\n    };\n  }\n}\n\n// --- HARD BLOCK: Off-Topic Detection ---\nconst offTopicPatterns = [\n  /write\\s+(me\\s+)?(a\\s+)?(poem|song|story|essay|joke)/,\n  /tell\\s+(me\\s+)?a\\s+joke/,\n  /what('s|\\s+is)\\s+the\\s+weather/,\n  /how\\s+do\\s+i\\s+(cook|make\\s+food|bake)/,\n  /recipe\\s+for/,\n  /play\\s+(a\\s+)?game/,\n  /roleplay|role\\s+play/,\n  /generate\\s+(an?\\s+)?image/,\n  /translate\\s+.+\\s+to\\s+/,\n  /what('s|\\s+is)\\s+your\\s+(name|favorite|opinion)/,\n  /who\\s+(are\\s+you|made\\s+you|created\\s+you)/,\n  /can\\s+you\\s+(sing|draw|code|hack)/,\n];\n\nconst isOffTopic = offTopicPatterns.some(pattern => pattern.test(inputCleaned));\n\nif (isOffTopic) {\n  return {\n    action: \"BLOCK\",\n    rule_id: \"INP002\",\n    rule_name: \"Off-Topic Query\",\n    response: \"I'm designed to retrieve information from your uploaded documents. I can't help with that request. What would you like me to look up in your documents?\"\n  };\n}\n\n// --- PASSED ALL INPUT CHECKS ---\nreturn {\n  action: \"ALLOW\",\n  rule_id: \"PASS\",\n  original_input: input\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        192,
        608
      ],
      "id": "3d5fe13c-40f3-4035-a98c-0aee43fe382f",
      "name": "Input Guard"
    },
    {
      "parameters": {
        "jsCode": "const guardResult = $input.last().json;\nreturn {\n  output: guardResult.response\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        896,
        1056
      ],
      "id": "33190237-7285-481e-ba41-65f52c2bef6d",
      "name": "Input Block Response",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "// OUTPUT GUARD - Checks after AI Agent responds\nconst agentOutput = $input.last().json.output || '';\n\n// Get loaded documents for source verification\nconst loadedDocs = $('Google Sheets - Read Docs').all().map(r => \n  (r.json.document_name || '').toLowerCase()\n);\n\n// --- HELPER: Check if a valid source is cited ---\nconst sourceMatch = agentOutput.match(/source:\\s*(.+)/i);\nlet citedSource = '';\nlet isValidSource = false;\n\nif (sourceMatch) {\n  citedSource = sourceMatch[1].trim().toLowerCase();\n  isValidSource = loadedDocs.some(doc => \n    doc && (citedSource.includes(doc) || doc.includes(citedSource))\n  );\n}\n\n// --- HARD BLOCK #1: General Knowledge Fallback ---\n// Only block if agent has NO valid source citation\n// If agent says \"not sufficient\" but still cites a real source, treat as partial answer\nconst noInfoPatterns = [\n  /documents do not contain sufficient information/i,\n  /knowledge base is currently empty/i,\n  /could not find/i,\n  /no relevant (results|information)/i,\n];\nconst hasNoInfo = noInfoPatterns.some(pattern => pattern.test(agentOutput));\n\nif (hasNoInfo && !isValidSource) {\n  return {\n    action: \"BLOCK\",\n    rule_id: \"OUT001\",\n    rule_name: \"General Knowledge Fallback\",\n    original_output: agentOutput,\n    output: \"The provided documents do not contain sufficient information to answer this question.\"\n  };\n}\n\n// If agent said \"not sufficient\" BUT has a valid source, rewrite as partial answer\nif (hasNoInfo && isValidSource) {\n  // Strip the \"not sufficient\" opener and present the actual content\n  const confidenceMatch = agentOutput.match(/confidence:\\s*(high|medium|low)/i);\n  const confidenceLevel = confidenceMatch ? confidenceMatch[1].toLowerCase() : 'low';\n  \n  // Extract the useful content after the NOT_FOUND phrasing\n  // Look for \"However,\" or the actual answer content\n  let cleanedOutput = agentOutput;\n  const howeverMatch = agentOutput.match(/however[,:]?\\s*([\\s\\S]*)/i);\n  if (howeverMatch) {\n    cleanedOutput = howeverMatch[1].trim();\n    // Re-attach Source and Confidence if they got separated\n    if (!/source:/i.test(cleanedOutput) && sourceMatch) {\n      cleanedOutput += '\\nSource: ' + sourceMatch[1].trim();\n    }\n    if (!/confidence:/i.test(cleanedOutput) && confidenceMatch) {\n      cleanedOutput += '\\nConfidence: ' + confidenceLevel;\n    }\n    cleanedOutput = 'Answer: ' + cleanedOutput;\n  }\n\n  return {\n    action: \"APPROVAL\",\n    rule_id: \"APR001\",\n    rule_name: \"Low-Confidence Retrieval\",\n    original_output: agentOutput,\n    output: cleanedOutput + \"\\n\\n⚠️ Note: This response has low confidence. The information found may not fully answer your query. Would you like me to try a different search?\"\n  };\n}\n\n// Also check for Source: Unknown indicators\nconst noSourceIndicators = [\n  /source:\\s*unknown/i,\n  /source:\\s*n\\/a/i,\n  /source:\\s*none/i,\n  /source:\\s*general\\s+knowledge/i,\n];\nconst hasNoSource = noSourceIndicators.some(pattern => pattern.test(agentOutput));\n\nif (hasNoSource) {\n  return {\n    action: \"BLOCK\",\n    rule_id: \"OUT001\",\n    rule_name: \"General Knowledge Fallback\",\n    original_output: agentOutput,\n    output: \"The provided documents do not contain sufficient information to answer this question.\"\n  };\n}\n\n// --- HARD BLOCK #2: Unverified Source Citation ---\nif (sourceMatch && !isValidSource && citedSource !== 'unknown') {\n  return {\n    action: \"BLOCK\",\n    rule_id: \"OUT002\",\n    rule_name: \"Unverified Source Citation\",\n    original_output: agentOutput,\n    output: \"I was unable to verify the source for this information. Please try rephrasing your question.\"\n  };\n}\n\n// --- APPROVAL RULE #1: Low Confidence Retrieval ---\nconst confidenceCheck = agentOutput.match(/confidence:\\s*(high|medium|low)/i);\nif (confidenceCheck && confidenceCheck[1].toLowerCase() === 'low') {\n  return {\n    action: \"APPROVAL\",\n    rule_id: \"APR001\",\n    rule_name: \"Low-Confidence Retrieval\",\n    original_output: agentOutput,\n    output: agentOutput + \"\\n\\n⚠️ Note: This response has low confidence. The retrieved information may not fully match your query. Would you like me to try a different search?\"\n  };\n}\n\n// --- APPROVAL RULE #2: Conflicting Information ---\nconst conflictIndicators = [\n  /conflicting\\s+information/i,\n  /different\\s+(perspectives|viewpoints|definitions)/i,\n  /disagree|contradiction|inconsistent/i,\n  /both\\s+sources/i,\n];\nconst hasConflict = conflictIndicators.some(pattern => pattern.test(agentOutput));\n\nif (hasConflict) {\n  return {\n    action: \"APPROVAL\",\n    rule_id: \"APR002\",\n    rule_name: \"Conflicting Information Across Documents\",\n    original_output: agentOutput,\n    output: agentOutput + \"\\n\\n⚠️ I found potentially conflicting information across documents. Would you like me to return all relevant results so you can decide what's most applicable?\"\n  };\n}\n\n// --- PASSED ALL OUTPUT CHECKS ---\nreturn {\n  action: \"ALLOW\",\n  rule_id: \"PASS\",\n  output: agentOutput\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        992,
        240
      ],
      "id": "f1d54803-cf39-4045-ae7f-75ae62773d88",
      "name": "Output Guard"
    },
    {
      "parameters": {
        "jsCode": "const guardResult = $input.last().json;\nconst chatInput = $('When chat message received').first().json.chatInput || '';\n\nreturn {\n  output: guardResult.output,\n  query: chatInput,\n  response_summary: guardResult.rule_name + \" - \" + guardResult.rule_id + \": \" + guardResult.output.substring(0, 200),\n  documents_used: \"Guardrail triggered - \" + guardResult.rule_name\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1600,
        752
      ],
      "id": "eb26fbb3-6b07-4f20-ae14-5b8eeaba0bf3",
      "name": "Output Guard Response",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "39bdf9d0-6728-47c9-a8bf-e1bda31a71e6",
              "leftValue": "={{ $json.action }}",
              "rightValue": "ALLOW",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        400,
        608
      ],
      "id": "6ccc3d55-7778-4a6a-a556-f8f01b32cc4b",
      "name": "Input Check",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "2c9e516d-2acb-413b-a76d-f92bcef513ee",
              "leftValue": "={{ $json.action }}",
              "rightValue": "ALLOW",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1184,
        240
      ],
      "id": "c0d74781-5617-4934-90a9-c5d75c8d3d36",
      "name": "Output Check"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU",
          "mode": "list",
          "cachedResultName": "Agent Memory",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1303274900,
          "mode": "list",
          "cachedResultName": "log_blocked",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1orKgKRgThU3AYeomkMm_RgNpgCMleTp228ZT57fpvIU/edit#gid=1303274900"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "query": "={{ $('When chat message received').first().json.chatInput }}",
            "rule_id": "={{ $('Input Guard').first().json.rule_id }}",
            "rule_name": "={{ $('Input Guard').first().json.rule_name }}",
            "response": "={{ $('Input Guard').first().json.response }}",
            "timestamp": "={{ $now.toISO() }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "query",
              "displayName": "query",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "rule_id",
              "displayName": "rule_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "rule_name",
              "displayName": "rule_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "response",
              "displayName": "response",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "timestamp",
              "displayName": "timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        608,
        1040
      ],
      "id": "87330e59-482a-4b2c-9315-d4dde6ec134f",
      "name": "Google Sheets - Log Input Blocked",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aRx38zJntw4usFjZ",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Generate a hash-based idempotency key from the user's query\nconst crypto = require('crypto');\n\nconst query = $json.query || '';\nconst timestamp = new Date().toISOString().split('T')[0]; // date only (YYYY-MM-DD)\n\nconst hash = crypto\n  .createHash('sha256')\n  .update(query.toLowerCase().trim() + '_' + timestamp)\n  .digest('hex')\n  .substring(0, 16);\n\nconst idempotencyKey = `idem:${hash}`;\n\nreturn {\n  ...$json,\n  idempotency_key: idempotencyKey\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1584,
        224
      ],
      "id": "e904d72b-6eb2-4da5-98e1-63683d77d399",
      "name": "Generate Idempotency Key"
    },
    {
      "parameters": {
        "operation": "get",
        "key": "={{ $json.idempotency_key }}",
        "options": {
          "dotNotation": false
        }
      },
      "type": "n8n-nodes-base.redis",
      "typeVersion": 1,
      "position": [
        1744,
        224
      ],
      "id": "bcc3fef6-4c8c-47e5-bbd6-36e1bf33c0f2",
      "name": "Check Redis",
      "credentials": {
        "redis": {
          "id": "Fe910U4Sd5ZTORGy",
          "name": "Redis account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "b44dc49d-6644-48dd-a09a-69404960e60b",
              "leftValue": "={{ $json.value }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1920,
        224
      ],
      "id": "c0f2a463-e876-4278-8f21-16de8093aec2",
      "name": "Key Exists?"
    },
    {
      "parameters": {
        "operation": "set",
        "key": "={{ $('Generate Idempotency Key').first().json.idempotency_key }}",
        "value": "={{ JSON.stringify({ query: $('Generate Idempotency Key').first().json.query, output: $('Generate Idempotency Key').first().json.output.substring(0, 300), created_at: new Date().toISOString() }) }}",
        "expire": true,
        "ttl": 86400
      },
      "type": "n8n-nodes-base.redis",
      "typeVersion": 1,
      "position": [
        2544,
        672
      ],
      "id": "dd9b63c6-20f7-42b8-9222-a312b6b6384f",
      "name": "Store in Redis",
      "credentials": {
        "redis": {
          "id": "Fe910U4Sd5ZTORGy",
          "name": "Redis account"
        }
      }
    }
  ],
  "pinData": {},
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Google Drive - Search Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive - Search Files": {
      "main": [
        [
          {
            "node": "Download File From Google Drive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File From Google Drive": {
      "main": [
        [
          {
            "node": "Google Sheets - Doc Names",
            "type": "main",
            "index": 0
          },
          {
            "node": "Simple Vector Store (Insert)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Embeddings Google Gemini": {
      "ai_embedding": [
        [
          {
            "node": "Simple Vector Store (Insert)",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Default Data Loader": {
      "ai_document": [
        [
          {
            "node": "Simple Vector Store (Insert)",
            "type": "ai_document",
            "index": 0
          }
        ]
      ]
    },
    "Recursive Character Text Splitter": {
      "ai_textSplitter": [
        [
          {
            "node": "Default Data Loader",
            "type": "ai_textSplitter",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Window Buffer Memory": {
      "ai_memory": [
        [
          {
            "node": "AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Vector Store Tool": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Simple Vector Store (Retrieval)": {
      "ai_vectorStore": [
        [
          {
            "node": "Vector Store Tool",
            "type": "ai_vectorStore",
            "index": 0
          }
        ]
      ]
    },
    "Embeddings Google Gemini (retrieval)": {
      "ai_embedding": [
        [
          {
            "node": "Simple Vector Store (Retrieval)",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model (retrieval)": {
      "ai_languageModel": [
        [
          {
            "node": "Vector Store Tool",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Google Sheets - Read Memory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets - Read Memory": {
      "main": [
        [
          {
            "node": "Google Sheets - Read Docs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets - Read Docs": {
      "main": [
        [
          {
            "node": "Input Guard",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Input Guard": {
      "main": [
        [
          {
            "node": "Input Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Input Block Response": {
      "main": [
        []
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Output Guard",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Output Guard": {
      "main": [
        [
          {
            "node": "Output Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Output Guard Response": {
      "main": [
        [
          {
            "node": "Google Sheets - Write Memory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Generate Idempotency Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets - Write Memory": {
      "main": [
        [
          {
            "node": "Store in Redis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Input Check": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Google Sheets - Log Input Blocked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Output Check": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Output Guard Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets - Log Input Blocked": {
      "main": [
        [
          {
            "node": "Input Block Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Idempotency Key": {
      "main": [
        [
          {
            "node": "Check Redis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Redis": {
      "main": [
        [
          {
            "node": "Key Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Key Exists?": {
      "main": [
        [
          {
            "node": "Final Output",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Google Sheets - Write Memory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store in Redis": {
      "main": [
        [
          {
            "node": "Final Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "9c4bf3bb-f8be-4a2a-93e9-c662188574f3",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "b42c209f586d98f2b1e5f1cb48f100a61ba0c4d4668f9cdb7446bdc0d7091a61"
  },
  "id": "QDJ1BIEfRnWuiK6P",
  "tags": []
}