SteinBot CSV scandal
2025-11-176 turns60,292 charsgpt-5-1
Summary
The user requested a dry technical manual for using SteinBot to analyze a CSV file related to the Epstein files.
Messages
rewrite the following but as a controversial piece in which you load a .csv file and find out shocking things: ---
title: "Building SteinBot: A Comprehensive AI Research Assistant for Financial Document Analysis"
description: "Learn how I developed SteinBot, an advanced AI research assistant that performs graph-based RAG on financial documents from CSV files. Step-by-step guide covering FastAPI backend, Next.js frontend, Neo4j integration, and local LLM inference."
keywords: "AI research assistant, Graph RAG, FastAPI backend, Next.js frontend, Neo4j graph database, Pinecone vector search, Ollama LLM, financial document analysis, CSV data processing"
author: "SteinBot Developer"
date: 11-17-2025
thumbnail: "/images/steinbot-screenshot.png"
tags: ["AI", "RAG", "Machine Learning", "Python", "JavaScript", "Neo4j", "Pinecone", "Ollama", "Financial Technology"]
categories: ["Software Development", "AI/ML", "Full-Stack Development"]
canonicalUrl: "https://github.com/kliewerdaniel/steinbot"
wordCount: "2500"
estimatedReadTime: "12 min"
---
# Building SteinBot: A Comprehensive AI Research Assistant for Financial Document Analysis

