"""Financing Starts Now — runtime client. Talks to the Modal backend (modal.App("financing-starts-now")) over HTTP, OR serves canned data when FINANCING_MOCK is set — same signatures either way, so the UI never branches on mode. The HF Space deploy injects FINANCING_STARTS_NOW_API_URL. """ from __future__ import annotations import os import httpx API_URL = os.environ.get( "FINANCING_STARTS_NOW_API_URL", "https://rafalbogusdxc--financing-starts-now-api.modal.run", ).rstrip("/") MOCK = os.environ.get("FINANCING_MOCK", "").strip().lower() in ("1", "true", "yes", "on") TIMEOUT_S = 30 # fail fast — local fallback is instant, don't make users wait class BackendError(RuntimeError): """Inference backend unreachable or returned an error.""" def health() -> dict: if MOCK: return {"status": "mock"} try: resp = httpx.get(f"{API_URL}/health", timeout=10, follow_redirects=True) resp.raise_for_status() return resp.json() except Exception: return {"status": "unreachable"} def genie_summary(path_key: str, profile_sentence: str, matches: list[dict]) -> tuple[str, bool]: """Personalized recommendation text for this answer path. Returns (markdown_text, was_cached). Raises BackendError on failure. """ if MOCK: top = matches[0] runner = matches[1] if len(matches) > 1 else None text = ( f"*Poof!* The scrolls are clear. As {profile_sentence.split(';')[0].strip()}, " f"your strongest path is **{top['name']}** — {top['amount_label']}. " f"It scores **{top['score']}%** against your answers: your stage and funding " f"needs sit squarely inside this programme's range, and the door is open " f"({top['deadline_label']}). Start with: {top['apply'][0].lower()}." ) if runner: text += ( f"\n\nKeep **{runner['name']}** ({runner['score']}%) as your second wish — " f"{runner['amount_label']}. Choose it if the first path stalls." ) text += "\n\n*(mock genie — set FINANCING_MOCK=0 and deploy Modal for the real one)*" return text, True try: resp = httpx.post( f"{API_URL}/genie", json={ "path_key": path_key, "profile_sentence": profile_sentence, "matches": matches, }, timeout=TIMEOUT_S, follow_redirects=True, ) resp.raise_for_status() data = resp.json() return data["summary"], bool(data.get("cached")) except httpx.HTTPStatusError as e: raise BackendError(f"Backend error {e.response.status_code}: " f"{e.response.text[:300]}") from e except httpx.HTTPError as e: raise BackendError(f"Cannot reach the Genie backend at {API_URL} ({e})") from e