File size: 4,464 Bytes
221080b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""FastAPI entry-point for the Outlet Intelligence web app.

Run locally:
    cd webapp/backend
    pip install -r requirements.txt
    cp .env.example .env       # set GEMINI_API_KEY
    python -m uvicorn main:app --reload --port 8000

The app loads gold parquets / audit CSVs into memory once at startup and
exposes the data through JSON endpoints. CORS is open in local dev so the
Next.js frontend on :3000 can call it directly.
"""
from __future__ import annotations

import sys
from contextlib import asynccontextmanager
from pathlib import Path

# Ensure routers/ and services/ are importable as top-level packages
# whether the app is launched as `main:app` from webapp/backend/ or as
# `backend.main:app` from webapp/.
_BACKEND_DIR = Path(__file__).resolve().parent
if str(_BACKEND_DIR) not in sys.path:
    sys.path.insert(0, str(_BACKEND_DIR))

from dotenv import load_dotenv                       # noqa: E402
from fastapi import FastAPI                          # noqa: E402
from fastapi.middleware.cors import CORSMiddleware   # noqa: E402

# Always load the .env that lives next to this file, regardless of CWD.
load_dotenv(_BACKEND_DIR / ".env")

from routers import outlets as outlets_router        # noqa: E402
from routers import dashboards as dash_router        # noqa: E402
from routers import xai as xai_router                # noqa: E402
from services.data_loader import get_cache           # noqa: E402


@asynccontextmanager
async def lifespan(app: FastAPI):
    import os
    cache = get_cache()
    print(f"[startup] Repo root: {cache.repo_root}")
    print(f"[startup] outlets table: {cache.outlets_table.shape}")
    print(f"[startup] predictions:   {cache.predictions.shape}")
    print(f"[startup] dormancy:      {cache.dormancy.shape}")
    key = os.environ.get("GEMINI_API_KEY")
    if key:
        print(f"[startup] GEMINI_API_KEY loaded ({len(key)} chars) "
              f"using model {os.environ.get('GEMINI_MODEL', 'gemini-2.5-flash')}")
    else:
        print("[startup] WARN: GEMINI_API_KEY not set — /api/xai endpoints will 503")
    yield


app = FastAPI(
    title="Outlet Intelligence API",
    version="0.1.0",
    description="Beverage outlet potential, budget allocation, and XAI narratives.",
    lifespan=lifespan,
)

# CORS — local dev (localhost / 127.0.0.1 any port) plus deployed origins:
# Vercel previews/production (*.vercel.app), the HF Space's own origin so
# the FastAPI Swagger UI works in-place, and any extra origins listed in
# the CORS_EXTRA_ORIGINS env var (comma-separated). The regex is intentionally
# strict so we don't accept arbitrary HTTP origins from the internet.
import os as _os  # noqa: E402

_extra = [o.strip() for o in _os.environ.get("CORS_EXTRA_ORIGINS", "").split(",") if o.strip()]
app.add_middleware(
    CORSMiddleware,
    allow_origin_regex=(
        r"https?://(localhost|127\.0\.0\.1)(:\d+)?|"
        r"https://[a-zA-Z0-9-]+\.vercel\.app|"
        r"https://[a-zA-Z0-9-]+-[a-zA-Z0-9-]+\.vercel\.app|"
        r"https://huggingface\.co|"
        r"https://[a-zA-Z0-9-]+\.hf\.space"
    ),
    allow_origins=_extra,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(outlets_router.router)
app.include_router(dash_router.router)
app.include_router(xai_router.router)


@app.get("/")
def root():
    return {
        "service": "Outlet Intelligence API",
        "endpoints": [
            "GET  /health",
            "GET  /api/summary",
            "GET  /api/outlets",
            "GET  /api/outlets/filters",
            "GET  /api/outlets/{outlet_id}",
            "POST /api/xai/explain/{outlet_id}",
            "GET  /api/budget/distributors",
            "GET  /api/budget/channels",
            "GET  /api/budget/outlets",
            "GET  /api/cooler-roi/top100",
            "GET  /api/cooler-roi/summary",
            "GET  /api/dormancy/top",
            "GET  /api/dormancy/bands",
            "GET  /api/scorecard",
            "GET  /api/territories",
            "GET  /api/forensics",
            "GET  /api/shap/global",
        ],
    }


@app.get("/health")
def health():
    """Liveness probe used by the frontend on first paint."""
    import os
    cache = get_cache()
    return {
        "status": "ok",
        "outlets_loaded": int(len(cache.outlets_table)),
        "predictions_loaded": int(len(cache.predictions)),
        "gemini_configured": bool(os.environ.get("GEMINI_API_KEY")),
    }