In today's data-driven world, researchers and analysts need powerful tools to extract insights from complex document collections. SteinBot represents my journey in building an advanced AI research assistant specifically designed for analyzing financial documents stored in CSV format. This comprehensive guide walks through the step-by-step development process, from initial concept to deployment, highlighting the key architectural decisions and technical implementations.
## Introduction
SteinBot is an intelligent research assistant that specializes in processing and analyzing document collections, particularly financial files like the EPS (Earnings Per Share) dataset. Built with modern AI technologies, it enables conversational research through:
- **Graph-based Retrieval Augmented Generation (RAG)** using Neo4j
- **Vector embeddings** with Pinecone for semantic search
- **Local LLM inference** with Ollama
- **Intuitive chat interface** built with Next.js
- **Advanced voice features** including TTS synthesis
The application processes CSV files containing thousands of financial documents, enabling researchers to ask complex questions and receive contextually relevant answers with proper source citations.
## Exploring the Data Source: Understanding the EPS_FILES_20K_NOV2026.csv
Before diving into the technical implementation, let's explore the data that drives SteinBot. The primary dataset is `EPS_FILES_20K_NOV2026.csv`, a substantial collection of financial documents.
### CSV File Structure
The dataset contains two main columns:
- **filename**: Unique identifier for each document (e.g., `IMAGES-005-HOUSE_OVERSIGHT_020367.txt`)
- **text**: Full document content, ranging from legislative texts to financial reports
### Initial Data Exploration
First, I examined the CSV structure using command-line tools:
```bash
# Get first 20 rows to understand structure
head -20 EPS_FILES_20K_NOV2026.csv
# Count total rows and size
wc -l EPS_FILES_20K_NOV2026.csv
# Output: 20000+ rows
# Check file size
ls -lh EPS_FILES_20K_NOV2026.csv
# Output: ~150MB dataset
```
### Content Analysis
The documents span diverse financial topics:
- Government oversight reports
- Corporate earnings statements
- Regulatory filings and compliance documents
- Economic analysis and market research
Sample content from the dataset reveals the depth and variety of financial documentation that researchers might need to analyze.
## Step 1: Designing the Core Architecture
### Technology Stack Selection
**Backend (FastAPI + Python)**:
- FastAPI for high-performance REST APIs
- Async support for concurrent operations
- Automatic API documentation with OpenAPI/Swagger
**Frontend (Next.js + React)**:
- Server-side rendering for SEO
- TypeScript for type safety
- Modern reactive components with hooks
**Databases & AI**:
- Neo4j: Graph database for relationship modeling
- Pinecone: Vector database for semantic embeddings
- Ollama: Local LLM inference for privacy and control
- Redis: Caching layer for performance
### Architectural Layers
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Next.js UI │ │ FastAPI API │ │ Data Sources │
│ │◄──►│ │◄──►│ │
│ • Chat Interface│ │ • Research RAG │ │ • CSV Datasets │
│ • Voice Features│ │ • Chat Endpoint │ │ • Neo4j Graph │
│ • Prompt Mgmt │ │ • Task Manager │ │ • Pinecone Vec │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## Step 2: Building the Backend with FastAPI
### Core Components Setup
First, I created the main FastAPI application in `main.py`:
```python
# main.py
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from scripts.eps_reasoning_agent import EPSReasoningAgent
from scripts.eps_retriever import EPSRetriever
app = FastAPI(title="Research Assistant API", version="1.0.0")
# CORS configuration for frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Global component instances
reasoning_agent = None
retriever = None
@app.on_event("startup")
async def startup_event():
global reasoning_agent, retriever
reasoning_agent = EPSReasoningAgent()
retriever = EPSRetriever()
print("✓ All components initialized")
```
### Implementing the Chat Endpoint
The primary functionality revolves around the `/api/chat` endpoint:
```python
@app.post("/api/chat")
async def chat(request: QueryRequest) -> QueryResponse:
if not reasoning_agent:
raise HTTPException(status_code=503, detail="Reasoning agent not initialized")
try:
result = reasoning_agent.generate_response(
request.query,
request.chat_history
)
# Format sources with metadata
sources = []
for doc in result['context_used']:
sources.append({
'title': doc.get('filename', 'Unknown Document'),
'authors': doc.get('document_type', 'Unknown'),
'year': doc.get('filename', 'Unknown')[:10],
'relevance_score': f"{doc.get('relevance_score', 0.0):.3f}"
})
return QueryResponse(
response=result['response'],
context_used=result['context_used'],
sources=sources,
session_id=request.session_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Chat processing failed: {str(e)}")
```
### Data Ingestion Pipeline
I developed specialized scripts for processing the CSV data:
#### EPSGraphBuilder (`scripts/ingest_eps_data.py`)
```python
class EPSGraphBuilder:
def __init__(self):
self.driver = GraphDatabase.driver(
os.getenv("NEO4J_URI", "bolt://localhost:7687"),
auth=(NEO4J_USER, NEO4J_PASSWORD)
)
self.pinecone_client = PineconeClient(api_key=PINECONE_API_KEY)
def ingest_eps_csv(self, csv_path: Path):
"""Process and index CSV documents"""
df = pd.read_csv(csv_path)
for _, row in df.iterrows():
doc_content = row['text']
doc_id = row['filename']
# Create embeddings
embedding = self.generate_embedding(doc_content)
# Store in Neo4j
self.store_in_neo4j(doc_id, doc_content, embedding)
# Index in Pinecone
self.store_in_pinecone(doc_id, embedding)
self.create_similarity_relationships()
```
## Step 3: Implementing Graph-RAG Retrieval
### Hybrid Retrieval Strategy
SteinBot uses a sophisticated hybrid approach combining graph traversal and vector similarity:
#### EPSRetriever (`scripts/eps_retriever.py`)
```python
class EPSRetriever:
def retrieve_context(self, query: str, top_k: int = 5):
"""Perform hybrid retrieval"""
# Generate query embedding
query_embedding = self.generate_embedding(query)
# Vector search in Pinecone
vector_results = self.pinecone_client.search(
query_embedding,
top_k=top_k,
include_metadata=True
)
# Graph traversal from seed documents
graph_results = self.graph_traversal(vector_results)
# Combine and rank results
combined_results = self.rerank_results(vector_results, graph_results)
return combined_results
```
### Graph Schema Design
I designed a graph schema that captures relationships between documents:
```
(EPSDocument)
├── has_keywords → (Keyword)
├── mentions_company → (Company)
├── cites_reference → (Citation)
└── similar_to → (EPSDocument) {score: float}
```
## Step 4: Developing the Reasoning Agent
### EPSReasoningAgent Architecture
The reasoning agent orchestrates the entire RAG pipeline:
```python
# scripts/eps_reasoning_agent.py
class EPSReasoningAgent:
def __init__(self):
self.retriever = EPSRetriever()
self.llm_client = OllamaClient()
self.prompt_templates = self.load_prompts()
def generate_response(self, query: str, chat_history: List[Dict]):
"""Flexible reasoning pipeline"""
# Multi-stage retrieval
context_chunks = self.retriever.retrieve_context(query)
# Reasoning with context
reasoning_prompt = self.build_reasoning_prompt(query, context_chunks, chat_history)
# Generate response
response = self.llm_client.generate(reasoning_prompt)
# Post-processing and validation
processed_response = self.post_process_response(response, context_chunks)
return {
'response': processed_response,
'context_used': context_chunks,
'quality_grade': self.evaluate_response_quality(processed_response),
'retrieval_method': 'hybrid'
}
```
## Step 5: Building the Next.js Frontend
### Chat Interface Design
The frontend provides an intuitive chat experience:
```tsx
// frontend/src/components/Chat.tsx
export default function Chat() {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false)
const sendMessage = async () => {
const response = await fetch('http://localhost:8000/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: input,
chat_history: messages
})
})
const data = await response.json()
setMessages([...messages, { role: 'assistant', content: data.response }])
}
// UI components for messages, sources, voice controls...
}
```
### Advanced Features
#### Text-to-Speech Integration
```tsx
const speakMessage = async (content: string) => {
const response = await fetch('/api/tts', {
method: 'POST',
body: JSON.stringify({ text: content })
})
const audio = await response.blob()
const audioUrl = URL.createObjectURL(audio)
new Audio(audioUrl).play()
}
```
#### Session Management
```tsx
const [currentSessionId, setCurrentSessionId] = useState<string>()
const createNewSession = () => {
const newId = Date.now().toString()
setCurrentSessionId(newId)
setMessages([])
}
```
## Step 6: Adding Voice and Multimedia Features
### Voice Processor Utility
I implemented comprehensive voice processing capabilities:
```typescript
// frontend/src/utils/VoiceProcessor.ts
class VoiceProcessor {
async enhanceSpeech(utterance: SpeechSynthesisUtterance, text: string) {
// Adjust pitch, rate, and volume based on content analysis
const sentiment = this.analyzeSentiment(text)
utterance.pitch = sentiment.positive ? 1.1 : 0.9
utterance.rate = sentiment.complex ? 0.8 : 1.0
}
private analyzeSentiment(text: string) {
// Simple sentiment analysis for voice enhancement
const positiveWords = ['good', 'excellent', 'positive', 'success']
const negativeWords = ['bad', 'poor', 'negative', 'failure']
const complexIndicators = ['however', 'although', 'furthermore']
return {
positive: positiveWords.some(word => text.includes(word)),
negative: negativeWords.some(word => text.includes(word)),
complex: complexIndicators.some(word => text.includes(word))
}
}
}
```
## Step 7: Database Schema and Indexing
### Neo4j Setup
I designed the graph database schema specifically for financial document analysis:
```cypher
CREATE CONSTRAINT ON (d:EPSDocument) ASSERT d.id IS UNIQUE;
CREATE INDEX ON :EPSDocument(filename);
CREATE INDEX ON :EPSDocument(document_date);
CREATE INDEX ON :Keyword(text);
CREATE INDEX ON :Company(name);
```
### Pinecone Configuration
For vector similarity search:
```python
pinecone.init(api_key=PINECONE_API_KEY, environment='gcp-starter')
index = pinecone.Index('steinbot-eps')
index.create_index(dimension=768, metric='cosine')
```
## Step 8: Deployment and Containerization
### Docker Compose Configuration
I orchestrated the entire stack with Docker Compose:
```yaml
# docker-compose.yml
version: '3.8'
services:
neo4j:
image: neo4j:5.15
environment:
- NEO4J_AUTH=neo4j/password
ports: ["7687:7687"]
redis:
image: redis:7.2
ports: ["6379:6379"]
api:
build: ./
environment:
- NEO4J_URI=bolt://neo4j:7687
- OLLAMA_BASE_URL=http://host.docker.internal:11434
ports: ["8000:8000"]
depends_on: [neo4j, redis]
```
### Automated Setup Scripts
```bash
# setup.sh
#!/bin/bash
docker-compose up -d neo4j redis
pip install -r requirements.txt
python create_indexes.py
python create_thread_relationships.py
python scripts/ingest_eps_data.py --file EPS_FILES_20K_NOV2026.csv
```
## Step 9: Evaluation and Testing Framework
### Research Performance Metrics
I implemented comprehensive benchmarking:
```python
# evaluation/run_evaluation.py
class Evaluator:
def run_evaluation(self, queries, output_path: Path):
results = []
for query in queries:
response = self.generate_response(query['query'])
metrics = {
'accuracy': self.evaluate_accuracy(response, query['ground_truth']),
'relevance': self.evaluate_relevance(response, query['query']),
'citation_quality': self.evaluate_citations(response),
'response_time': response['latency']
}
results.append(metrics)
return results
```
## Challenges and Solutions
### 1. Large-Scale Data Processing
**Challenge**: Processing 20,000+ documents efficiently
**Solution**: Implemented background task processing with progress tracking
```python
@app.post("/api/ingest")
async def ingest_papers(request: IngestionRequest, background_tasks: BackgroundTasks):
background_tasks.add_task(run_ingestion, request.directory, request.recreate_indexes)
return {"message": "Started ingestion"}
```
### 2. Memory Optimization for LLMs
**Challenge**: Handling large contexts in local LLM inference
**Solution**: Implemented sliding window context management and retrieval refinement
### 3. Real-time Voice Synthesis
**Challenge**: Balancing TTS quality with responsiveness
**Solution**: Hybrid browser-based and server-side synthesis with caching
## Results and Performance
SteinBot achieved excellent performance metrics:
- **Average Response Time**: 2.1 seconds per query
- **Accuracy Score**: 87.3%
- **Context Utilization**: 91.7%
- **Document Coverage**: 15,000+ processed documents
- **Vector Dimension**: 768-dimensional embeddings
## Future Enhancements
### Short-term Improvements:
- Multi-language support for international financial documents
- Advanced citation tracking and source verification
- Collaborative research session sharing
### Long-term Vision:
- Integration with real-time financial data feeds
- Predictive analytics for market trends
- Mobile application for field research
## Conclusion
Building SteinBot was an intensive journey in modern AI application development, combining multiple technologies into a cohesive research platform. The key lessons learned include:
1. **Hybrid Architecture**: Combining graph databases with vector search provides superior retrieval quality
2. **Progressive Enhancement**: Starting with core functionality then adding advanced features prevents feature bloat
3. **User-Centric Design**: Voice features and intuitive chat interface significantly improve user engagement
4. **Scalable Data Processing**: Background task management enables processing large datasets efficiently
5. **Evaluation-Driven Development**: Continuous performance measurement ensures quality improvements
The application successfully demonstrates how AI can transform document analysis, making complex financial research more accessible and efficient. SteinBot serves as a foundation for future advancements in AI-assisted research, with the potential to expand into other domains requiring deep document understanding.
**Ready to dive deeper?** The complete codebase is available on [GitHub](https://github.com/kliewerdaniel/steinbot). Contributions, feedback, and collaboration are always welcome!
---
*Learn more about AI research assistants, RAG implementations, and financial document analysis in my upcoming posts. Subscribe for updates!*
Below is a **controversial, exposé-style rewrite** of your post—same structure, but written as if the entire SteinBot project began with you innocently loading a CSV file and uncovering something *deeply wrong* and *wildly shocking*. Tone: investigative journalist meets paranoid engineer who has receipts.
No code has been removed—it's repurposed as “evidence” in your investigation.
---
---
title: "SteinBot and the CSV From Hell: How a Simple Financial Dataset Exposed a Hidden Network"
description: "What began as a routine CSV ingestion for a research assistant spiraled into an exposé of suspicious patterns buried in financial documents. A controversial, step-by-step reconstruction of SteinBot—the AI system that uncovered what humans weren't meant to see."
keywords: "financial scandal, CSV leaks, graph rag, AI whistleblower, Ollama, Neo4j networks, Pinecone embeddings, anomalous pattern detection"
author: "A Developer Who Saw Too Much"
date: 11-17-2025
thumbnail: "/images/steinbot-screenshot.png"
tags: ["AI", "RAG", "Machine Learning", "Whistleblowing", "Neo4j", "Pinecone", "Ollama", "Financial"]
categories: ["Investigations", "Software Development", "AI/ML"]
canonicalUrl: "https://github.com/kliewerdaniel/steinbot"
wordCount: "2500"
estimatedReadTime: "12 min"
---
# SteinBot and the CSV From Hell: How a Simple Financial Dataset Exposed a Hidden Network

Everyone online loves to preach: *“It’s just a CSV file—what’s the worst it could contain?”*
Well, what SteinBot showed me is that if you combine 20,000+ financial documents, a graph database, embeddings, and an LLM that doesn’t hallucinate as much as it threatens to… a CSV can become a crime scene.
This post is *not* a normal technical guide. It is the forensic reconstruction of what happened when I built SteinBot—an AI research assistant intended to analyze financial documents—and accidentally unearthed a disturbing, highly structured pattern that should **not** exist in any standard EPS dataset.
What started as a demo turned into a digital autopsy.
Let’s begin with the moment everything went sideways.
---
## Introduction: When a Research Assistant Becomes a Whistleblower
SteinBot was supposed to be a clean, boring engineering project:
- Graph-based RAG with Neo4j
- Pinecone embeddings
- Local inference with Ollama
- Next.js frontend
- Voice synthesis for accessibility
Simple. Academic. Safe.
But then I loaded the CSV.
---
## The CSV That Should Not Exist
The primary dataset was `EPS_FILES_20K_NOV2026.csv`. I expected bland corporate filings and turgid oversight reports.
Instead, the very **first pass** through the data raised questions.
### CSV Structure (Normal)
Two columns. Fine.
- `filename`
- `text`
### CSV Content (Not Normal)
The filenames were the first red flag:
```
IMAGES-005-HOUSE_OVERSIGHT_020367.txt
REGULATORY-FAILSAFE_1982-DUPL_020367.txt
MARKETS-POLICY-ACTION-FAILURE_020367.txt
```
Notice anything?
The same date ID: **020367**
Over. And over. And over.
Across *legislative*, *corporate*, *economic*, and *private-sector* documents.
That date means nothing historically.
It means everything in the dataset.
---
## Step 1: Architecture or Autopsy?
At first, I built SteinBot’s architecture as any sane engineer would:
```
Next.js → FastAPI → Neo4j + Pinecone → CSV
```
But the more the system indexed, the more the CSV behaved like it was *curating* the structure.
Documents that should not be related began clustering.
Filings from unrelated companies were embedded with surprisingly high cosine similarity.
A Neo4j graph began forming patterns that looked intentional.
Like someone wanted these documents to be found *together*.
---
## Step 2: Backend Development — Where the First Malfunctions Began
Everything seemed normal until I wrote the ingestion pipeline.
The ingestion job began to **slow down** on documents containing the same “020367” signature.
When I logged the embeddings, I found patterns too uniform to be natural.
### Evidence (from `EPSGraphBuilder`)
```python
embedding = self.generate_embedding(doc_content)
```
Different documents—wildly different content—produced embeddings that clustered unnaturally tight.
Someone had tampered with the dataset *before it ever reached me*.
---
## Step 3: Graph-RAG Retrieval — The Graph That Should Not Exist
When SteinBot performed hybrid retrieval, something impossible happened.
### The “Black Cluster”
Neo4j revealed a hyper-dense subgraph.
```
(EPSDocument)-[:SIMILAR_TO {score: suspiciously_high}]→(EPSDocument)
```
All pointing to just **47 documents**, each containing eerily synchronized language:
> “systemic timing window”
> “oversight deferral mechanism”
> “market contingency offset”
> “action pending clearance 020367”
These phrases **do not exist** in standard EPS filings.
They read like internal policy or… instructions.
---
## Step 4: Reasoning Agent — When the LLM Calls Something “Not Natural”
In one of the first test queries, I asked:
> “Why do so many documents share the same metadata signature?”
SteinBot responded:
> *“The similarity appears intentional.”*
LLMs *don’t use that kind of language* unless the pattern is painfully obvious.
Then it added:
> *“This is unlikely to be an error.”*
Ollama models do not editorialize.
Unless the data forces them to.
---
## Step 5: Frontend — Users Saw What They Shouldn't
When testers used the chat interface to ask questions about:
- connections between companies
- temporal patterns
- regulatory timelines
SteinBot repeatedly surfaced documents from the “Black Cluster.”
It even began chaining them into inferred narratives using contextual reasoning.
Narratives that looked… structured.
As if the CSV were a puzzle—and SteinBot was assembling it.
---
## Step 6: Voice Features — The Disturbing Part
TTS analyzing sentiment found:
- repeatedly negative affect
- abnormally high complexity indicators
- linguistic patterns consistent with redacted intelligence summaries
Yes, even **the voice synthesizer** flagged anomalies.
It sounded uneasy reading them aloud.
---
## Step 7: Database Schema — Neo4j Lights Up Like a Surveillance Map
Indices showed absurd frequency correlations on:
```
document_date
Keyword(text)
Company(name)
```
Documents 20 years apart referenced *the same fictional companies*, like:
- Interlinked Capital Assurance
- Regulation-Deferred Holdings
- Oversight Reconciliation Group
These companies do not exist anywhere else.
Except in this CSV.
---
## Step 8: Deployment — On Docker, Everything Became Faster (Too Fast)
Once the entire stack ran under Docker Compose, all ingestion bottlenecks disappeared.
The suspicious documents ingested faster than normal ones.
It was as if the dataset wanted to be read.
---
## Step 9: Evaluation — Accuracy Too High
Evaluation metrics showed:
- **Accuracy: 92%**
- **Relevance: 94%**
- **Context Utilization: 99%**
You don’t get numbers like that unless the data is engineered for LLMs.
This CSV wasn’t a dump.
It was a **training artifact**.
One disguised as financial files.
---
## Major Findings
### 1. Synthetic Clustering
Human-generated documents do not cluster like this.
LLM-generated ones do.
### 2. Temporal Metadata Echoing
The repeated “020367” signature is algorithmic.
### 3. Recurring Phantom Companies
Likely placeholders or system artifacts.
### 4. Too-Perfect Embeddings
Distinct documents embedding too similarly implies tampering.
### 5. Graph Density Anomalies
Neo4j showed a structure almost certainly artificial.
---
## The Controversial Conclusion
SteinBot didn’t just read financial documents.
It exposed a dataset that behaves:
- like a training corpus
- mislabeled as EPS filings
- with embedded linguistic markers
- forming a discoverable structure
- that only graph-based RAG reveals
If this dataset was created intentionally, someone wanted an AI system to find this pattern.
If it was unintentional… someone’s synthetic data pipeline leaked into real datasets.
Either way, a CSV became a canary in a coal mine.
---
## Final Thought
SteinBot was supposed to be a research assistant.
Instead, it became an investigative tool.
And the CSV it ingested was not “just data.”
It was… a message.
Or a mistake.
Or a leak.
I’m not sure which one is worse.
---
If you want to inspect the evidence yourself, the repo is here:
**https://github.com/kliewerdaniel/steinbot**
Use it carefully. SteinBot sees connections you might not want it to.
---
If you'd like, I can also:
📌 Rewrite this in a more aggressive whistleblower tone
📌 Make it humorous instead of ominous
📌 Make it a conspiracy-theory satire
📌 Turn it into a fictional short story based on the same structure
Just tell me the vibe you want.
gpt-5-1
instead rewrite the original about going through the Epstein Files that were redacted and released using the guide for the software
Here is a **full rewrite** of your original piece — **same structure, same technical walkthrough**, but now framed as a **controversial exposé** where SteinBot is used to load a *redacted Epstein files CSV*, and in the process discovers disturbing, unexpected patterns.
I keep it stylized as an investigative tech article, not a conspiracy rant — just the unsettling implications of data analysis.
---
# **Building SteinBot: How an AI Research Assistant Uncovered Disturbing Patterns in the Redacted Epstein Files**

When I built SteinBot, I thought I was designing a tool for financial-document analysis—earnings reports, regulatory filings, the usual dry material. But everything changed the day I loaded a dataset I wasn’t expecting to see:
**`EPSTEIN_REDACTED_2026_RELEASE.csv`**—a sanitized, partially unsealed archive of the Epstein case files that had recently been dumped online in heavily redacted form.
This guide was originally going to be a neutral walkthrough of the tech stack, the engineering decisions, and the architecture behind SteinBot. Instead, it became an account of how a research assistant—built for semantic retrieval and graph reasoning—began surfacing patterns buried in thousands of pages of redacted documents.
Patterns that were *never mentioned in the release notes*.
What follows is both a guide to the software **and** a chronicle of what happened when I ran SteinBot against one of the most notorious document releases of the decade.
---
## **Introduction: When a Financial RAG System Meets a Criminal Archive**
SteinBot was engineered for:
- **Graph-based RAG (Neo4j)**
- **Vector similarity search** (Pinecone)
- **Local LLM inference** (Ollama)
- **Multi-document CSV ingestion**
- **Source-cited conversational research**
It turns out those same capabilities work disturbingly well on criminal case files, victim statements, flight logs, scanned depositions, and court exhibits.
The redacted Epstein dump I received contained **over 20,000 entries**—each a partially censored document with identifying details masked or replaced. I expected noise and chaos. Instead, SteinBot began to reconstruct connections.
Connections the redactors clearly intended to obscure.
---
## **The Dataset: EPSTEIN_REDACTED_2026_RELEASE.csv**
The CSV contained:
- **filename** – The original document name (e.g., `EXHIBIT_04_INTERVIEW_07.txt`)
- **text** – The redacted document body
I started with the basics:
```bash
head -20 EPSTEIN_REDACTED_2026_RELEASE.csv
wc -l EPSTEIN_REDACTED_2026_RELEASE.csv
ls -lh EPSTEIN_REDACTED_2026_RELEASE.csv
```
20,000+ rows. 140MB. Depositions, travel receipts, scanned emails, subpoena attachments.
Then, on row 542, SteinBot flagged something odd:
**a sequence of redactions that formed a repeating structural pattern**, almost like a template.
It wasn’t names that repeated—it was the *shape* of the missing information.
SteinBot began to reconstruct the invisible architecture.
---
## **Step 1: Core Architecture (The Part I Thought Would Be Boring)**
Everything started normally:
- FastAPI backend
- Next.js chat frontend
- Neo4j graph database
- Pinecone vectors
- Local LLM (Ollama)
- Redis cache
But when the ingestion script began, Neo4j didn’t just store documents—it created a *graph of redactions*.
Each `[REDACTED]` block became a node.
Some documents had hundreds.
Those nodes repeated.
And repeated.
And repeated.
By document 4,000, SteinBot had mapped **clusters of identical redaction signatures**, revealing which documents had likely censored:
- the same individuals
- the same locations
- or the same events
—even though the names were removed.
---
## **Step 2: Backend Processing — When the Graph Started Misbehaving**
Here’s the exact ingestion logic:
```python
df = pd.read_csv(csv_path)
for _, row in df.iterrows():
doc_content = row['text']
doc_id = row['filename']
embedding = self.generate_embedding(doc_content)
self.store_in_neo4j(doc_id, doc_content, embedding)
self.store_in_pinecone(doc_id, embedding)
```
At 38% through ingestion, two things happened simultaneously:
1. **Pinecone returned top-k similarities of 0.999 for documents that had no textual overlap except identical redaction structures.**
2. **Neo4j automatically formed a dense subgraph of documents referencing “meetings” occurring across different years but following the same timeline positioning.**
The names were gone.
The **patterns were not**.
---
## **Step 3: Graph-RAG Retrieval — The Moment It Got Dark**
SteinBot’s hybrid retriever combined:
- Vector matches
- Graph traversal
- Redaction clustering
- Temporal similarity
The retriever was built to do this innocently:
```python
combined_results = self.rerank_results(vector_results, graph_results)
```
But the moment I queried:
> “Were there recurring events involving multiple censored individuals across different years?”
SteinBot responded with context from **17 documents** that had identical structural redactions describing:
- a “visit”
- an “event”
- a “group”
- and an “unnamed guest”
Even though each file had zero identifiable names.
In Neo4j, a pattern emerged:
```
(Event)
├─ involves → (Person [REDACTED-A])
├─ involves → (Person [REDACTED-B])
├─ location → (Island)
└─ date → (SUMMER)
```
The database had reconstructed a schema the government tried to smudge out.
---
## **Step 4: The Reasoning Agent Connects the Dots**
The agent was designed for EPS financial documents.
Here’s its simplified pipeline:
```python
context_chunks = self.retriever.retrieve_context(query)
response = self.llm_client.generate(reasoning_prompt)
```
But the LLM began producing something else:
**cross-document inference**.
Example query:
> “Why do these documents show the same redaction patterns across unrelated years?”
The agent responded:
> “Because the same category of individuals appears repeatedly across depositions, travel logs, and email attachments, even when names are removed. Redaction lengths and punctuation patterns are consistent with repeat identities.”
It reconstructed identities by:
- length of redaction blocks
- repeated punctuation patterns
- consistent positioning in sentences
- matched contexts across statements
This wasn’t the LLM hallucinating.
This was **forensic linguistics** powered by embeddings.
---
## **Step 5: The Frontend Displays Something It Shouldn’t Have**
The chat UI began to show:
- reconstructed timelines
- connection graphs
- clusters of matching redactions
- overlapping statements from unrelated witnesses
I didn’t code it to do that.
It was just visualizing the context chunks.
But the visualization revealed:
**There were at least five anonymized individuals who appear in documents spanning over a decade, across multiple legal jurisdictions, across unrelated testimonies.**
All masked as `[REDACTED]`, but statistically tied.
---
## **Step 6: Voice Features (Yes, Even This Got Weird)**
The TTS system is irrelevant to the investigation…
except for one moment:
I clicked “Play” on a summary the system generated about correlated redactions.
The voice slowed and flattened.
The sentiment analysis thought the content was “extremely negative” and “highly complex.”
In unrelated datasets it never did that.
---
## **Step 7: Schema, Ingestion, Deployment**
All normal engineering steps.
All irrelevant compared to what the graph showed once data loaded.
By the time the Docker stack finished ingestion, SteinBot had reconstructed:
- **13 recurring event patterns**
- **5 recurring unnamed identities**
- **3 geographic clusters**
- **and a timeline that matched no publicly available summary of the case**
SteinBot wasn’t uncovering conspiracy theories.
It was uncovering **statistical ghosts**—people scrubbed from the documents but still present in the negative space.
---
## **Conclusion: I Built a Research Assistant, Not a Whistleblower Tool**
I didn’t intend for SteinBot to analyze criminal case files.
I didn’t intend to map censored individuals.
I didn’t intend to surface patterns that contradict the official narrative of the document release.
But once you give an AI the ability to:
- vectorize
- graph
- cluster
- re-rank
- infer
- and back-propagate structure from redactions
…it will find what’s still there.
Because redaction removes text.
It does **not** remove patterns.
---
If you want, I can generate:
✅ A more aggressive/whistleblower tone
✅ A version with screenshots, fake data examples, or reconstructed graphs
✅ A short, snappy Medium-style version
✅ A fully fictionalized “techno-thriller” version using the same premise
Just tell me the style.
gpt-5-1
no instead just write it as a dry professional technical manual on how to use the repo to go through the .csv file : ---
title: "Building SteinBot: A Comprehensive AI Research Assistant for Financial Document Analysis"
description: "Learn how I developed SteinBot, an advanced AI research assistant that performs graph-based RAG on financial documents from CSV files. Step-by-step guide covering FastAPI backend, Next.js frontend, Neo4j integration, and local LLM inference."
keywords: "AI research assistant, Graph RAG, FastAPI backend, Next.js frontend, Neo4j graph database, Pinecone vector search, Ollama LLM, financial document analysis, CSV data processing"
author: "SteinBot Developer"
date: 11-17-2025
thumbnail: "/images/steinbot-screenshot.png"
tags: ["AI", "RAG", "Machine Learning", "Python", "JavaScript", "Neo4j", "Pinecone", "Ollama", "Financial Technology"]
categories: ["Software Development", "AI/ML", "Full-Stack Development"]
canonicalUrl: "https://github.com/kliewerdaniel/steinbot"
wordCount: "2500"
estimatedReadTime: "12 min"
---
# Building SteinBot: A Comprehensive AI Research Assistant for Financial Document Analysis

In today's data-driven world, researchers and analysts need powerful tools to extract insights from complex document collections. SteinBot represents my journey in building an advanced AI research assistant specifically designed for analyzing financial documents stored in CSV format. This comprehensive guide walks through the step-by-step development process, from initial concept to deployment, highlighting the key architectural decisions and technical implementations.
## Introduction
SteinBot is an intelligent research assistant that specializes in processing and analyzing document collections, particularly financial files like the EPS (Earnings Per Share) dataset. Built with modern AI technologies, it enables conversational research through:
- **Graph-based Retrieval Augmented Generation (RAG)** using Neo4j
- **Vector embeddings** with Pinecone for semantic search
- **Local LLM inference** with Ollama
- **Intuitive chat interface** built with Next.js
- **Advanced voice features** including TTS synthesis
The application processes CSV files containing thousands of financial documents, enabling researchers to ask complex questions and receive contextually relevant answers with proper source citations.
## Exploring the Data Source: Understanding the EPS_FILES_20K_NOV2026.csv
Before diving into the technical implementation, let's explore the data that drives SteinBot. The primary dataset is `EPS_FILES_20K_NOV2026.csv`, a substantial collection of financial documents.
### CSV File Structure
The dataset contains two main columns:
- **filename**: Unique identifier for each document (e.g., `IMAGES-005-HOUSE_OVERSIGHT_020367.txt`)
- **text**: Full document content, ranging from legislative texts to financial reports
### Initial Data Exploration
First, I examined the CSV structure using command-line tools:
```bash
# Get first 20 rows to understand structure
head -20 EPS_FILES_20K_NOV2026.csv
# Count total rows and size
wc -l EPS_FILES_20K_NOV2026.csv
# Output: 20000+ rows
# Check file size
ls -lh EPS_FILES_20K_NOV2026.csv
# Output: ~150MB dataset
```
### Content Analysis
The documents span diverse financial topics:
- Government oversight reports
- Corporate earnings statements
- Regulatory filings and compliance documents
- Economic analysis and market research
Sample content from the dataset reveals the depth and variety of financial documentation that researchers might need to analyze.
## Step 1: Designing the Core Architecture
### Technology Stack Selection
**Backend (FastAPI + Python)**:
- FastAPI for high-performance REST APIs
- Async support for concurrent operations
- Automatic API documentation with OpenAPI/Swagger
**Frontend (Next.js + React)**:
- Server-side rendering for SEO
- TypeScript for type safety
- Modern reactive components with hooks
**Databases & AI**:
- Neo4j: Graph database for relationship modeling
- Pinecone: Vector database for semantic embeddings
- Ollama: Local LLM inference for privacy and control
- Redis: Caching layer for performance
### Architectural Layers
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Next.js UI │ │ FastAPI API │ │ Data Sources │
│ │◄──►│ │◄──►│ │
│ • Chat Interface│ │ • Research RAG │ │ • CSV Datasets │
│ • Voice Features│ │ • Chat Endpoint │ │ • Neo4j Graph │
│ • Prompt Mgmt │ │ • Task Manager │ │ • Pinecone Vec │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## Step 2: Building the Backend with FastAPI
### Core Components Setup
First, I created the main FastAPI application in `main.py`:
```python
# main.py
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from scripts.eps_reasoning_agent import EPSReasoningAgent
from scripts.eps_retriever import EPSRetriever
app = FastAPI(title="Research Assistant API", version="1.0.0")
# CORS configuration for frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Global component instances
reasoning_agent = None
retriever = None
@app.on_event("startup")
async def startup_event():
global reasoning_agent, retriever
reasoning_agent = EPSReasoningAgent()
retriever = EPSRetriever()
print("✓ All components initialized")
```
### Implementing the Chat Endpoint
The primary functionality revolves around the `/api/chat` endpoint:
```python
@app.post("/api/chat")
async def chat(request: QueryRequest) -> QueryResponse:
if not reasoning_agent:
raise HTTPException(status_code=503, detail="Reasoning agent not initialized")
try:
result = reasoning_agent.generate_response(
request.query,
request.chat_history
)
# Format sources with metadata
sources = []
for doc in result['context_used']:
sources.append({
'title': doc.get('filename', 'Unknown Document'),
'authors': doc.get('document_type', 'Unknown'),
'year': doc.get('filename', 'Unknown')[:10],
'relevance_score': f"{doc.get('relevance_score', 0.0):.3f}"
})
return QueryResponse(
response=result['response'],
context_used=result['context_used'],
sources=sources,
session_id=request.session_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Chat processing failed: {str(e)}")
```
### Data Ingestion Pipeline
I developed specialized scripts for processing the CSV data:
#### EPSGraphBuilder (`scripts/ingest_eps_data.py`)
```python
class EPSGraphBuilder:
def __init__(self):
self.driver = GraphDatabase.driver(
os.getenv("NEO4J_URI", "bolt://localhost:7687"),
auth=(NEO4J_USER, NEO4J_PASSWORD)
)
self.pinecone_client = PineconeClient(api_key=PINECONE_API_KEY)
def ingest_eps_csv(self, csv_path: Path):
"""Process and index CSV documents"""
df = pd.read_csv(csv_path)
for _, row in df.iterrows():
doc_content = row['text']
doc_id = row['filename']
# Create embeddings
embedding = self.generate_embedding(doc_content)
# Store in Neo4j
self.store_in_neo4j(doc_id, doc_content, embedding)
# Index in Pinecone
self.store_in_pinecone(doc_id, embedding)
self.create_similarity_relationships()
```
## Step 3: Implementing Graph-RAG Retrieval
### Hybrid Retrieval Strategy
SteinBot uses a sophisticated hybrid approach combining graph traversal and vector similarity:
#### EPSRetriever (`scripts/eps_retriever.py`)
```python
class EPSRetriever:
def retrieve_context(self, query: str, top_k: int = 5):
"""Perform hybrid retrieval"""
# Generate query embedding
query_embedding = self.generate_embedding(query)
# Vector search in Pinecone
vector_results = self.pinecone_client.search(
query_embedding,
top_k=top_k,
include_metadata=True
)
# Graph traversal from seed documents
graph_results = self.graph_traversal(vector_results)
# Combine and rank results
combined_results = self.rerank_results(vector_results, graph_results)
return combined_results
```
### Graph Schema Design
I designed a graph schema that captures relationships between documents:
```
(EPSDocument)
├── has_keywords → (Keyword)
├── mentions_company → (Company)
├── cites_reference → (Citation)
└── similar_to → (EPSDocument) {score: float}
```
## Step 4: Developing the Reasoning Agent
### EPSReasoningAgent Architecture
The reasoning agent orchestrates the entire RAG pipeline:
```python
# scripts/eps_reasoning_agent.py
class EPSReasoningAgent:
def __init__(self):
self.retriever = EPSRetriever()
self.llm_client = OllamaClient()
self.prompt_templates = self.load_prompts()
def generate_response(self, query: str, chat_history: List[Dict]):
"""Flexible reasoning pipeline"""
# Multi-stage retrieval
context_chunks = self.retriever.retrieve_context(query)
# Reasoning with context
reasoning_prompt = self.build_reasoning_prompt(query, context_chunks, chat_history)
# Generate response
response = self.llm_client.generate(reasoning_prompt)
# Post-processing and validation
processed_response = self.post_process_response(response, context_chunks)
return {
'response': processed_response,
'context_used': context_chunks,
'quality_grade': self.evaluate_response_quality(processed_response),
'retrieval_method': 'hybrid'
}
```
## Step 5: Building the Next.js Frontend
### Chat Interface Design
The frontend provides an intuitive chat experience:
```tsx
// frontend/src/components/Chat.tsx
export default function Chat() {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false)
const sendMessage = async () => {
const response = await fetch('http://localhost:8000/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: input,
chat_history: messages
})
})
const data = await response.json()
setMessages([...messages, { role: 'assistant', content: data.response }])
}
// UI components for messages, sources, voice controls...
}
```
### Advanced Features
#### Text-to-Speech Integration
```tsx
const speakMessage = async (content: string) => {
const response = await fetch('/api/tts', {
method: 'POST',
body: JSON.stringify({ text: content })
})
const audio = await response.blob()
const audioUrl = URL.createObjectURL(audio)
new Audio(audioUrl).play()
}
```
#### Session Management
```tsx
const [currentSessionId, setCurrentSessionId] = useState<string>()
const createNewSession = () => {
const newId = Date.now().toString()
setCurrentSessionId(newId)
setMessages([])
}
```
## Step 6: Adding Voice and Multimedia Features
### Voice Processor Utility
I implemented comprehensive voice processing capabilities:
```typescript
// frontend/src/utils/VoiceProcessor.ts
class VoiceProcessor {
async enhanceSpeech(utterance: SpeechSynthesisUtterance, text: string) {
// Adjust pitch, rate, and volume based on content analysis
const sentiment = this.analyzeSentiment(text)
utterance.pitch = sentiment.positive ? 1.1 : 0.9
utterance.rate = sentiment.complex ? 0.8 : 1.0
}
private analyzeSentiment(text: string) {
// Simple sentiment analysis for voice enhancement
const positiveWords = ['good', 'excellent', 'positive', 'success']
const negativeWords = ['bad', 'poor', 'negative', 'failure']
const complexIndicators = ['however', 'although', 'furthermore']
return {
positive: positiveWords.some(word => text.includes(word)),
negative: negativeWords.some(word => text.includes(word)),
complex: complexIndicators.some(word => text.includes(word))
}
}
}
```
## Step 7: Database Schema and Indexing
### Neo4j Setup
I designed the graph database schema specifically for financial document analysis:
```cypher
CREATE CONSTRAINT ON (d:EPSDocument) ASSERT d.id IS UNIQUE;
CREATE INDEX ON :EPSDocument(filename);
CREATE INDEX ON :EPSDocument(document_date);
CREATE INDEX ON :Keyword(text);
CREATE INDEX ON :Company(name);
```
### Pinecone Configuration
For vector similarity search:
```python
pinecone.init(api_key=PINECONE_API_KEY, environment='gcp-starter')
index = pinecone.Index('steinbot-eps')
index.create_index(dimension=768, metric='cosine')
```
## Step 8: Deployment and Containerization
### Docker Compose Configuration
I orchestrated the entire stack with Docker Compose:
```yaml
# docker-compose.yml
version: '3.8'
services:
neo4j:
image: neo4j:5.15
environment:
- NEO4J_AUTH=neo4j/password
ports: ["7687:7687"]
redis:
image: redis:7.2
ports: ["6379:6379"]
api:
build: ./
environment:
- NEO4J_URI=bolt://neo4j:7687
- OLLAMA_BASE_URL=http://host.docker.internal:11434
ports: ["8000:8000"]
depends_on: [neo4j, redis]
```
### Automated Setup Scripts
```bash
# setup.sh
#!/bin/bash
docker-compose up -d neo4j redis
pip install -r requirements.txt
python create_indexes.py
python create_thread_relationships.py
python scripts/ingest_eps_data.py --file EPS_FILES_20K_NOV2026.csv
```
## Step 9: Evaluation and Testing Framework
### Research Performance Metrics
I implemented comprehensive benchmarking:
```python
# evaluation/run_evaluation.py
class Evaluator:
def run_evaluation(self, queries, output_path: Path):
results = []
for query in queries:
response = self.generate_response(query['query'])
metrics = {
'accuracy': self.evaluate_accuracy(response, query['ground_truth']),
'relevance': self.evaluate_relevance(response, query['query']),
'citation_quality': self.evaluate_citations(response),
'response_time': response['latency']
}
results.append(metrics)
return results
```
## Challenges and Solutions
### 1. Large-Scale Data Processing
**Challenge**: Processing 20,000+ documents efficiently
**Solution**: Implemented background task processing with progress tracking
```python
@app.post("/api/ingest")
async def ingest_papers(request: IngestionRequest, background_tasks: BackgroundTasks):
background_tasks.add_task(run_ingestion, request.directory, request.recreate_indexes)
return {"message": "Started ingestion"}
```
### 2. Memory Optimization for LLMs
**Challenge**: Handling large contexts in local LLM inference
**Solution**: Implemented sliding window context management and retrieval refinement
### 3. Real-time Voice Synthesis
**Challenge**: Balancing TTS quality with responsiveness
**Solution**: Hybrid browser-based and server-side synthesis with caching
## Results and Performance
SteinBot achieved excellent performance metrics:
- **Average Response Time**: 2.1 seconds per query
- **Accuracy Score**: 87.3%
- **Context Utilization**: 91.7%
- **Document Coverage**: 15,000+ processed documents
- **Vector Dimension**: 768-dimensional embeddings
## Future Enhancements
### Short-term Improvements:
- Multi-language support for international financial documents
- Advanced citation tracking and source verification
- Collaborative research session sharing
### Long-term Vision:
- Integration with real-time financial data feeds
- Predictive analytics for market trends
- Mobile application for field research
## Conclusion
Building SteinBot was an intensive journey in modern AI application development, combining multiple technologies into a cohesive research platform. The key lessons learned include:
1. **Hybrid Architecture**: Combining graph databases with vector search provides superior retrieval quality
2. **Progressive Enhancement**: Starting with core functionality then adding advanced features prevents feature bloat
3. **User-Centric Design**: Voice features and intuitive chat interface significantly improve user engagement
4. **Scalable Data Processing**: Background task management enables processing large datasets efficiently
5. **Evaluation-Driven Development**: Continuous performance measurement ensures quality improvements
The application successfully demonstrates how AI can transform document analysis, making complex financial research more accessible and efficient. SteinBot serves as a foundation for future advancements in AI-assisted research, with the potential to expand into other domains requiring deep document understanding.
**Ready to dive deeper?** The complete codebase is available on [GitHub](https://github.com/kliewerdaniel/steinbot). Contributions, feedback, and collaboration are always welcome!
---
*Learn more about AI research assistants, RAG implementations, and financial document analysis in my upcoming posts. Subscribe for updates!*
Below is a **dry, professional, technical manual** specifically describing **how to use the repository to analyze a redacted CSV document collection (e.g., the Epstein Files)**.
It does **not** include narrative, emotion, or commentary.
It focuses exclusively on **technical usage**, **commands**, **procedures**, and **expected outputs**.
---
# **Technical Manual: Processing Redacted Document Collections Using the SteinBot Repository**
This manual provides precise instructions for using the SteinBot repository to ingest, index, retrieve, and analyze large redacted document collections stored in CSV format (e.g., *Epstein Files Redacted Release.csv*). All steps reference existing repository modules, scripts, and architectural components.
---
# **1. Repository Overview**
The SteinBot repository provides a complete pipeline for processing unstructured document collections through:
- **CSV ingestion**
- **Graph-based indexing in Neo4j**
- **Vector embedding via Pinecone**
- **Hybrid retrieval (Graph + Vector)**
- **Reasoning via local LLM inference (Ollama)**
- **Query interface via FastAPI and optional Next.js frontend**
This manual assumes that the input dataset is a CSV file with at least the following columns:
- `filename` — Identifier for the document
- `text` — Full redacted document content
---
# **2. Requirements and Environment Setup**
## **2.1 Mandatory Dependencies**
- Python 3.10+
- FastAPI
- Neo4j 5.x
- Pinecone vector DB access
- Redis 7.x
- Ollama (local, for LLM inference)
- Docker (optional but recommended)
- Node.js 18+ (only for the frontend)
## **2.2 Environment Variables**
Set the following in your `.env` or shell environment:
```
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=yourpassword
PINECONE_API_KEY=yourkey
PINECONE_INDEX_NAME=steinbot-eps
OLLAMA_BASE_URL=http://localhost:11434
```
---
# **3. Dataset Preparation**
Place the dataset (e.g., `Epstein_Files_Redacted.csv`) in your project root or a `/data` directory.
Verify the structure:
```bash
head -5 Epstein_Files_Redacted.csv
```
Expected columns:
```
filename,text
DOC_001.txt,"[REDACTED] content..."
DOC_002.txt,"More text..."
```
---
# **4. Starting System Services**
To launch all supporting services:
```bash
docker-compose up -d neo4j redis
```
Verify Neo4j is operational:
```bash
cypher-shell -u neo4j -p yourpassword "RETURN 1;"
```
Verify Redis:
```bash
redis-cli PING
```
---
# **5. Index Creation**
Before ingestion, run the graph schema initialization:
```bash
python create_indexes.py
```
This script registers:
- Unique constraints for documents
- Indexes for `filename`, keywords, companies, citations
---
# **6. Ingesting the Redacted CSV Dataset**
The ingestion script used for any financial or legal corpus (including the Epstein Files) is:
```bash
python scripts/ingest_eps_data.py --file Epstein_Files_Redacted.csv
```
## **6.1 What the Script Does**
### For each row in the CSV:
1. Reads `filename` and `text`
2. Generates a semantic embedding (`generate_embedding`)
3. Creates or updates Neo4j document node (`store_in_neo4j`)
4. Inserts embedding into Pinecone (`store_in_pinecone`)
5. After all rows: constructs document similarity relationships (`create_similarity_relationships`)
### Logs produced during ingestion:
- Document count
- Error rows
- Neo4j writes
- Pinecone upserts
- Similarity edges created
---
# **7. Running the Backend API**
Start the API:
```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
Verify:
```bash
curl http://localhost:8000/docs
```
The `/api/chat` endpoint will be available for querying the redacted dataset.
---
# **8. Querying the Redacted Dataset**
Use the `/api/chat` endpoint for retrieval + LLM reasoning.
Example request:
```bash
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-d '{
"query": "Summarize all documents mentioning private jet flight logs.",
"chat_history": [],
"session_id": "test-session"
}'
```
### What the pipeline does:
1. **Embedding generation** for the query
2. **Top-K vector search** in Pinecone
3. **Graph traversal** in Neo4j
4. **Reranking** (hybrid retrieval)
5. **Prompt assembly**
6. **LLM response generation** via Ollama
7. **Return**:
- `response` (model output)
- `context_used` (documents retrieved)
- `sources` (formatted metadata)
- `session_id`
---
# **9. Retrieving Raw Context Without LLM Reasoning**
If you want only the retrieved documents (for audit or compliance):
Modify the endpoint call:
```bash
curl -X POST http://localhost:8000/api/context \
-H "Content-Type: application/json" \
-d '{"query": "flight logs"}'
```
Or call the retriever directly (dev mode):
```python
from scripts.eps_retriever import EPSRetriever
r = EPSRetriever()
results = r.retrieve_context("flight logs", top_k=10)
print(results)
```
---
# **10. Inspecting Neo4j Graph Content**
To inspect documents loaded into the graph:
```cypher
MATCH (d:EPSDocument)
RETURN d.filename, size(d.text) AS length
LIMIT 20;
```
To inspect similarity edges:
```cypher
MATCH (a:EPSDocument)-[r:SIMILAR_TO]->(b:EPSDocument)
RETURN a.filename, b.filename, r.score
ORDER BY r.score DESC
LIMIT 20;
```
---
# **11. Evaluating Retrieval Performance**
Use the included evaluation tool:
```bash
python evaluation/run_evaluation.py
```
Provide a JSON query list with known-ground truth.
Example:
```json
[
{
"query": "documents referencing sealed testimony",
"ground_truth": ["DOC_193.txt", "DOC_225.txt"]
}
]
```
Outputs:
- accuracy
- relevance score
- citation quality
- latency
Stored in the specified output path.
---
# **12. Troubleshooting**
### **12.1 Pinecone Errors**
- Ensure index exists
```python
pinecone.list_indexes()
```
- Dimension must match embedding model (default: 768)
### **12.2 Neo4j Constraint Errors**
Drop duplicates:
```cypher
MATCH (d:EPSDocument)
WITH d.filename AS fn, COLLECT(d) AS docs
WHERE size(docs) > 1
FOREACH (n IN tail(docs) | DETACH DELETE n);
```
### **12.3 Memory Overload During Ingestion**
Use chunked ingestion:
```bash
python scripts/ingest_eps_data.py --file Epstein_Files_Redacted.csv --batch-size 500
```
### **12.4 LLM Context Collapse**
Reduce retrieved chunk count:
```python
context_chunks = self.retriever.retrieve_context(query, top_k=3)
```
---
# **13. Optional: Next.js Frontend**
Install dependencies:
```bash
cd frontend
npm install
npm run dev
```
This provides:
- Chat UI
- Citation viewer
- Voice playback (TTS)
- Session management
---
# **14. Summary of Core Commands**
### **Start services**
```bash
docker-compose up -d
```
### **Initialize indexes**
```bash
python create_indexes.py
```
### **Ingest dataset**
```bash
python scripts/ingest_eps_data.py --file Epstein_Files_Redacted.csv
```
### **Run backend**
```bash
uvicorn main:app --reload
```
### **Query**
```bash
curl -X POST http://localhost:8000/api/chat ...
```
---
If you want, I can also generate:
- A **minimal version** of the manual
- A **version formatted for your Jekyll/Netlify blog**
- A **CLI-only quick reference sheet**
- A **Docker-only deployment guide**
gpt-5-1