Catalyst / admin/Apex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Apex

public

Bid Management and Orchestration Tool with AI Capability

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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