admin / Bid-Sentinel
publicBid Scrape and Tracking Application with AI Capability
Bid-Sentinel / bid-sentinel-v2 / backend / app / routers / intelligence.py
6104 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | """Analyst Intelligence endpoints + printable PDF report (analyst/admin only).""" from fastapi import APIRouter, Depends, Query, Response from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app import ai, intelligence from app.analysis import get_capability_terms from app.appsettings import get_ai_enabled from app.database import get_db from app.deps import get_current_analyst from app.models import Accreditation, CustomKeyword, Tender, User from app.schemas import ( BuyerItem, CapabilityGapItem, CertGapItem, IntelSummary, SalesAngleItem, TrendPoint, WinLoss, ) from app.reports import build_intelligence_report, build_management_summary router = APIRouter(prefix="/intelligence", tags=["intelligence"]) async def _load(db: AsyncSession): """Fetch the inputs shared by every intelligence view.""" tenders = list((await db.execute(select(Tender))).scalars().all()) capability_terms = await get_capability_terms(db) custom_kw = [k for (k,) in (await db.execute(select(CustomKeyword.keyword))).all()] accreditations = [a for (a,) in (await db.execute(select(Accreditation.name).order_by(Accreditation.id))).all()] demand = intelligence.demand_vocabulary(custom_kw) return tenders, capability_terms, demand, accreditations @router.get("/summary", response_model=IntelSummary) async def get_summary(db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst)): tenders, terms, demand, accred = await _load(db) return IntelSummary(**intelligence.summary(tenders, terms, demand, accred)) @router.get("/capability-gaps", response_model=list[CapabilityGapItem]) async def get_capability_gaps(db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst)): tenders, terms, demand, _ = await _load(db) return intelligence.capability_gaps(tenders, terms, demand) @router.get("/cert-gaps", response_model=list[CertGapItem]) async def get_cert_gaps(db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst)): tenders, _, _, accred = await _load(db) return intelligence.cert_gaps(tenders, accred) @router.get("/buyers", response_model=list[BuyerItem]) async def get_buyers(db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst)): tenders, _, _, _ = await _load(db) return intelligence.buyer_profiles(tenders) @router.get("/winloss", response_model=WinLoss) async def get_winloss(db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst)): tenders, _, _, _ = await _load(db) return intelligence.win_loss(tenders) @router.get("/trends", response_model=list[TrendPoint]) async def get_trends(db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst)): tenders, _, _, _ = await _load(db) return intelligence.trends(tenders) @router.get("/angles", response_model=list[SalesAngleItem]) async def get_angles( db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst), use_ai: bool = Query(default=False), limit: int = Query(default=5, le=15), ): """Sales angles for the top target buyers (deterministic, or AI when requested).""" tenders, terms, _, accred = await _load(db) buyers = intelligence.buyer_profiles(tenders) targets = [b for b in buyers if b["is_target"]] or buyers ai_on = use_ai and await get_ai_enabled(db) and ai.is_available() out: list[SalesAngleItem] = [] for buyer in targets[:limit]: angle, source = intelligence.deterministic_angle(buyer, accred), "rules" if ai_on: written = await ai.sales_angle(intelligence.angle_context(buyer, terms, accred)) if written: angle, source = written, "AI" out.append(SalesAngleItem(subject=buyer["buyer"], angle=angle, source=source)) return out @router.get("/management-summary.pdf") async def management_summary_pdf( db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst), ): """Executive pipeline report: tracked / open / being-bid-on opportunities, pipeline value, and capability + certification gaps.""" tenders, terms, demand, accred = await _load(db) pdf = build_management_summary( stats=intelligence.management_stats(tenders), bidding_list=intelligence.bidding_opportunities(tenders), capability_gaps=intelligence.capability_gaps(tenders, terms, demand), cert_gaps=intelligence.cert_gaps(tenders, accred), ) return Response( content=pdf, media_type="application/pdf", headers={"Content-Disposition": 'attachment; filename="bid-sentinel-management-summary.pdf"'}, ) @router.get("/report.pdf") async def report_pdf( db: AsyncSession = Depends(get_db), _a: User = Depends(get_current_analyst), use_ai: bool = Query(default=False), ): """Printable Intelligence Report PDF.""" tenders, terms, demand, accred = await _load(db) buyers = intelligence.buyer_profiles(tenders) targets = [b for b in buyers if b["is_target"]] or buyers angles = [] ai_on = use_ai and await get_ai_enabled(db) and ai.is_available() for buyer in targets[:5]: text = intelligence.deterministic_angle(buyer, accred) source = "rules" if ai_on: written = await ai.sales_angle(intelligence.angle_context(buyer, terms, accred)) if written: text, source = written, "AI" angles.append({"subject": buyer["buyer"], "angle": text, "source": source}) pdf = build_intelligence_report( summary=intelligence.summary(tenders, terms, demand, accred), capability_gaps=intelligence.capability_gaps(tenders, terms, demand), cert_gaps=intelligence.cert_gaps(tenders, accred), buyers=buyers, winloss=intelligence.win_loss(tenders), angles=angles, ) return Response( content=pdf, media_type="application/pdf", headers={"Content-Disposition": 'attachment; filename="bid-sentinel-intelligence.pdf"'}, ) |