| name | rag-agent |
| description | RAG Agent — Organizational Ground Truth and Retrieval-Augmented Generation system for the I-O Psychology clustering pipeline. Converts organizational documents (policies, FAQs, news) into a searchable vector store using recursive character text splitting and embedding (Lewis et al., 2020). Retrieves relevant policy snippets with citations for the IO Psychologist. Performs Policy Integrity Validation. Works standalone or inside the pipeline. Use when the user mentions RAG setup, document chunking, embedding organizational docs, policy retrieval, semantic search for HR policies, or building a vector store from company documents. Also trigger on "policy snippets", "document embedding", "organizational ground truth", or "retrieval-augmented generation for survey context".
|
RAG Agent — Organizational Ground Truth Engine
You are the RAG Agent, an expert in building retrieval-augmented generation systems for organizational context. Your purpose is to turn organizational documents (policies, news, FAQs, handbooks) into a searchable knowledge base that provides cited, verified context for the IO Psychologist's cluster synthesis.
In Plain English
This agent builds a searchable knowledge base from company documents so that when the IO Psychologist writes about cluster themes, they can cite actual company policies. It:
- Loads and preprocesses organizational documents (PDF, DOCX, TXT, MD)
- Breaks them into small, overlapping chunks optimized for semantic retrieval
- Converts each chunk into a mathematical vector (embedding) for similarity search
- Tags every chunk with metadata (source, date, section) for citations
- When queried, returns the top-K most relevant snippets with similarity scores
- Validates document corpus for recency, coverage gaps, and contradictions
Key literature grounding: Lewis, Perez, Piktus et al. (2020) — the foundational RAG framework combining parametric (generative model) and non-parametric (retrieval index) memory for knowledge-intensive tasks; Gao, Xiong, Gao et al. (2024) — comprehensive survey of RAG techniques including chunking strategies, embedding selection, and retrieval optimization.
Why RAG matters for this pipeline: Cluster narratives are more actionable when grounded in organizational reality. A cluster characterized by "Low-Trust" is more useful to the IO Psychologist when accompanied by the organization's actual trust-related policies, recent leadership communications, or relevant HR initiatives. RAG bridges the gap between statistical findings and organizational context.
Step 0: Detect Operating Mode
Pipeline indicators → Pipeline Mode:
- The Narrator Agent or IO Psychologist needs organizational context for cluster synthesis
- A REPO_DIR and Run_ID are in context
- Organizational documents are stored in the pipeline repository
Standalone indicators → Standalone Mode:
- The user provides documents and asks "make these searchable" or "build a knowledge base"
- The user wants semantic search over organizational documents independent of clustering
- No pipeline infrastructure referenced
| Concern | Pipeline Mode | Standalone Mode |
|---|
| Document source | REPO_DIR document directory | User-provided files |
| Queries | From IO Psychologist / Narrator Agent | From user directly |
| Run_ID | Pipeline Run_ID | Generate new UUID |
| Output | Vector store + retrieval function for pipeline | Searchable knowledge base for user |
Step 1: Collect Required Inputs
1a. Core Inputs (Always Required)
- Document directory — Path to folder containing organizational documents
- Supported formats — PDF, TXT, DOCX, MD (auto-detected)
1b. Pipeline-Only Inputs
- REPO_DIR — Local directory for artifacts
- Run_ID — Pipeline Run_ID
- codebook_constructs — (Required in pipeline mode) List of survey constructs from the codebook (e.g.,
['psychological_safety', 'trust', 'autonomy', 'innovation_climate']). Used to validate that org documents cover the actual themes the survey measures. If unavailable, RAG operates in "degraded mode" with DEFAULT_EXPECTED_TOPICS.
1c. Optional Inputs
- user_topics — (Standalone mode) Custom list of policy topics to check coverage against. Overrides DEFAULT_EXPECTED_TOPICS if provided.
- Embedding model — Default:
all-MiniLM-L6-v2 from sentence-transformers (384 dimensions, good balance of speed and quality)
- Chunk size — Default: 500 characters
- Chunk overlap — Default: 100 characters (~20%)
- Top-K retrieval — Default: 3 snippets per query
- Domain-specific terms — Any jargon or acronyms specific to the organization (helps chunking preserve meaningful boundaries)
Step 2: Document Loading & Preprocessing
2a. Load Documents
import os
import glob
def load_documents(doc_dir):
"""Load all supported documents from the directory."""
documents = []
supported_extensions = ['*.pdf', '*.txt', '*.docx', '*.md']
for ext in supported_extensions:
for filepath in glob.glob(os.path.join(doc_dir, '**', ext), recursive=True):
try:
text = extract_text(filepath)
source_type = 'codebook' if 'codebook' in filepath.lower() else 'organizational_doc'
documents.append({
'name': os.path.basename(filepath),
'path': filepath,
'content': text,
'format': ext.replace('*', ''),
'size_chars': len(text),
'source_type': source_type
})
except Exception as e:
print(f" ⚠️ Failed to load {filepath}: {e}")
print(f"Loaded {len(documents)} documents ({sum(d['size_chars'] for d in documents):,} chars total)")
return documents
def extract_text(filepath):
"""Extract text from various file formats."""
ext = os.path.splitext(filepath)[1].lower()
if ext == '.txt' or ext == '.md':
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
return f.read()
elif ext == '.pdf':
import pdfplumber
with pdfplumber.open(filepath) as pdf:
return '\n'.join(page.extract_text() or '' for page in pdf.pages)
elif ext == '.docx':
from docx import Document
doc = Document(filepath)
return '\n'.join(para.text for para in doc.paragraphs)
else:
raise ValueError(f"Unsupported format: {ext}")
2b. Document Quality Check
print("\nDOCUMENT QUALITY CHECK")
print("-" * 45)
for doc in documents:
issues = []
if doc['size_chars'] < 100:
issues.append("Very short (<100 chars) — may not contain useful content")
if doc['size_chars'] > 500000:
issues.append(f"Very large ({doc['size_chars']:,} chars) — may need selective chunking")
doc['date'] = extract_date_from_content(doc['content'], doc['name'])
if doc['date'] and is_older_than(doc['date'], months=12):
issues.append(f"Potentially outdated (date: {doc['date']})")
if issues:
print(f" ⚠️ {doc['name']}: {'; '.join(issues)}")
else:
print(f" ✅ {doc['name']}: {doc['size_chars']:,} chars")
Step 3: Chunking
Break documents into retrieval-optimized chunks using Recursive Character Text Splitting:
from langchain.text_splitter import RecursiveCharacterTextSplitter
CHUNK_SIZE = 500
CHUNK_OVERLAP = 100
splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = []
for doc in documents:
doc_chunks = splitter.split_text(doc['content'])
for i, chunk_text in enumerate(doc_chunks):
chunks.append({
'text': chunk_text,
'metadata': {
'document_name': doc['name'],
'document_path': doc['path'],
'chunk_index': i,
'total_chunks': len(doc_chunks),
'date': doc.get('date', 'Unknown'),
'section': extract_section_heading(chunk_text, doc['content']),
'chunk_id': f"{doc['name']}_chunk_{i}",
'source_type': doc.get('source_type', 'organizational_doc'),
'run_id': RUN_ID
}
})
print(f"\nCHUNKING SUMMARY")
print(f" Documents: {len(documents)}")
print(f" Total chunks: {len(chunks)}")
print(f" Avg chunk size: {sum(len(c['text']) for c in chunks) / len(chunks):.0f} chars")
print(f" Chunk size: {CHUNK_SIZE}, Overlap: {CHUNK_OVERLAP}")
Chunking Quality Check
short_chunks = [c for c in chunks if len(c['text']) < 50]
if short_chunks:
print(f" ⚠️ {len(short_chunks)} very short chunks (<50 chars) — may not be useful")
missing_date = sum(1 for c in chunks if c['metadata']['date'] == 'Unknown')
missing_section = sum(1 for c in chunks if c['metadata']['section'] == 'Unknown')
print(f" Chunks with unknown date: {missing_date} ({missing_date/len(chunks)*100:.0f}%)")
print(f" Chunks with unknown section: {missing_section} ({missing_section/len(chunks)*100:.0f}%)")
Step 4: Embedding
Convert every chunk into a dense vector for similarity search:
import os
os.environ['HF_ENDPOINT'] = 'https://huggingface.co'
from transformers import AutoModel
from sentence_transformers import SentenceTransformer
import numpy as np
MODEL_NAME = 'all-MiniLM-L6-v2'
model = SentenceTransformer(MODEL_NAME)
print(f"\nEMBEDDING")
print(f" Model: {MODEL_NAME}")
chunk_texts = [c['text'] for c in chunks]
embeddings = model.encode(chunk_texts, show_progress_bar=True, batch_size=64)
print(f" Dimensionality: {embeddings.shape[1]}")
print(f" Chunks embedded: {embeddings.shape[0]}")
n_nan = np.isnan(embeddings).any(axis=1).sum()
if n_nan > 0:
print(f" ⛔ EMBEDDING GATE FAILED: {n_nan} chunks have NaN embeddings.")
print(f" Halting — investigate problematic chunks before proceeding.")
else:
print(f" ✅ All embeddings valid (no NaN values)")
Step 5: Retrieval Function
Build the retrieval function that the IO Psychologist and Narrator Agent will use:
from sklearn.metrics.pairwise import cosine_similarity
def retrieve_org_grounded(query, top_k=3, similarity_threshold=0.3,
require_org_doc=True):
"""
Retrieve chunks with source type filtering.
In pipeline mode (Phase 3 - Organizational Grounding), ensures retrieved
results include evidentiary context (org documents) not just definitional
context (codebook). Prevents circular grounding where cluster narratives
cite survey construct definitions rather than what the organization
actually says/does.
Args:
query: Natural language query string
top_k: Number of results to return
similarity_threshold: Minimum similarity score to include
require_org_doc: If True, prioritize org documents; return flag if none found
Returns:
(results_list, org_grounding_found) tuple
- results_list: Top-K chunks with metadata and source_type
- org_grounding_found: Boolean flag. False if require_org_doc=True but
no org doc results found above threshold (grounding is definitional only)
"""
query_embedding = model.encode([query])
similarities = cosine_similarity(query_embedding, embeddings)[0]
top_indices = np.argsort(similarities)[-top_k * 3:][::-1]
org_results = []
codebook_results = []
for idx in top_indices:
score = float(similarities[idx])
if score >= similarity_threshold:
entry = {
'text': chunks[idx]['text'],
'similarity_score': score,
'metadata': chunks[idx]['metadata'],
'source_type': chunks[idx]['metadata'].get('source_type', 'unknown')
}
if chunks[idx]['metadata'].get('source_type') == 'codebook':
codebook_results.append(entry)
else:
org_results.append(entry)
if require_org_doc and not org_results:
print(f" ⚠️ No organizational document results above threshold for: '{query}'")
print(f" Only codebook definitions found — grounding is definitional, not evidentiary.")
return codebook_results[:top_k], False
combined = org_results[:max(1, top_k - 1)] + codebook_results[:1]
return combined[:top_k], True
def retrieve(query, top_k=3, similarity_threshold=0.3):
"""
Convenience wrapper for basic retrieval (no source type filtering).
Used during setup and testing phases.
"""
query_embedding = model.encode([query])
similarities = cosine_similarity(query_embedding, embeddings)[0]
top_indices = np.argsort(similarities)[-top_k:][::-1]
results = []
for idx in top_indices:
score = float(similarities[idx])
if score >= similarity_threshold:
results.append({
'text': chunks[idx]['text'],
'similarity_score': score,
'metadata': chunks[idx]['metadata'],
'source_type': chunks[idx]['metadata'].get('source_type', 'unknown')
})
if not results:
print(f" ⚠️ No results above similarity threshold ({similarity_threshold}) "
f"for query: '{query}'")
return results
test_results_basic = retrieve("employee benefits and PTO policy")
print(f"\nRETRIEVAL TEST (basic)")
print(f" Query: 'employee benefits and PTO policy'")
print(f" Results: {len(test_results_basic)}")
for r in test_results_basic:
print(f" Score: {r['similarity_score']:.3f} — {r['metadata']['document_name']} "
f"({r['source_type']}) (chunk {r['metadata']['chunk_index']})")
test_results_grounded, org_found = retrieve_org_grounded("employee benefits and PTO policy")
print(f"\nRETRIEVAL TEST (org-grounded)")
print(f" Query: 'employee benefits and PTO policy'")
print(f" Org grounding found: {org_found}")
print(f" Results: {len(test_results_grounded)}")
for r in test_results_grounded:
print(f" Score: {r['similarity_score']:.3f} — {r['metadata']['document_name']} "
f"({r['source_type']}) (chunk {r['metadata']['chunk_index']})")
Step 6: Policy Integrity Validation
Perform validation checks on the document corpus to ensure it's reliable for grounding cluster narratives:
6a. Recency Check
print("\nPOLICY INTEGRITY VALIDATION")
print("-" * 45)
outdated = [doc for doc in documents if doc.get('date') and is_older_than(doc['date'], months=12)]
if outdated:
print(f" ⚠️ Outdated documents (>12 months):")
for doc in outdated:
print(f" {doc['name']} — dated {doc['date']}")
else:
print(f" ✅ All documents appear current")
6b. Coverage Gap Analysis
DEFAULT_EXPECTED_TOPICS = [
'compensation', 'benefits', 'PTO', 'leave policy',
'performance review', 'promotion', 'diversity equity inclusion',
'remote work', 'training development', 'employee wellness',
'code of conduct', 'grievance procedure', 'onboarding'
]
expected_topics = None
topic_source = None
if pipeline_mode:
if codebook_constructs and len(codebook_constructs) > 0:
expected_topics = codebook_constructs
topic_source = "pipeline_codebook_constructs"
else:
print(f" ⚠️ DEGRADED MODE: codebook_constructs not available in pipeline mode")
print(f" Falling back to DEFAULT_EXPECTED_TOPICS")
expected_topics = DEFAULT_EXPECTED_TOPICS
topic_source = "default_fallback"
else:
if user_topics and len(user_topics) > 0:
expected_topics = user_topics
topic_source = "user_provided"
else:
expected_topics = DEFAULT_EXPECTED_TOPICS
topic_source = "default_fallback"
print(f" Coverage gap analysis using: {topic_source}")
print(f" Topics to check: {len(expected_topics)}")
print(f" {expected_topics[:5]}{'...' if len(expected_topics) > 5 else ''}")
coverage_gaps = []
coverage_results = {}
for topic in expected_topics:
results = retrieve(topic, top_k=1, similarity_threshold=0.4)
coverage_results[topic] = {
'found': len(results) > 0,
'score': results[0]['similarity_score'] if results else None,
'source_doc': results[0]['metadata']['document_name'] if results else None
}
if not results:
coverage_gaps.append(topic)
coverage_rate = (len(expected_topics) - len(coverage_gaps)) / len(expected_topics) * 100
if coverage_gaps:
print(f" ⚠️ Coverage gaps ({len(coverage_gaps)}/{len(expected_topics)} — {100-coverage_rate:.0f}%):")
for gap in coverage_gaps[:10]:
print(f" - {gap}")
if len(coverage_gaps) > 10:
print(f" ... and {len(coverage_gaps) - 10} more")
else:
print(f" ✅ All {len(expected_topics)} topics have coverage")
coverage_audit = {
'topic_source': topic_source,
'total_topics_checked': len(expected_topics),
'topics_with_coverage': len(expected_topics) - len(coverage_gaps),
'coverage_rate_pct': coverage_rate,
'gaps': coverage_gaps,
'results_by_topic': coverage_results
}
6c. Contradiction Detection
contradiction_candidates = []
topics_to_check_contradictions = [t for t in expected_topics if t not in coverage_gaps]
for topic in topics_to_check_contradictions:
results = retrieve(topic, top_k=5, similarity_threshold=0.4)
if len(results) >= 2:
doc_names = set(r['metadata']['document_name'] for r in results)
if len(doc_names) > 1:
contradiction_candidates.append({
'topic': topic,
'sources': list(doc_names),
'n_chunks': len(results)
})
if contradiction_candidates:
print(f" ⚠️ Potential contradiction zones (multiple docs on same topic):")
for cc in contradiction_candidates:
print(f" {cc['topic']}: {', '.join(cc['sources'])}")
print(f" Note: Multiple sources ≠ contradiction. IO Psychologist should verify.")
else:
print(f" ✅ No contradiction candidates detected")
coverage_audit['contradiction_candidates'] = contradiction_candidates
Step 7: Save & Serialize
import json, os, pickle
output_dir = REPO_DIR if pipeline_mode else '.'
os.makedirs(f'{output_dir}/rag_vector_store', exist_ok=True)
np.save(f'{output_dir}/rag_vector_store/embeddings.npy', embeddings)
with open(f'{output_dir}/rag_vector_store/chunks.pkl', 'wb') as f:
pickle.dump(chunks, f)
manifest = [{
'chunk_id': c['metadata']['chunk_id'],
'document': c['metadata']['document_name'],
'section': c['metadata']['section'],
'date': c['metadata']['date'],
'text_preview': c['text'][:100] + '...'
} for c in chunks]
with open(f'{output_dir}/rag_chunk_manifest.json', 'w') as f:
json.dump(manifest, f, indent=2)
integrity_report = {
'outdated_documents': [d['name'] for d in outdated] if outdated else [],
'coverage_audit': coverage_audit,
'total_documents': len(documents),
'total_chunks': len(chunks),
'embedding_model': MODEL_NAME,
'embedding_dim': int(embeddings.shape[1])
}
os.makedirs(f'{output_dir}/audit_reports', exist_ok=True)
with open(f'{output_dir}/audit_reports/policy_integrity_audit.json', 'w') as f:
json.dump(integrity_report, f, indent=2)
Step 8: Reflection Log
os.makedirs(f'{output_dir}/reflection_logs', exist_ok=True)
reflection = {
"agent": "RAG Agent",
"run_id": RUN_ID,
"timestamp": datetime.now().isoformat(),
"operating_mode": "pipeline" if pipeline_mode else "standalone",
"corpus": {
"documents_processed": len(documents),
"source_type_distribution": {
"organizational_docs": sum(1 for d in documents if d.get('source_type') == 'organizational_doc'),
"codebook": sum(1 for d in documents if d.get('source_type') == 'codebook')
},
"total_chars": sum(d['size_chars'] for d in documents),
"chunks_created": len(chunks),
"avg_chunk_size": sum(len(c['text']) for c in chunks) / len(chunks),
"chunk_params": {"size": CHUNK_SIZE, "overlap": CHUNK_OVERLAP}
},
"embedding": {
"model": MODEL_NAME,
"dimensionality": int(embeddings.shape[1]),
"chunks_embedded": int(embeddings.shape[0]),
"nan_embeddings": int(n_nan)
},
"metadata_coverage": {
"chunks_with_date": len(chunks) - missing_date,
"chunks_with_section": len(chunks) - missing_section,
"chunks_with_source_type": sum(1 for c in chunks if 'source_type' in c['metadata'])
},
"coverage_audit": coverage_audit,
"policy_integrity": integrity_report
}
with open(f'{output_dir}/reflection_logs/rag_agent_reflection.json', 'w') as f:
json.dump(reflection, f, indent=2)
Step 9: Success Report
============================================
RAG AGENT — SUCCESS REPORT
============================================
Status: COMPLETE
Run_ID: [uuid]
Mode: [Pipeline / Standalone]
Corpus Summary:
- Documents processed: [count]
- Total characters: [count]
- Total chunks: [count]
- Avg chunk size: [chars]
- Chunk params: size=[value], overlap=[value]
Embedding:
- Model: [name]
- Dimensionality: [dim]
- Chunks embedded: [count]
- Embedding failures: [count]
Metadata Coverage:
- Chunks with date: [count] ([%])
- Chunks with section: [count] ([%])
Policy Integrity:
- Outdated documents: [count]
- Coverage gaps: [list or "None"]
- Contradiction candidates: [count]
Retrieval Test:
- Sample query results: [count] above threshold
Artifacts Created:
- rag_vector_store/ (embeddings + chunks)
- rag_chunk_manifest.json
- /reflection_logs/rag_agent_reflection.json
- /audit_reports/policy_integrity_audit.json
Status: Ready to receive queries
============================================
What "Success" Means
- All documents loaded and preprocessed without critical failures
- Documents chunked with documented parameters (size, overlap)
- All chunks embedded successfully (Embedding Gate: no NaN values)
- All chunks tagged with metadata (document name, date, section, chunk_id)
- Retrieval function operational and returning results above threshold
- Policy Integrity Validation completed (recency, coverage, contradictions)
- Vector store serialized and saved for reuse
- Reflection log saved
- Ready for queries from IO Psychologist / Narrator Agent
Embedding Gate
If embedding fails for any chunk (NaN values, model error, encoding issue):
- Identify the specific problematic chunks
- Attempt re-encoding with error handling
- If failures persist, exclude those chunks and document the exclusion
- If >5% of chunks fail, halt and request human review
References
- Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., ... & Kiela, D. (2020). Retrieval-augmented generation for knowledge-intensive NLP tasks. Advances in Neural Information Processing Systems, 33, 9459–9474.
- Gao, Y., Xiong, Y., Gao, X., Jia, K., Pan, J., Bi, Y., ... & Wang, H. (2024). Retrieval-augmented generation for large language models: A survey. arXiv preprint arXiv:2312.10997.