"""Generate TenderIQ_Pitch.pdf — 8-slide pitch deck using reportlab.""" import sys from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent sys.path.insert(0, str(BASE_DIR)) from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.units import cm, mm from reportlab.pdfgen.canvas import Canvas W, H = A4 NAVY = colors.HexColor("#0D1B2A") BLUE = colors.HexColor("#2563EB") LBLUE = colors.HexColor("#DBEAFE") GOLD = colors.HexColor("#F0A500") WHITE = colors.white GREY = colors.HexColor("#64748B") LGREY = colors.HexColor("#F1F5F9") GREEN = colors.HexColor("#059669") RED = colors.HexColor("#DC2626") AMBER = colors.HexColor("#D97706") BORD = colors.HexColor("#E2E8F0") def _header_bar(c: Canvas, title: str, subtitle: str = "") -> None: c.setFillColor(NAVY) c.rect(0, H - 2.8*cm, W, 2.8*cm, fill=1, stroke=0) c.setFillColor(GOLD) c.rect(0, H - 2.85*cm, W, 0.18*cm, fill=1, stroke=0) c.setFillColor(WHITE) c.setFont("Helvetica-Bold", 18) c.drawString(1.8*cm, H - 1.7*cm, title) if subtitle: c.setFont("Helvetica", 10) c.setFillColor(colors.HexColor("#94A3B8")) c.drawString(1.8*cm, H - 2.3*cm, subtitle) def _footer(c: Canvas, page: int, total: int = 8) -> None: c.setFillColor(LGREY) c.rect(0, 0, W, 1.0*cm, fill=1, stroke=0) c.setFillColor(GREY) c.setFont("Helvetica", 8) c.drawString(1.8*cm, 0.35*cm, "TenderIQ · CRPF Hackathon Theme 3 · Explainable AI for Government Procurement") c.drawRightString(W - 1.8*cm, 0.35*cm, f"{page} / {total}") def _bullet(c: Canvas, x: float, y: float, text: str, size: int = 10, indent: float = 0.5*cm) -> float: c.setFillColor(BLUE) c.circle(x + 0.15*cm, y + 0.3*cm, 0.12*cm, fill=1, stroke=0) c.setFillColor(NAVY) c.setFont("Helvetica", size) lines = _wrap(text, 85 - int(indent / mm)) for i, line in enumerate(lines): c.drawString(x + indent, y - i * (size + 3) * 0.035 * cm * 28.35 / 10, line) return y - len(lines) * (size + 4) * 0.035 * cm * 28.35 / 10 def _wrap(text: str, width: int) -> list[str]: words = text.split() lines, cur = [], "" for w in words: if len(cur) + len(w) + 1 <= width: cur = (cur + " " + w).strip() else: if cur: lines.append(cur) cur = w if cur: lines.append(cur) return lines or [""] def _card(c: Canvas, x: float, y: float, w: float, h: float, title: str, body: str, accent: colors.Color = BLUE) -> None: c.setFillColor(WHITE) c.setStrokeColor(BORD) c.roundRect(x, y, w, h, 0.3*cm, fill=1, stroke=1) c.setFillColor(accent) c.roundRect(x, y + h - 0.35*cm, w, 0.35*cm, 0.3*cm, fill=1, stroke=0) c.rect(x, y + h - 0.35*cm, w, 0.2*cm, fill=1, stroke=0) c.setFillColor(WHITE) c.setFont("Helvetica-Bold", 9) c.drawString(x + 0.3*cm, y + h - 0.25*cm, title) c.setFillColor(GREY) c.setFont("Helvetica", 8.5) lines = _wrap(body, int(w / (0.22*cm))) for i, line in enumerate(lines[:5]): c.drawString(x + 0.3*cm, y + h - 0.75*cm - i * 0.45*cm, line) def slide_1_title(c: Canvas) -> None: c.setFillColor(NAVY) c.rect(0, 0, W, H, fill=1, stroke=0) c.setFillColor(BLUE) c.rect(0, 0, W, 0.5*cm, fill=1, stroke=0) c.setFillColor(GOLD) c.rect(0, 0.5*cm, W, 0.12*cm, fill=1, stroke=0) c.setFillColor(WHITE) c.setFont("Helvetica", 40) c.drawCentredString(W / 2, H - 6*cm, "⚖️") c.setFont("Helvetica-Bold", 36) c.drawCentredString(W / 2, H - 8*cm, "TenderIQ") c.setFont("Helvetica", 15) c.setFillColor(colors.HexColor("#CBD5E1")) c.drawCentredString(W / 2, H - 9.2*cm, "Explainable AI for Government Tender Evaluation") c.setFillColor(GOLD) c.roundRect(W/2 - 5*cm, H - 11.5*cm, 10*cm, 1.1*cm, 0.3*cm, fill=1, stroke=0) c.setFillColor(NAVY) c.setFont("Helvetica-Bold", 11) c.drawCentredString(W / 2, H - 11.0*cm, "CRPF Hackathon · Theme 3") c.setFillColor(colors.HexColor("#64748B")) c.setFont("Helvetica", 9) c.drawCentredString(W / 2, H - 13.5*cm, "Central Reserve Police Force · Ministry of Home Affairs") c.drawCentredString(W / 2, H - 14.1*cm, "AI-Based Tender Evaluation and Eligibility Analysis") _footer(c, 1) def slide_2_problem(c: Canvas) -> None: _header_bar(c, "The Problem", "Manual tender evaluation is slow, inconsistent, and opaque") _footer(c, 2) y = H - 4.0*cm c.setFont("Helvetica-Bold", 11) c.setFillColor(NAVY) c.drawString(1.8*cm, y, "Government procurement officers today must:") y -= 0.6*cm problems = [ "Manually read hundreds of pages of tender documents and bidder submissions", "Identify eligibility criteria buried in legal language across multiple sections", "Cross-check financial statements, certificates, and project records for each bidder", "Handle scanned documents, photographs, and mixed-format submissions", "Reach consistent decisions — yet two evaluators routinely disagree on the same bid", "Produce an auditable trail for every decision, under compliance and RTI pressure", ] for p in problems: y = _bullet(c, 1.8*cm, y, p) y -= 0.25*cm y -= 0.3*cm c.setFillColor(LBLUE) c.roundRect(1.8*cm, y - 1.6*cm, W - 3.6*cm, 1.6*cm, 0.3*cm, fill=1, stroke=0) c.setFillColor(BLUE) c.setFont("Helvetica-Bold", 11) c.drawCentredString(W / 2, y - 0.7*cm, "For one tender, a committee may spend 3–5 days.") c.setFont("Helvetica", 10) c.setFillColor(GREY) c.drawCentredString(W / 2, y - 1.15*cm, "TenderIQ reduces this to minutes, with full explainability.") def slide_3_solution(c: Canvas) -> None: _header_bar(c, "Our Solution", "TenderIQ automates evaluation while preserving human oversight") _footer(c, 3) pillars = [ (BLUE, "📄 Extract", "DeepSeek LLM reads the tender PDF and structures every eligibility criterion as JSON — category, rule, source clause, query hints."), (GREEN, "🔍 OCR & Index","Three-tier pipeline handles any document: PyMuPDF → Tesseract → Vision LLM. All text indexed into ChromaDB with provenance."), (AMBER, "⚖️ Evaluate", "Per-criterion vector search + LLM evaluation. Combined confidence score. Safety rule: never silent disqualification."), (RED, "👤 Review", "Borderline verdicts surface in a human review queue with full evidence. Every action logged to SQLite for compliance."), ] cw = (W - 3.6*cm) / 2 ch = 5.0*cm positions = [ (1.8*cm, H - 9.5*cm), (1.8*cm + cw + 0.4*cm, H - 9.5*cm), (1.8*cm, H - 9.5*cm - ch - 0.5*cm), (1.8*cm + cw + 0.4*cm, H - 9.5*cm - ch - 0.5*cm), ] for (px, py), (color, title, body) in zip(positions, pillars): _card(c, px, py, cw, ch, title, body, color) def slide_4_architecture(c: Canvas) -> None: _header_bar(c, "Architecture", "Single-process Streamlit app — no separate services") _footer(c, 4) boxes = [ (1.8*cm, H - 5.5*cm, 5.0*cm, 1.2*cm, "Tender PDF", LBLUE, BLUE), (8.5*cm, H - 5.5*cm, 5.5*cm, 1.2*cm, "Criteria (JSON)", LBLUE, BLUE), (15.5*cm, H - 5.5*cm, 4.5*cm, 1.2*cm, "ChromaDB Index", LGREY, GREY), (1.8*cm, H - 9.0*cm, 5.0*cm, 1.2*cm, "Bidder Docs", LGREY, GREY), (8.5*cm, H - 9.0*cm, 5.5*cm, 1.2*cm, "OCR Pipeline ×3", colors.HexColor("#FDF4FF"), colors.HexColor("#7E22CE")), (15.5*cm, H - 9.0*cm, 4.5*cm, 1.2*cm, "Verdicts", colors.HexColor("#F0FDF4"), GREEN), (5.5*cm, H - 13.0*cm, 10*cm, 1.2*cm, "SQLite Audit Log", colors.HexColor("#FFFBEB"), AMBER), ] for bx, by, bw, bh, label, fill, stroke in boxes: c.setFillColor(fill) c.setStrokeColor(stroke) c.roundRect(bx, by, bw, bh, 0.25*cm, fill=1, stroke=1) c.setFillColor(stroke) c.setFont("Helvetica-Bold", 9) c.drawCentredString(bx + bw / 2, by + 0.4*cm, label) arrows = [ (6.8*cm, H - 4.95*cm, 8.5*cm, H - 4.95*cm), (8.5*cm + 5.5*cm, H - 4.95*cm, 15.5*cm, H - 4.95*cm), (1.8*cm + 5.0*cm, H - 8.45*cm, 8.5*cm, H - 8.45*cm), (8.5*cm + 5.5*cm, H - 8.45*cm, 15.5*cm, H - 8.45*cm), (15.5*cm + 2.25*cm, H - 9.0*cm, 15.5*cm + 2.25*cm, H - 5.5*cm - 1.2*cm), (W / 2, H - 9.0*cm - 0, W / 2, H - 13.0*cm + 1.2*cm), ] c.setStrokeColor(GREY) c.setLineWidth(1) for x1, y1, x2, y2 in arrows: c.line(x1, y1, x2, y2) c.setFont("Helvetica", 8.5) c.setFillColor(GREY) c.drawCentredString(W / 2, H - 14.5*cm, "DeepSeek API · ChromaDB (embedded) · SQLite · No external services") def slide_5_ocr(c: Canvas) -> None: _header_bar(c, "Three-Tier OCR Pipeline", "Handles typed PDFs, scanned documents, and photographs") _footer(c, 5) tiers = [ (BLUE, "Tier 1 — PyMuPDF", "Cost: free, instant\nTrigger: document is a typed/digital PDF\nConfidence: 1.0 (lossless)\nOutput: exact text with page numbers"), (colors.HexColor("#7E22CE"), "Tier 2 — Tesseract OCR", "Cost: free, fast\nTrigger: scanned PDF or image file\nConfidence: mean of per-word scores\nOutput: extracted text (quality varies)"), (AMBER, "Tier 3 — DeepSeek Vision LLM", "Cost: API call, slower\nTrigger: Tesseract confidence < 65%\nConfidence: 0.95\nOutput: faithfully transcribed text\nAudit: vision_ocr_invoked logged"), ] tw = (W - 4.0*cm) / 3 for i, (color, title, body) in enumerate(tiers): x = 1.8*cm + i * (tw + 0.3*cm) y = H - 9.0*cm c.setFillColor(color) c.roundRect(x, y, tw, 5.5*cm, 0.3*cm, fill=1, stroke=0) c.setFillColor(WHITE) c.setFont("Helvetica-Bold", 10) c.drawCentredString(x + tw / 2, y + 5.0*cm, title) c.setFont("Helvetica", 9) for j, line in enumerate(body.split("\n")): c.drawString(x + 0.4*cm, y + 4.2*cm - j * 0.5*cm, line) y = H - 11.0*cm c.setFillColor(LGREY) c.roundRect(1.8*cm, y - 1.8*cm, W - 3.6*cm, 1.8*cm, 0.3*cm, fill=1, stroke=0) c.setFillColor(NAVY) c.setFont("Helvetica-Bold", 10) c.drawCentredString(W / 2, y - 0.65*cm, "Demo: Bidder C submits a blurry, rotated CA certificate scan") c.setFont("Helvetica", 9) c.setFillColor(GREY) c.drawCentredString(W / 2, y - 1.2*cm, "Tesseract confidence ~55% → Vision LLM transcribes correctly → combined confidence 0.58 → needs_review") def slide_6_explainability(c: Canvas) -> None: _header_bar(c, "Explainability & Compliance", "Every verdict is traceable to a document, page, and model decision") _footer(c, 6) features = [ ("Criterion-level verdicts", "Each (bidder × criterion) pair has an independent verdict with extracted value, source document, page number, OCR tier, LLM confidence, and plain-English reason."), ("Never silent disqualification", "The safety threshold rule: if combined confidence is 0.55–0.80 and the LLM says not_eligible, the verdict is downgraded to needs_review and surfaced for human review."), ("Full audit trail", "Every action is logged to SQLite: criteria_extracted, bidder_processed, criterion_evaluated, human_review_action, vision_ocr_invoked, precomputed_fallback_used."), ("Interpretability tab", "Plain-English explanation of each verdict with inline PDF page previews. LLM-powered Q&A lets officers ask specific questions with source citations."), ("Human review queue", "Flagged verdicts show the evidence snippet, extracted value, source page, and OCR tier badge. Officers Approve / Edit & Approve / Reject with audit logging."), ("Pre-computed fallback", "If the API is unavailable, pre-computed JSON is served transparently. The sidebar shows an amber dot and a banner. No silent failures."), ] col_w = (W - 4.0*cm) / 2 for i, (title, body) in enumerate(features): col = i % 2 row = i // 2 x = 1.8*cm + col * (col_w + 0.4*cm) y = H - 4.5*cm - row * 2.8*cm c.setFillColor(WHITE) c.setStrokeColor(BORD) c.roundRect(x, y - 2.0*cm, col_w, 2.0*cm, 0.25*cm, fill=1, stroke=1) c.setFillColor(BLUE) c.setFont("Helvetica-Bold", 9) c.drawString(x + 0.3*cm, y - 0.45*cm, title) c.setFillColor(GREY) c.setFont("Helvetica", 8) lines = _wrap(body, int(col_w / (0.22*cm))) for j, line in enumerate(lines[:3]): c.drawString(x + 0.3*cm, y - 0.9*cm - j * 0.38*cm, line) def slide_7_demo(c: Canvas) -> None: _header_bar(c, "Demo: Three Test Scenarios", "Mock CRPF tender with 5 criteria evaluated against 3 realistic bidders") _footer(c, 7) scenarios = [ (GREEN, "✅ Bidder A — Eligible", "Apex Constructions Pvt. Ltd.", [ "C1 Turnover: INR 6.37 Cr avg — exceeds 5 Cr threshold", "C2 Projects: 5 completed including CRPF barracks (2024)", "C3 GST: GSTIN 27AABCA1234F1Z5, Active", "C4 ISO 9001:2015: Valid through June 2027", "C5 Paramilitary: CRPF Camp Pune project on record", ]), (RED, "❌ Bidder B — Not Eligible", "BuildRight Enterprises", [ "C1 Turnover: INR 1.5 Cr avg — BELOW 5 Cr threshold", "C2 Projects: 4 completed — passes", "C3 GST: GSTIN 29AABCB5678G1Z3, Active", "C4 ISO 9001:2015: Valid through August 2027", "C5 Paramilitary: No relevant experience", ]), (AMBER, "⚠️ Bidder C — Needs Review", "Shree Constructions & Services", [ "C1 Turnover: Scanned cert → Tesseract 55% → Vision LLM", " INR 5.4 Cr found, but borderline — human review required", "C2 Projects: Exactly 3 — borderline meets threshold", "C3 GST: GSTIN 24AABCC9012H1Z1, Active", "C4 ISO 9001:2015: Valid through September 2027", ]), ] cw = (W - 4.0*cm) / 3 for i, (color, title, company, bullets) in enumerate(scenarios): x = 1.8*cm + i * (cw + 0.3*cm) y_top = H - 3.8*cm c.setFillColor(color) c.roundRect(x, y_top - 0.9*cm, cw, 0.9*cm, 0.25*cm, fill=1, stroke=0) c.setFillColor(WHITE) c.setFont("Helvetica-Bold", 9) c.drawCentredString(x + cw / 2, y_top - 0.55*cm, title) c.setFillColor(WHITE) c.setStrokeColor(BORD) c.roundRect(x, y_top - 8.5*cm, cw, 7.6*cm, 0.25*cm, fill=1, stroke=1) c.rect(x, y_top - 0.9*cm, cw, 0.2*cm, fill=1, stroke=0) c.setFillColor(GREY) c.setFont("Helvetica-Oblique", 8) c.drawString(x + 0.3*cm, y_top - 1.35*cm, company) c.setFont("Helvetica", 8) c.setFillColor(NAVY) for j, b in enumerate(bullets): c.drawString(x + 0.3*cm, y_top - 2.0*cm - j * 0.5*cm, b) def slide_8_stack(c: Canvas) -> None: _header_bar(c, "Technology Stack & Impact", "Built for the hackathon — deployable to Streamlit Cloud or HuggingFace Spaces in minutes") _footer(c, 8) stack = [ ("UI & Orchestration", "Streamlit 1.39", "Single-process app, tabs, session state"), ("LLM", "DeepSeek API", "chat_json + chat_vision (OpenAI-compatible)"), ("OCR Tier 1", "PyMuPDF 1.24", "Lossless text extraction from digital PDFs"), ("OCR Tier 2", "Tesseract", "Open-source OCR for scanned documents"), ("OCR Tier 3", "DeepSeek Vision","Multimodal LLM for low-confidence scans"), ("Vector Store", "ChromaDB 0.5", "Embedded, file-backed, all-MiniLM-L6-v2"), ("Schemas", "Pydantic v2", "Strict validation of all LLM outputs"), ("Audit Log", "SQLite", "Append-only, exportable as CSV"), ] c.setFillColor(NAVY) c.setFont("Helvetica-Bold", 9) col_x = [1.8*cm, 6.5*cm, 11.5*cm] for x, lbl in zip(col_x, ["Component", "Technology", "Role"]): c.drawString(x, H - 4.2*cm, lbl) c.setStrokeColor(BORD) c.line(1.8*cm, H - 4.4*cm, W - 1.8*cm, H - 4.4*cm) for i, (comp, tech, role) in enumerate(stack): y = H - 4.9*cm - i * 0.6*cm if i % 2 == 0: c.setFillColor(LGREY) c.rect(1.8*cm, y - 0.1*cm, W - 3.6*cm, 0.55*cm, fill=1, stroke=0) c.setFillColor(NAVY); c.setFont("Helvetica-Bold", 8.5); c.drawString(1.8*cm + 0.2*cm, y + 0.2*cm, comp) c.setFillColor(BLUE); c.setFont("Helvetica", 8.5); c.drawString(6.5*cm + 0.2*cm, y + 0.2*cm, tech) c.setFillColor(GREY); c.setFont("Helvetica", 8.5); c.drawString(11.5*cm + 0.2*cm, y + 0.2*cm, role) y_impact = H - 10.5*cm c.setFillColor(NAVY) c.roundRect(1.8*cm, y_impact - 2.8*cm, W - 3.6*cm, 2.8*cm, 0.3*cm, fill=1, stroke=0) c.setFillColor(GOLD) c.setFont("Helvetica-Bold", 11) c.drawCentredString(W / 2, y_impact - 0.7*cm, "Business Impact") impacts = [ "⏱ Days of manual evaluation → minutes of automated processing", "📋 Criterion-level audit trail satisfies RTI and compliance requirements", "🔍 Every verdict traceable to a document, page, OCR tier, and model version", ] c.setFont("Helvetica", 9.5) c.setFillColor(colors.HexColor("#CBD5E1")) for j, imp in enumerate(impacts): c.drawString(2.5*cm, y_impact - 1.35*cm - j * 0.5*cm, imp) def main() -> None: out = BASE_DIR / "deck" / "TenderIQ_Pitch.pdf" out.parent.mkdir(parents=True, exist_ok=True) c = Canvas(str(out), pagesize=A4) slides = [ slide_1_title, slide_2_problem, slide_3_solution, slide_4_architecture, slide_5_ocr, slide_6_explainability, slide_7_demo, slide_8_stack, ] for fn in slides: fn(c) c.showPage() c.save() print(f"Deck saved: {out} ({len(slides)} slides)") if __name__ == "__main__": main()