import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, Response import logging from app.logging_config import configure_logging configure_logging() # Must be called before any logger is created from app.config import settings from app.routers import auth, chatbots, documents, chat, marketplace, billing, models, analytics, inbox, leads, upload from app.routers.documents import router_url_sources from app.routers.leads import leads_public_router from app.routers.channels import router as channels_router, webhook_router as channels_webhook_router from app.routers import admin as admin_router from app.routers.appointments import router as appointments_router, public_router as appointments_public_router from app.routers.campaigns import router as campaigns_router logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Contexta API starting up...") logger.info(f"Environment: {settings.app_env}") logger.info(f"Allowed origins: {settings.allowed_origins_list}") asyncio.create_task(_requeue_pending_url_sources()) yield logger.info("Contexta API shutting down...") async def _requeue_pending_url_sources(): """Re-queue any url_sources stuck in pending/processing from a previous crash.""" try: from app.database import get_supabase from app.routers.documents import _process_url_source supabase = get_supabase() stuck = supabase.table("url_sources") \ .select("id, url, chatbot_id") \ .in_("status", ["pending", "processing"]) \ .execute() if not stuck.data: return logger.info(f"Re-queuing {len(stuck.data)} stuck URL source(s) from previous run") for src in stuck.data: asyncio.create_task(_process_url_source(src["id"], src["url"], src["chatbot_id"], supabase)) except Exception as e: logger.warning(f"Failed to re-queue pending URL sources: {e}") # ── App ────────────────────────────────────────────────────────────────────────── app = FastAPI( title="Contexta API", description="AI Chatbot Platform - Create, deploy and share custom AI chatbots powered by your data", version="1.0.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, ) # ── Middleware ───────────────────────────────────────────────────────────────── app.add_middleware( CORSMiddleware, allow_origins=settings.allowed_origins_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ── Routers ──────────────────────────────────────────────────────────────────── app.include_router(auth.router, prefix="/api/v1") app.include_router(chatbots.router, prefix="/api/v1") app.include_router(documents.router, prefix="/api/v1") app.include_router(chat.router, prefix="/api/v1") app.include_router(marketplace.router, prefix="/api/v1") app.include_router(billing.router, prefix="/api/v1") app.include_router(models.router, prefix="/api/v1") app.include_router(analytics.router, prefix="/api/v1") app.include_router(inbox.router, prefix="/api/v1") app.include_router(leads.router, prefix="/api/v1") app.include_router(upload.router, prefix="/api/v1") app.include_router(router_url_sources, prefix="/api/v1") app.include_router(leads_public_router, prefix="/api/v1") app.include_router(channels_router, prefix="/api/v1") app.include_router(channels_webhook_router, prefix="/api/v1") app.include_router(appointments_router, prefix="/api/v1") app.include_router(appointments_public_router, prefix="/api/v1") app.include_router(campaigns_router, prefix="/api/v1") app.include_router(admin_router.router, prefix="/api/v1") # ── Widget ───────────────────────────────────────────────────────────────────── @app.get("/widget.js", include_in_schema=False) async def serve_widget(): from app.services.widget import generate_widget_js return Response( content=generate_widget_js(settings.app_url), media_type="application/javascript", headers={ # Allow any site to load this script tag cross-origin "Access-Control-Allow-Origin": "*", # Cache for 1 hour in browsers / CDN; revalidate when stale "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400", # Prevent MIME sniffing "X-Content-Type-Options": "nosniff", }, ) # ── Health & Info ────────────────────────────────────────────────────────────── @app.get("/") async def root(): return { "name": "Contexta API", "version": "1.0.0", "status": "running", "docs": "/docs", } @app.get("/health") async def health(): return {"status": "healthy", "environment": settings.app_env} # ── Prometheus Metrics ────────────────────────────────────────────────────────── try: from prometheus_fastapi_instrumentator import Instrumentator Instrumentator().instrument(app).expose(app, endpoint="/metrics", include_in_schema=False) logger.info("Prometheus metrics enabled at /metrics") except ImportError: logger.info("prometheus-fastapi-instrumentator not installed, metrics endpoint disabled") # ── Sentry ───────────────────────────────────────────────────────────────────── if settings.sentry_dsn: import sentry_sdk sentry_sdk.init(dsn=settings.sentry_dsn, traces_sample_rate=0.1) logger.info("Sentry initialized") if __name__ == "__main__": import uvicorn uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)