A RAG (Retrieval-Augmented Generation) system for querying financial earnings call transcripts. Ask natural language questions about a company's quarterly results and get precise, cited answers grounded strictly in the source documents.
💬 Question: What did HDFC Bank say about their NIM guidance for FY26?
=============== VERBATIM RESPONSE ===============
Context: HDFC_Bank | FY26
HDFC Bank management indicated that they expect the NIM to stabilise and
gradually improve over the coming quarters as the CD ratio normalises...
[HDFC_Bank, FY26, Q2, Page 14]
==================================================
Each question goes through a four-stage pipeline:
User Question
│
▼
┌─────────────────┐
│ 1. Extraction │ GPT-4o-mini parses the question into structured filters
│ │ (company, FY, quarter) and a retrieval-optimised query
└────────┬────────┘
│
▼
┌─────────────────┐
│ 2. Retrieval │ Hybrid search — 70% vector (pgvector cosine) +
│ │ 30% keyword (PostgreSQL FTS) fused via Weighted RRF
└────────┬────────┘
│ 20 candidate chunks
▼
┌─────────────────┐
│ 3. Reranking │ Gemini 2.5 Flash (RankGPT pattern) selects the
│ │ top-5 most relevant chunks from the 20 candidates
└────────┬────────┘
│ 5 precise chunks
▼
┌─────────────────┐
│ 4. Synthesis │ GPT-4o-mini generates a cited answer using only the
│ │ retrieved chunks — no hallucination, strict citations
└─────────────────┘
Every query is logged to the database with split-stage latencies, retrieval signal breakdown (vector-only / keyword-only / overlap), and a reranker displacement score that measures how much the LLM reranker promoted lower-ranked chunks.
| Component | Technology |
|---|---|
| Database | PostgreSQL 16 + pgvector |
| Embeddings | OpenAI text-embedding-3-small (1536-dim) |
| Query extraction | OpenAI GPT-4o-mini via LangChain |
| Reranking | Google Gemini 2.5 Flash Lite |
| Synthesis | OpenAI GPT-4o |
| PDF parsing | pdfplumber + LangChain RecursiveCharacterTextSplitter |
| Package manager | uv |
| Type checking | mypy (strict) |
git clone https://github.com/agrawal-vatsal/verbatim.git
cd verbatim
uv synccp .env.example .envEdit .env and fill in your credentials:
DATABASE_URL=postgresql://user:user@localhost:5433/verbatim
OPENAI_API_KEY=sk-proj-...
GEMINI_API_KEY=AIza...docker compose up -dThis starts a pgvector/pgvector:pg16 container on port 5433 (to avoid conflicts with any local PostgreSQL on 5432). Data is persisted in a named Docker volume.
Enable the pgvector extension:
uv run python -m scripts.init_dbApply all schema migrations:
uv run python -m scripts.migrateDownload the PDFs listed in manifest.json:
uv run python -m scripts.download_dataIngest all downloaded PDFs into the vector database:
uv run python -m scripts.ingest_allIngestion is idempotent — re-running it skips any transcripts already in the database.
uv run python -m scripts.chatType your question at the prompt. Type exit or quit to stop.
- Open
manifest.jsonand add an entry:
{
"company": "Infosys",
"fy": "FY26",
"quarter": "Q1",
"url": "https://..."
}- Re-run the download and ingest steps:
uv run python -m scripts.download_data
uv run python -m scripts.ingest_allIf you already have PDFs locally, place them in data/raw/transcripts/ following the naming convention Company_FYXX_QX.pdf (e.g. Infosys_FY26_Q1.pdf) and run only the ingest step.
View system statistics including per-stage latencies, retrieval signal breakdown, reranker effectiveness, and content coverage:
uv run python -m scripts.stats════════════════════════════════════════════════════════════
📊 VERBATIM OBSERVABILITY DASHBOARD
════════════════════════════════════════════════════════════
⏳ LATENCY PIPELINE (24H)
Phase | Average | P95 (Tail)
---------------------------------------------
🧠 Processing | 320ms | 480ms
🔍 Retrieval | 85ms | 130ms
🔀 Reranking | 410ms | 680ms
✍️ Synthesis | 1840ms | 2500ms
---------------------------------------------
🚀 TOTAL E2E | 2655ms | 3790ms
🎯 RAG QUALITY (DISTANCE)
• Median (P50): 0.3142 🟢
• Worst (P95): 0.4560 🟢 (Lower is better)
🔀 RETRIEVAL SIGNAL (AVG CHUNKS PER QUERY)
• Both engines: 3.2 (16%)
• Vector only: 12.4 (62%)
• Keyword only: 4.4 (22%)
🔁 RERANKER DISPLACEMENT (0=no change, 1=full reorder)
• Median (P50): 0.3120 🟢
• Tail (P95): 0.6840
🏢 CONTENT COVERAGE
• HDFC_Bank 1284 chunks
Reading the dashboard:
- Reranking row: shows what the Gemini reranker call costs in latency; if P95 is high relative to synthesis it may not be worth the trade-off.
- Retrieval signal: shows whether hybrid search is balanced. High vector-only % means keyword search isn't contributing much. High overlap % means both engines agree, which is a strong relevance signal.
- Reranker displacement: measures how much the LLM reranker actually reorders chunks. P50 near 0 means it rubber-stamps the RRF ranking (wasted API call); P50 above ~0.15 means it's genuinely promoting different chunks.
verbatim/
├── verbatim/ # Core library
│ ├── db.py # Database layer (PostgreSQL + pgvector)
│ ├── processor.py # Query extraction (GPT-4o-mini via LangChain)
│ ├── reranker.py # LLM reranker (Gemini RankGPT) + displacement metric
│ └── synthesizer.py # Answer synthesis (GPT-4o)
│
├── scripts/
│ ├── chat.py # Main CLI — ChatManager orchestrator
│ ├── download_data.py # Downloads PDFs from manifest.json
│ ├── ingest_data.py # PDF parsing and embedding generation
│ ├── ingest_all.py # Batch ingestion for a directory of PDFs
│ ├── init_db.py # Enables the pgvector extension
│ ├── migrate.py # Applies SQL migrations in order
│ ├── stats.py # Observability dashboard
│ └── check_db.py # Quick DB connectivity check
│
├── migrations/ # SQL schema migrations (applied in numbered order)
├── prompts/ # Prompt templates
│ ├── extraction.txt # Metadata extraction + query optimisation
│ ├── reranking.txt # RankGPT reranking prompt
│ └── synthesis.txt # Cited answer generation
│
├── data/raw/transcripts/ # Downloaded PDF files (gitignored)
├── manifest.json # List of transcripts to download
├── docker-compose.yaml # PostgreSQL + pgvector service
├── pyproject.toml
└── .env.example
- Always specify a company. The system requires a company name to scope the search. If you omit it, it will list available options.
- FY and quarter are optional filters. Omitting them searches across all available periods for that company.
- Specifying a quarter requires a FY.
Q3alone is ambiguous — provideFY25 Q3. - Ask about what was said, not facts. This system is grounded in earnings call transcripts, so questions like "What did management say about X?" or "What guidance was given for Y?" work best.
Type-check the codebase:
uv run mypy .Verify database connectivity:
uv run python -m scripts.check_db