A full-stack system that extracts structured, evidence-grounded facts from PDF documents, resolves entities across documents, and identifies cross-document relationships (corroboration, contradiction, reconciliation).
Demo video: https://youtu.be/CNV7ICs5Wns
- What it does
- Architecture
- Tech stack
- Prerequisites
- Setup and run
- UI pages
- API reference
- LLM providers
- Features built
- Known limitations
- Demo cases
Upload any PDF → the system:
- Parses and chunks the document into page-aware text segments
- Extracts structured facts using an LLM — every fact includes entity, attribute, value, unit, time period, confidence, and an exact source quote
- Validates evidence — facts without a verifiable quote in the source text are rejected
- Resolves entities — "Delhivery", "Delhivery Ltd.", "Delhivery Limited" → same canonical entity
- Compares facts across documents — finds corroborations, contradictions, and reconciliations
- Stores everything in PostgreSQL with pgvector for future semantic search
PDF Upload
↓
FastAPI (REST API)
↓
Redis + Celery (background workers)
↓
PDF Parse → Chunking → LLM Extraction → Evidence Validation
↓
Entity Resolution → Fact Clustering → Cross-document Comparison
↓
PostgreSQL + pgvector (persistence)
↓
React / Vite (frontend)
backend/app/
├── api/v1/
│ ├── routes_documents.py — upload, list, delete, status, rerun
│ ├── routes_facts.py — list, get, review (accept/reject)
│ ├── routes_relationships.py
│ ├── routes_compare.py — selective comparison
│ ├── routes_entities.py — entity explorer
│ ├── routes_analytics.py — system-wide stats
│ ├── routes_jobs.py — Celery job status
│ └── routes_schema.py — fact type management
├── domain/
│ ├── models/ — Document, Chunk, Entity, Fact, Relationship
│ ├── services/ — ingestion, extraction, comparison, entity resolution
│ └── value_objects/ — EvidenceRef, TemporalScope
├── infra/
│ ├── db/ — SQLAlchemy ORM, repositories, session
│ ├── llm/ — LLM client (Ollama / Gemini / OpenAI)
│ ├── pdf/ — PyMuPDF parser and chunker
│ ├── storage/ — blob storage
│ └── vectorstore/ — pgvector client
└── workers/
├── tasks_ingestion.py
├── tasks_extraction.py
└── tasks_comparison.py
frontend/src/
├── pages/
│ ├── UploadPage.tsx — PDF upload with live status polling
│ ├── StatusDashboard.tsx — live processing dashboard
│ ├── AnalyticsPage.tsx — charts and system overview
│ ├── FactExplorer.tsx — search, filter, sort, export facts
│ ├── EntityExplorer.tsx — browse entities and their facts
│ ├── ReviewQueue.tsx — accept / reject flagged facts
│ ├── DocumentView.tsx — per-document facts with evidence
│ ├── RelationshipGraph.tsx — cross-document relationships
│ └── ComparePage.tsx — selective document comparison
├── components/
│ ├── FactCard.tsx
│ ├── EvidenceHighlighter.tsx
│ └── RelationshipBadge.tsx
└── api/client.ts — typed API client (axios)
| Layer | Technology |
|---|---|
| Frontend | React 19 + TypeScript + Vite |
| Backend API | FastAPI (Python 3.11+) |
| Background workers | Celery + Redis |
| Database | PostgreSQL 16 + pgvector |
| LLM (default) | Gemini 3.5-flash-lite (free tier) |
| LLM (alternative) | Ollama (local), OpenAI-compatible |
| PDF parsing | PyMuPDF |
| Migrations | Alembic |
| Containers | Docker Compose |
Make sure these are installed:
- Python 3.11+
- Node.js 18+
- Docker Desktop (for PostgreSQL + Redis)
- Ollama — only needed if using local LLM instead of Gemini
git clone https://github.com/Harsh-128/fact-knowledge-layer-submission.git
cd fact-knowledge-layer-submissionFrom the project root:
docker compose up -d
docker compose ps # both should show "healthy"cd backend
cp .env.example .envOpen backend/.env and set your LLM provider. Recommended — Gemini free tier (no credit card):
LLM_PROVIDER=gemini
GEMINI_API_KEY=your_key_here
GEMINI_MODEL=gemini-3.5-flash-liteGet a free key at https://aistudio.google.com/apikey.
Alternatively use local Ollama:
LLM_PROVIDER=ollama
OLLAMA_BASE_URL=http://127.0.0.1:11434
OLLAMA_MODEL=qwen2.5-coder:1.5b# Inside backend/
python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux / macOS
source .venv/bin/activate
pip install -e ".[dev]"# Inside backend/ with venv activated
alembic upgrade headThis creates all tables and enables the pgvector extension.
ollama pull qwen2.5-coder:1.5b
ollama serveOpen 4 separate terminals:
Terminal 1 — FastAPI backend
cd backend
.venv\Scripts\activate # or source .venv/bin/activate
uvicorn app.main:app --reloadAPI available at http://localhost:8000
Docs available at http://localhost:8000/docs
Terminal 2 — Celery worker
cd backend
.venv\Scripts\activate
# Windows — must use --pool=solo
celery -A app.workers.celery_app:celery_app worker --loglevel=info --pool=solo
# Linux / macOS
celery -A app.workers.celery_app:celery_app worker --loglevel=infoTerminal 3 — Frontend
cd frontend
npm install
npm run devFrontend available at http://localhost:5173
Terminal 4 — Docker (already running from step 2)
http://localhost:5173
| Service | Command | Purpose |
|---|---|---|
| PostgreSQL + Redis | docker compose up -d |
Database and message broker |
| FastAPI | uvicorn app.main:app --reload |
REST API |
| Celery | celery -A app.workers.celery_app:celery_app worker ... |
Background processing |
| Frontend | npm run dev |
UI |
| Ollama | ollama serve |
Local LLM (skip if using Gemini) |
| Page | Nav label | Description |
|---|---|---|
| Upload | Upload | Upload PDFs, track processing status |
| Status Dashboard | 📊 Status | Live cards for all documents — chunks, facts, review count |
| Analytics | 📈 Analytics | Charts: facts per doc, relationship types, confidence distribution, top entities |
| Fact Explorer | Fact Explorer | Search/filter/sort 600+ facts, export as CSV |
| Entity Explorer | 🏢 Entities | Browse canonical entities, see all facts per entity across documents |
| Review Queue | 🔍 Review | Accept or reject facts flagged as uncertain |
| Document View | Document View | Per-document fact list with evidence; delete documents |
| Relationships | Relationships | Cross-document relationships with LLM explanations; filter by doc/type |
| Compare | Compare | Select specific PDFs and compare only those |
All endpoints are under /api/v1/.
| Method | Path | Description |
|---|---|---|
GET |
/documents |
List all documents |
POST |
/documents/upload |
Upload a PDF (queues background job) |
DELETE |
/documents/{id} |
Delete document + all facts/relationships |
GET |
/documents/{id}/status |
Status with chunk/fact/review counts |
POST |
/documents/{id}/rerun-comparisons |
Re-dispatch comparison tasks |
| Method | Path | Description |
|---|---|---|
GET |
/facts |
List facts (filter by doc, entity, attribute, needs_review, confidence) |
GET |
/facts/{id} |
Get single fact with evidence |
PATCH |
/facts/{id}/review |
Accept (accept: true) or reject (accept: false) a fact |
| Method | Path | Description |
|---|---|---|
GET |
/relationships |
List relationships (filter by doc, type, fact_id) |
GET |
/relationships/{id} |
Get single relationship with explanation |
| Method | Path | Description |
|---|---|---|
GET |
/entities |
List entities (search by name, filter by type) |
GET |
/entities/{id} |
Entity detail — all facts grouped by attribute |
| Method | Path | Description |
|---|---|---|
POST |
/compare |
Compare specific documents: { "document_ids": ["id1", "id2"] } |
| Method | Path | Description |
|---|---|---|
GET |
/analytics/summary |
Full system stats — totals, charts data |
| Method | Path | Description |
|---|---|---|
GET |
/jobs/{task_id} |
Celery task status |
The system supports three providers, switchable via .env:
LLM_PROVIDER=gemini
GEMINI_API_KEY=your_key
GEMINI_MODEL=gemini-3.5-flash-liteFree tier: 15 RPM, ~1000 RPD, no credit card. Get key at https://aistudio.google.com/apikey
LLM_PROVIDER=ollama
OLLAMA_BASE_URL=http://127.0.0.1:11434
OLLAMA_MODEL=qwen2.5-coder:1.5bRequires Ollama installed and running. Slower but fully offline.
LLM_PROVIDER=openai
OPENAI_API_KEY=your_key
OPENAI_MODEL=gpt-4o-mini
OPENAI_BASE_URL=https://api.openai.com/v1Also works with OpenRouter and other OpenAI-compatible providers.
- PDF upload, parse, page-aware chunking
- LLM-based structured fact extraction with evidence validation
- Deterministic fast path for financial tables (no LLM call needed)
- Entity resolution across documents (normalises company name variants)
- Cross-document fact comparison → CORROBORATES / CONTRADICTS / RECONCILES
- SHA-256 duplicate detection (same PDF never processed twice)
- Asynchronous processing via Celery + Redis
- Full REST API with 9 route groups
- Selective comparison endpoint (compare only chosen documents)
- Analytics endpoint (all stats in one call)
- Entity explorer endpoint with fact grouping
- Review workflow (accept/reject flagged facts)
- Re-run comparisons on demand
- Upload with live job status polling
- Status Dashboard — auto-polls every 3 s, progress bars
- Analytics — horizontal bar charts + donut charts (pure CSS/SVG, zero new packages)
- Fact Explorer — pagination, sort by confidence, min-confidence slider, CSV export
- Entity Explorer — search/filter entities, see all facts grouped by attribute
- Review Queue — accept / reject / undo with optimistic UI
- Document View — per-document facts, delete button, back navigation
- Relationship Explorer — filter by document and type, LLM explanations shown
- Compare page — checkbox selection, runs comparison only between selected PDFs
TemporalScopenull construction corrupting every DB read- Comparison pipeline completely unwired (relationships never created)
- Schema router never registered (all
/schemaendpoints 404) - Temp upload files never cleaned up
- Auth permanently disabled (missing
api_keyfield in Settings) - Gemini model default was a non-existent model name
- PGVectorClient dimension hardcoded to 1536 (breaks with Ollama)
- setTimeout memory leak in upload polling loop
- Stale closure in Fact Explorer selected-fact logic
- pgvector extension not enabled before migrations
- And 16 more medium/low severity issues
- Scanned PDFs (image-based) — no text layer, nothing extracted. OCR support (Tesseract) is planned.
- Complex tables — row/column relationships can break during PDF text extraction.
- Gemini rate limits — free tier allows ~15 RPM. Large documents (100+ pages) hit the limit and retry automatically, adding processing time.
- Local Ollama — much slower than Gemini for large documents (~40 sec/batch vs ~2 sec).
- Temporal reasoning — facts from different time periods with the same attribute can be hard to compare correctly.
- Human review UI — review metadata exists and the Review Queue page is functional, but there is no bulk-review workflow yet.
Two documents report the same underlying fact (possibly in different wording). The system extracts both, resolves to the same entity and attribute, and creates a CORROBORATES relationship.
Two documents report different values for the same entity, attribute, and time period. After numerical comparison the system creates a CONTRADICTS relationship and the LLM explains why.
Two values initially appear to conflict but are reconcilable — for example INR 100 crore vs INR 1000 million (same amount, different units), or the same metric reported for different quarters. The system creates a RECONCILES relationship with a full explanation.
During testing with an unseen stock-report PDF, the extraction pipeline encountered a table whose numerical columns were not reliably mapped to their row headers. The extracted text values were meaningful but could not be unambiguously grounded to the correct row. This is documented as a known limitation of plain-text PDF chunking for complex tables.
- Gemini 3.5-flash-lite (or Ollama
qwen2.5-coder:1.5b) — fact extraction, entity comparison, relationship reasoning, explanation generation - Deterministic logic — evidence validation, exact duplicate detection, table parsing, structural validation
The system deliberately keeps the LLM to semantic tasks only. Validation, persistence, deduplication, and exact comparisons are all deterministic.
