TenderIQ — Architecture
Overview
TenderIQ is a single-process Streamlit application that automates eligibility evaluation of bidders against government tender criteria. All state is local: SQLite for the audit log, ChromaDB (file-backed) for vector indices, and the filesystem for PDFs and cached OCR results. The only external dependency is the DeepSeek API.
┌──────────────────────────────────────────────────────────────────────┐
│ Streamlit App (app.py) │
│ │
│ Tab 1: Overview Tab 2: Tender Analysis Tab 3: Bidder Evaluation │
│ Tab 4: Human Review Tab 5: Audit Log Tab 6: Interpretability │
└────────────────────────────┬─────────────────────────────────────────┘
│ calls
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────────┐ ┌──────────────┐
│ criteria_ │ │ bidder_processor │ │ evaluator │
│ extractor │ │ ocr_pipeline │ │ fallback │
└────┬───────┘ └────┬─────────────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌─────────────┐ ┌──────────────┐
│ DeepSeek │ │ ChromaDB │ │ DeepSeek LLM │
│ LLM │ │ (vectors) │◄────────│ (evaluate) │
└──────────┘ └─────────────┘ └──────────────┘
│ ▲
│ │ index chunks
│ ┌──────┴──────┐
│ │ 3-tier OCR │
│ │ PyMuPDF │
│ │ Tesseract │
│ │ Vision LLM │
│ └─────────────┘
│
▼
┌──────────┐
│ audit │ (SQLite, append-only)
└──────────┘
Module Responsibilities
| Module | Responsibility |
|---|---|
core/config.py |
Environment loading, constants, paths |
core/schemas.py |
Pydantic models: Criterion, Evidence, Verdict, AuditEntry |
core/prompts.py |
LLM prompt strings (criteria extraction, evaluation, OCR) |
core/llm_client.py |
DeepSeek API wrapper — chat_json, chat_vision, retry logic, LLMUnavailable |
core/pdf_utils.py |
PyMuPDF: text extraction, text-PDF detection, page rendering |
core/ocr_pipeline.py |
Three-tier OCR orchestrator, MD5-based result cache |
core/chunker.py |
Text chunking for tender and bidder documents |
core/vectorstore.py |
ChromaDB helpers: get/create collection, upsert, query |
core/criteria_extractor.py |
Stage 1: tender PDF → list[Criterion] |
core/bidder_processor.py |
Stage 2: bidder docs → chunks + evidence retrieval |
core/evaluator.py |
Stage 3: per-criterion verdict with combined confidence |
core/audit.py |
SQLite audit log: write and query |
core/fallback.py |
Load pre-computed JSON when LLM unavailable |
Three-Tier OCR Pipeline
The robustness centrepiece. Handles typed PDFs, scanned PDFs, and photographs of documents.
Input file
│
├─ Is image (PNG/JPG)? ──────────────────────────────────┐
│ │
└─ Is PDF? ▼
│ Tier 2: Tesseract
├─ is_text_pdf() == True │
│ │ mean_conf ≥ 0.65?
│ ▼ │
│ Tier 1: PyMuPDF Yes ──┘ No
│ confidence = 1.0 │
│ source_type = "text_pdf" ▼
│ Tier 3: DeepSeek Vision LLM
└─ is_text_pdf() == False confidence = 0.95
│ source_type = "vision_llm"
▼
Render pages → Tier 2
Each ExtractedPage carries: page, text, source_type, confidence, raw_tier_results.
Results cached under .ocr_cache/<md5>.json.
Confidence & Threshold Logic
Combined confidence weights LLM certainty against OCR quality:
| OCR Tier | Formula |
|---|---|
text_pdf |
combined = llm_confidence |
vision_llm |
combined = 0.7 × llm_confidence + 0.3 × 0.95 |
tesseract |
combined = 0.6 × llm_confidence + 0.4 × tesseract_conf |
Safety threshold rules (applied in order):
- LLM returns
needs_review→ keep (regardless of confidence) combined ≥ 0.80→ keep LLM verdict0.55 ≤ combined < 0.80AND verdict isnot_eligible→ downgrade toneeds_reviewcombined < 0.55→ forceneeds_review
The core safety guarantee: a bidder is never silently disqualified at medium or low confidence.
Fallback Strategy
Every live LLM call is wrapped in try/except LLMUnavailable. On failure:
audit.log("precomputed_fallback_used", ...)is writtenst.session_state["fallback_active"] = Truetriggers the amber sidebar dot- Data is loaded from
data/precomputed/*.json(committed to the repo)
This means the demo works even if the API is down, rate-limited, or the key is missing.
Audit Log Schema
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL, -- UTC ISO timestamp
action TEXT NOT NULL, -- see action vocabulary below
actor TEXT NOT NULL, -- "system" or "officer"
model_version TEXT, -- e.g. "deepseek-chat@2026-05-07"
bidder_id TEXT,
criterion_id TEXT,
payload_json TEXT -- action-specific JSON payload
);
Action vocabulary: criteria_extracted, bidder_processed, criterion_evaluated,
human_review_action, precomputed_fallback_used, vision_ocr_invoked.
Data Flow (full pipeline)
1. Officer uploads tender PDF
│
▼
2. criteria_extractor.extract_criteria(pdf)
→ LLM reads tender text
→ Returns List[Criterion] with structured rules
→ Stored in st.session_state["criteria"]
│
▼
3. bidder_processor.process_bidder(bidder_id, files)
→ For each file: ocr_pipeline.extract_document()
→ chunker.chunk_bidder()
→ vectorstore.add_chunks("bidder_chunks", ...)
│
▼
4. evaluator.evaluate(bidder_id, criterion)
→ bidder_processor.gather_evidence() — semantic search in ChromaDB
→ LLM evaluates evidence against criterion rule
→ combined_confidence computed
→ threshold safety rules applied
→ Verdict returned + audit logged
│
▼
5. Verdicts stored in st.session_state["verdicts"]
→ Tab 3: evaluation matrix
→ Tab 4: needs_review items surfaced for human action
→ Tab 6: plain-English explanation + Q&A
→ Tab 5: full audit trail