| # 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): |
|
|
| 1. LLM returns `needs_review` → keep (regardless of confidence) |
| 2. `combined ≥ 0.80` → keep LLM verdict |
| 3. `0.55 ≤ combined < 0.80` AND verdict is `not_eligible` → downgrade to `needs_review` |
| 4. `combined < 0.55` → force `needs_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: |
|
|
| 1. `audit.log("precomputed_fallback_used", ...)` is written |
| 2. `st.session_state["fallback_active"] = True` triggers the amber sidebar dot |
| 3. 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 |
|
|
| ```sql |
| 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 |
| ``` |
|
|