| """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() |
|
|