Updated backend

This commit is contained in:
sriram
2026-08-12 16:37:28 +05:30
parent eb3567df62
commit ac8cfacbc9
21 changed files with 1496 additions and 155 deletions

View File

@@ -10,9 +10,10 @@ from __future__ import annotations
import logging
import threading
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
@@ -31,19 +32,17 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
app = FastAPI(
title="Brand Product Search Engine - RAG API",
description=(
"Local, CPU-only RAG API over an Indian FMCG product catalog stored in pgvector. "
"Includes automated multi-store intelligence, ML discount engines, and nutrition intelligence."
),
version="3.2.0",
)
@asynccontextmanager
async def lifespan(_app: FastAPI):
"""
Schema checks and auto-seeding, kicked off without blocking startup.
The work runs on a daemon thread rather than being awaited: it touches
Postgres, which may be slow or briefly unreachable on a cold boot, and the
server answering /api/health in under a second is what lets the container
healthcheck pass while that settles.
"""
@app.on_event("startup")
def _on_startup() -> None:
# Asynchronously ensure schemas and background auto-init so server starts instantly (<1s)
def _async_init():
try:
ensure_store_intelligence_schema()
@@ -54,12 +53,42 @@ def _on_startup() -> None:
logger.warning("Startup background init warning: %s", e)
threading.Thread(target=_async_init, daemon=True).start()
yield
app = FastAPI(
title="Brand Product Search Engine - RAG API",
description=(
"Local, CPU-only RAG API over an Indian FMCG product catalog stored in pgvector. "
"Includes automated multi-store intelligence, ML discount engines, and nutrition intelligence."
),
version="3.2.0",
lifespan=lifespan,
)
# When the app is served same-origin (the frontend's nginx proxies /api/* to
# this service), API_CORS_ORIGINS is empty and no cross-origin request is ever
# made. It is populated only when the API is also published on its own host -
# see api.{$DOMAIN} in the Caddyfile.
#
# A wildcard origin and credentialed requests are mutually exclusive under the
# CORS spec: browsers reject `Access-Control-Allow-Origin: *` on any request
# carrying credentials. Sending both is a silent misconfiguration - the server
# looks configured while every browser call fails - so a wildcard turns
# credentials off explicitly and says so in the log.
_allow_credentials = "*" not in API_CORS_ORIGINS
if not _allow_credentials:
logger.warning(
"API_CORS_ORIGINS contains '*': disabling allow_credentials, because "
"browsers reject credentialed cross-origin requests to a wildcard origin. "
"List the exact origins instead if you need cookies or Authorization headers."
)
app.add_middleware(
CORSMiddleware,
allow_origins=API_CORS_ORIGINS,
allow_credentials=True,
allow_credentials=_allow_credentials,
allow_methods=["*"],
allow_headers=["*"],
)
@@ -90,8 +119,14 @@ if FRONTEND_DIST.exists() and (FRONTEND_DIST / "assets").exists():
@app.get("/{full_path:path}")
def serve_frontend(full_path: str):
if full_path.startswith("api") or full_path.startswith("docs") or full_path.startswith("openapi.json"):
return None
# API and docs paths are served by the routers registered above, so
# this catch-all only sees them when the path genuinely doesn't exist.
# Returning None there would answer 200 with a `null` body - an unknown
# endpoint would look like a successful call to any client. 404 is the
# honest answer, and it matters now that the API is also reachable
# directly at api.{$DOMAIN} rather than only behind the frontend.
if full_path.startswith(("api", "docs", "redoc", "openapi.json")):
raise HTTPException(status_code=404, detail="Not found")
file_path = FRONTEND_DIST / full_path
if file_path.exists() and file_path.is_file():
return FileResponse(file_path)