Files
contexta_be/app/main.py
2026-05-27 23:17:34 +00:00

145 lines
6.5 KiB
Python

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)