admin / Apex
publicBid Management and Orchestration Tool with AI Capability
Apex / Synapse-Apexv2 / backend / app / services / library.py
3239 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 | """Answer Library harvesting — turn approved bid responses into catalogue entries. The Library fills itself: whenever a response is approved the question+answer is upserted here (keyed by source_response_id so re-approval just refreshes it), and when a bid is won/lost that outcome is propagated onto its harvested entries so search ranking reflects reality. """ from __future__ import annotations from datetime import datetime, timezone from sqlalchemy.orm import Session from app.models import AnswerEntry, Bid, BidRequirement, BidResponse def _outcome_for(bid: Bid) -> str: if bid.state == "won": return "won" if bid.state == "lost": return "lost" if bid.state in ("submitted", "in_review", "approved", "in_progress", "planning", "draft"): return "in_progress" return "n/a" def harvest_response(db: Session, response: BidResponse, actor_id: int | None = None) -> None: """Upsert an Answer Library entry from an approved response.""" text = (response.final_text or response.draft_text or "").strip() if not text: return req = db.get(BidRequirement, response.requirement_id) if not req: return bid = db.get(Bid, req.bid_id) outcome = _outcome_for(bid) if bid else "n/a" existing = ( db.query(AnswerEntry) .filter(AnswerEntry.source_response_id == response.id) .first() ) now = datetime.now(timezone.utc) if existing: existing.question = req.question_text existing.answer = text existing.outcome = outcome existing.word_count = len(text.split()) existing.client = bid.client if bid else existing.client existing.frameworks = bid.frameworks if bid else existing.frameworks if existing.status == "archived": existing.status = "curated" existing.updated_at = now else: db.add( AnswerEntry( question=req.question_text, answer=text, category=req.type if req.type != "question" else "", tags=[], frameworks=(bid.frameworks if bid else []) or [], source_type="bid_response", source_bid_id=bid.id if bid else None, source_response_id=response.id, client=bid.client if bid else "", outcome=outcome, status="curated", word_count=len(text.split()), created_by=actor_id, ) ) db.commit() def propagate_outcome(db: Session, bid: Bid) -> int: """Push a bid's won/lost outcome onto every entry harvested from it.""" outcome = _outcome_for(bid) rows = db.query(AnswerEntry).filter(AnswerEntry.source_bid_id == bid.id).all() for r in rows: r.outcome = outcome if rows: db.commit() return len(rows) def backfill(db: Session, actor_id: int | None = None) -> int: """Harvest every already-approved response into the Library (idempotent).""" approved = db.query(BidResponse).filter(BidResponse.status == "approved").all() count = 0 for resp in approved: harvest_response(db, resp, actor_id) count += 1 return count |