Spaces:
Runtime error
Runtime error
| """FastAPI application for the template-agnostic v2 backend.""" | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from fastapi import FastAPI, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from backend.config import settings | |
| from backend.core import pii_scrubber, template_discoverer | |
| from backend.core.rag_store import TIER_MASTER, TIER_REFERENCE, get_rag_store | |
| from backend.startup import run_startup_ingest | |
| from backend.utils.runtime_paths import ensure_data_drive_runtime_dirs | |
| ensure_data_drive_runtime_dirs() | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| _FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend" | |
| _STARTUP_SUMMARY: dict = {} | |
| _PII_SCAN_PATHS = {"/api/report/generate", "/api/report/preview"} | |
| async def lifespan(app: FastAPI): | |
| global _STARTUP_SUMMARY | |
| _STARTUP_SUMMARY = run_startup_ingest() | |
| from backend.core import document_library | |
| recovered = document_library.recover_all_tenants_stale_processing() | |
| if recovered: | |
| logger.info("Startup recovered %d stale processing document(s)", recovered) | |
| yield | |
| app = FastAPI(title="RICS Template-Agnostic Backend (v2)", version="2.0.0", lifespan=lifespan) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def pii_request_middleware(request: Request, call_next): | |
| """Log PII in inbound notes for awareness (notes are not scrubbed; RAG docs are).""" | |
| if request.method == "POST" and request.url.path in _PII_SCAN_PATHS: | |
| body = await request.body() | |
| async def _receive(): | |
| return {"type": "http.request", "body": body, "more_body": False} | |
| request._receive = _receive # re-inject consumed body | |
| try: | |
| raw_notes = json.loads(body or b"{}").get("raw_notes", "") | |
| audit = pii_scrubber.detect_pii(raw_notes) | |
| flagged = {k: v for k, v in audit.items() if k in ("EMAIL", "POSTCODE")} | |
| if flagged: | |
| logger.warning( | |
| "Inbound notes contain PII (notes kept verbatim; scrub RAG uploads only): %s", | |
| flagged, | |
| ) | |
| except Exception: # noqa: BLE001 | |
| pass | |
| response = await call_next(request) | |
| return response | |
| # Routers | |
| from backend.api.routes import admin, auth, compat, photos, report, schema, upload # noqa: E402 | |
| app.include_router(auth.router) | |
| app.include_router(compat.router) | |
| app.include_router(schema.router) | |
| app.include_router(report.router) | |
| app.include_router(photos.router) | |
| app.include_router(upload.router) | |
| if settings.master_template_upload_enabled: | |
| app.include_router(admin.router) | |
| def health() -> dict: | |
| tenant = settings.default_tenant_id | |
| loaded = template_discoverer.load_schema(tenant) is not None | |
| store = get_rag_store() | |
| master_faiss_count = store.count(tenant, TIER_MASTER) | |
| reference_faiss_count = store.count(tenant, TIER_REFERENCE) | |
| # Reference-only model: readiness depends on the (canonical) schema being | |
| # installed, not on a shared master being seeded. When the operator master is | |
| # explicitly enabled, its chunks must be present for an "ok" status. | |
| master_ready = (not settings.master_template_auto_ingest) or master_faiss_count > 0 | |
| return { | |
| "status": "ok" if loaded and master_ready else "degraded", | |
| "master_loaded": loaded, | |
| "master_faiss_count": master_faiss_count, | |
| "reference_faiss_count": reference_faiss_count, | |
| "reference_ready": reference_faiss_count > 0, | |
| "startup": _STARTUP_SUMMARY, | |
| "admin_override_enabled": settings.master_template_upload_enabled, | |
| "ui_path": "/", | |
| } | |
| if _FRONTEND_DIR.is_dir(): | |
| app.mount("/static", StaticFiles(directory=str(_FRONTEND_DIR)), name="static") | |
| def root_ui() -> FileResponse: | |
| index = _FRONTEND_DIR / "index.html" | |
| if index.is_file(): | |
| return FileResponse(index) | |
| return FileResponse(_FRONTEND_DIR / "v2.html") | |
| def v2_ui() -> FileResponse: | |
| return FileResponse(_FRONTEND_DIR / "v2.html") | |
| def legacy_index() -> FileResponse: | |
| return FileResponse(_FRONTEND_DIR / "index.html") | |
| def favicon() -> FileResponse: | |
| path = _FRONTEND_DIR / "favicon.ico" | |
| if path.is_file(): | |
| return FileResponse(path) | |
| return FileResponse(_FRONTEND_DIR / "v2.html") | |