RAG Document Pipeline
How a company safely ingests internal documents into a RAG system so the vector store and LLM never see employee or customer PII
How a company safely ingests internal documents into a RAG system so the vector store and LLM never see employee or customer PII.
Note: This scenario relies on detecting person and company names. Name, company, and location detection requires the Python NER sidecar (
GUARDAI_DETECTOR_URL, withdetector.mode: bothoradvanced); in builtin-only mode those are not detected. Setdetection.required_for: [person, company]in the policy to fail closed if the sidecar is down. See Detector Capabilities.
The Situation
A 2,000-person technology company wants to build an internal knowledge assistant. The knowledge base includes HR policies, employee handbooks, customer contracts, support ticket archives, and internal memos. These documents are full of names, email addresses, phone numbers, employee IDs, customer account numbers, and salary figures.
The engineering team builds a standard RAG pipeline: chunk documents, embed them, store vectors, retrieve relevant chunks at query time, and pass them to an LLM for answer generation. The security review blocks deployment. The vector store (hosted on a managed service) would contain raw PII in the chunk text. The LLM provider would receive PII in retrieved context. Every query could leak sensitive data.
The Solution
OGuardAI is inserted at two points in the RAG pipeline: document ingestion (tokenize before embedding) and query time (transform the question, then rehydrate the answer). The vector store only ever contains tokenized text. The LLM only ever sees tokenized context.
The 4-Step RAG Flow
The vector store and the LLM only ever see tokens; raw values are restored only at the final answer,
for the user. A shared corpus_id keeps token identity consistent across ingest, query, and context.
Step 1: Document Ingestion (Tokenize Before Embedding)
A document from the support archive is being ingested:
From: Sarah Chen <sarah.chen@acmecorp.com>
To: Support Team
Subject: Account #AC-2026-8834 - Billing dispute
Hi team,
Customer James Rodriguez (james.r@outlook.com, phone 415-555-0192)
called about a double charge of $847.50 on invoice INV-44021.
His employee contact at our company is Mike Thompson (ext. 4421).
Please resolve within 24 hours per our SLA.
Sarah Chen
Customer Success ManagerThe ingestion service sends the document to OGuardAI:
curl -X POST http://localhost:3000/v1/transform \
-H "Content-Type: application/json" \
-H "X-API-Key: $GUARDAI_API_KEY" \
-d '{
"input": "From: Sarah Chen <sarah.chen@acmecorp.com>\nTo: Support Team\nSubject: Account #AC-2026-8834 - Billing dispute\n\nHi team,\n\nCustomer James Rodriguez (james.r@outlook.com, phone 415-555-0192) called about a double charge of $847.50 on invoice INV-44021. His employee contact at our company is Mike Thompson (ext. 4421). Please resolve within 24 hours per our SLA.\n\nSarah Chen\nCustomer Success Manager",
"policy": "rag-ingest"
}'Detected entities:
| Original Value | Entity Type | Token |
|---|---|---|
| Sarah Chen | person | {{person:p_001:ad4f97591c16}} |
| sarah.chen@acmecorp.com | {{email:e_001:1bcaef1a4aff}} | |
| AC-2026-8834 | customer_id | {{customer_id:cid_001:d00188a0c40c}} |
| James Rodriguez | person | {{person:p_002:98e1f012a96f}} |
| james.r@outlook.com | {{email:e_002:3bd0ea511c7d}} | |
| 415-555-0192 | phone | {{phone:ph_001:71453119be53}} |
| Mike Thompson | person | {{person:p_003:e19cd15f15fb}} |
The tokenized text stored in the vector database:
From: `{{person:p_001:ad4f97591c16}}` <`{{email:e_001:1bcaef1a4aff}}`>
To: Support Team
Subject: Account #`{{customer_id:cid_001:d00188a0c40c}}` - Billing dispute
Hi team,
Customer `{{person:p_002:98e1f012a96f}}` (`{{email:e_002:3bd0ea511c7d}}`, phone `{{phone:ph_001:71453119be53}}`)
called about a double charge of `{{custom:money:x_001:601595ba1e2c}}` on invoice INV-44021.
His employee contact at our company is `{{person:p_003:e19cd15f15fb}}` (ext. 4421).
Please resolve within 24 hours per our SLA.
`{{person:p_001:ad4f97591c16}}`
Customer Success ManagerThe session state blob is stored alongside the document chunk metadata in the ingestion database (not in the vector store).
Step 2: Query Transformation
An employee asks the knowledge assistant: "What happened with the billing dispute from James Rodriguez?"
The query is transformed before retrieval:
curl -X POST http://localhost:3000/v1/transform \
-H "Content-Type: application/json" \
-H "X-API-Key: $GUARDAI_API_KEY" \
-d '{
"input": "What happened with the billing dispute from James Rodriguez?",
"policy": "rag-query"
}'Result:
What happened with the billing dispute from `{{person:p_001:ad4f97591c16}}`?The vector search uses the tokenized query. Because the ingested chunks also contain {{person:...}} tokens, semantic similarity still works: the embedding model captures the structural pattern around "billing dispute" and the person token.
Aligning tokens across documents and sessions with
corpus_id. The simplified/v1/transformcalls above assign token ids per request, so the query's "James Rodriguez" gets{{person:p_001:ad4f97591c16}}here while ingestion assigned{{person:p_002:98e1f012a96f}}. Different ids do not hurt vector similarity, but they do not let you join a query entity to a specific ingested entity. To make the same raw value tokenize to the same id across every document and query, the RAG endpoints (/v1/rag/ingest,/v1/rag/query,/v1/rag/context) accept an optionalcorpus_id. Pass the samecorpus_idon ingest and query and "James Rodriguez" resolves to one deterministic corpus-scoped token everywhere, so the query token lines up with the ingested document token.corpus_idis corpus-scoped, not a global fingerprint; leaving it unset keeps the random per-token scheme, and/v1/rag/contextrejects a mismatchedcorpus_idrather than restoring across identity domains.
Step 3: Context Assembly
The retrieval system finds the matching chunk (from Step 1). The tokenized chunk is passed directly to the LLM as context: it is already safe. No additional transformation is needed for the retrieved context because it was tokenized at ingestion time.
In a production pipeline that retrieves across many documents through /v1/rag/ingest and /v1/rag/context, ingest returns a per-chunk chunk_session_state (a sealed blob holding only that chunk's tokens). Store it next to the chunk in the vector database. At context time, pass the retrieved chunks plus their chunk_session_state values as the document_sessions array to /v1/rag/context. The runtime merges each admitted chunk's document tokens into the session so a document-only entity resolves in the answer, fail closed: a chunk filtered by access level is never merged, and a token id that two documents disagree on is rejected rather than restored to a wrong value.
Step 4: Answer Rehydration
The LLM generates an answer using the tokenized context:
Based on the support ticket, `{{person:p_002:98e1f012a96f}}` reported a double charge
of $847.50 on invoice INV-44021 for account `{{customer_id:cid_001:d00188a0c40c}}`.
The ticket was assigned to `{{person:p_003:e19cd15f15fb}}` for resolution within
24 hours per SLA. The original report was filed by `{{person:p_001:ad4f97591c16}}`
from the Customer Success team.The answer is rehydrated for the requesting employee:
curl -X POST http://localhost:3000/v1/rehydrate \
-H "Content-Type: application/json" \
-H "X-API-Key: $GUARDAI_API_KEY" \
-d '{
"output": "<LLM answer with tokens>",
"session_state": "<encrypted-blob-from-ingest>",
"output_channel": "internal_summary"
}'The employee sees the fully restored answer:
Based on the support ticket, James Rodriguez reported a double charge
of $847.50 on invoice INV-44021 for account AC-2026-8834. The ticket
was assigned to Mike Thompson for resolution within 24 hours per SLA.
The original report was filed by Sarah Chen from the Customer Success team.Policy Configuration
name: rag-ingest
version: "1.0"
rules:
- entity_type: person
protection_level: 2
action: tokenize
restore_mode: full
- entity_type: email
protection_level: 2
action: tokenize
restore_mode: full
- entity_type: phone
protection_level: 2
action: tokenize
restore_mode: masked
- entity_type: customer_id
protection_level: 2
action: tokenize
restore_mode: full
- entity_type: ssn
protection_level: 1
action: redact
defaults:
protection_level: 2
action: tokenize
restore_mode: full
metadata_policy:
expose_gender: true
expose_formality: true
expose_language: true
expose_role: true
channel_rules:
internal_summary:
person: full
email: full
phone: masked
customer_id: full
customer_email:
person: partial
email: none
phone: none
customer_id: maskedSave the policy under policies/rag-ingest/policy.yaml (and the matching rag-query policy referenced in Step 2 under policies/rag-query/), then start the server with the policy directory so both policy names resolve at request time.
What OGuardAI Made Possible
Safe vector storage. The managed vector store service contains zero PII. If the vector store is breached, attackers find only semantic tokens that cannot be reversed without the encrypted session blobs stored separately in the company's infrastructure.
End-to-end protection. PII is tokenized once at ingestion and stays tokenized through retrieval, context assembly, and LLM processing. Real values are restored only at the final step, only for authorized users, only according to policy.
Semantic search still works. Tokenized text preserves document structure and surrounding context. The embedding model captures the meaning around tokens ("billing dispute," "double charge," "SLA") so retrieval quality is maintained.
Blocked sensitive categories. SSN values in HR documents are blocked entirely at ingestion: they never enter the vector store, not even as tokens. The policy enforces this for the entire document corpus.