system-prompts-and-models-o.../salesflow-saas/backend/app/main.py
Claude 0723407a6d
fix: harden Railway startup — fault-tolerant lifespan + DB retry + credential cleanup
- Wrap PostHog/DLQ init in try/except so startup survives missing services
- Delay self-improvement worker 30s to reduce startup load
- Run init_db() for ALL database types (was SQLite-only, skipping PostgreSQL)
- Add 3-attempt retry with backoff in init_db() for Railway DB startup race
- Fix FastAPI deprecation: regex → pattern in intelligence.py
- Remove hardcoded Ultramsg credentials from auto_pipeline.py

https://claude.ai/code/session_01W1rJthWDkasijTdXCfxVHs
2026-04-25 22:19:25 +00:00

163 lines
5.3 KiB
Python

# ── SQLite Patch (must be first!) ─────────────────────────────
from app.sqlite_patch import apply_patch
apply_patch()
# ──────────────────────────────────────────────────────────────
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
import asyncio
from app.config import get_settings
from app.database import IS_SQLITE, init_db
from app.api.v1.router import api_router
from app.flows.self_improvement_flow import self_improvement_flow
from app.middleware.internal_api import InternalApiTokenMiddleware
settings = get_settings()
def _cors_origins() -> list[str]:
base = [
settings.FRONTEND_URL,
"http://localhost:3000",
"http://localhost:5173",
"https://dealix.sa",
"https://app.dealix.sa",
]
extra = [x.strip() for x in (settings.CORS_EXTRA_ORIGINS or "").split(",") if x.strip()]
seen: set[str] = set()
out: list[str] = []
for o in base + extra:
if o not in seen:
seen.add(o)
out.append(o)
return out
def _openapi_urls() -> tuple[str | None, str | None, str | None]:
if not settings.EXPOSE_OPENAPI:
return None, None, None
return "/api/docs", "/api/redoc", "/api/openapi.json"
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application startup and shutdown events."""
stop_event = asyncio.Event()
async def _self_improvement_worker():
await asyncio.sleep(30)
while not stop_event.is_set():
try:
self_improvement_flow.run(
tenant_id="system_tenant",
input_state={
"signals": [],
"bottlenecks": [],
"experiments": [{"name": "always-on-ab-loop"}],
"ab_results": {},
"governance_passed": True,
"promoted": True,
},
)
except Exception:
pass
await asyncio.sleep(max(60, settings.SELF_IMPROVEMENT_INTERVAL_SECONDS))
worker_task = asyncio.create_task(_self_improvement_worker())
# Startup
print(f"[startup] {settings.APP_NAME} starting...")
print(f" Environment: {settings.ENVIRONMENT}")
print(f" LLM Primary: {settings.LLM_PRIMARY_PROVIDER}")
print(f" LLM Fallback: {settings.LLM_FALLBACK_PROVIDER}")
try:
from app.services.posthog_client import get_posthog
ph = get_posthog()
print(f" PostHog: {'enabled' if getattr(ph, '_enabled', False) else 'disabled (no API key)'}")
except Exception as e:
print(f" PostHog: init failed ({e})")
try:
from app.services.dlq import dlq # noqa: F841
print(" DLQ: initialized")
except Exception as e:
print(f" DLQ: init failed ({e})")
await init_db()
yield
# Shutdown
stop_event.set()
worker_task.cancel()
print(f"[shutdown] {settings.APP_NAME} shutting down...")
_docs, _redoc, _openapi = _openapi_urls()
app = FastAPI(
title=f"{settings.APP_NAME} API",
description=(
"AI-powered B2B Revenue Operating System for the Saudi market. "
"Lead management, AI agents, affiliate system, meeting automation, "
"deal pipeline, and commission processing — all driven by 18 specialized AI agents."
),
version="2.0.0",
docs_url=_docs,
redoc_url=_redoc,
openapi_url=_openapi,
lifespan=lifespan,
)
app.add_middleware(InternalApiTokenMiddleware)
# CORS runs outermost (added last) so browser preflight is handled first
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def root_health():
"""Root-level health check for Railway/load balancer healthchecks."""
return {"status": "ok"}
# API Routes
app.include_router(api_router, prefix="/api/v1")
# ── Static marketing assets (browse + direct download) ─────────
def _resolve_salesflow_root() -> Path:
if settings.MARKETING_STATIC_ROOT.strip():
return Path(settings.MARKETING_STATIC_ROOT).resolve()
# backend/app/main.py -> parents: app, backend, salesflow-saas
return Path(__file__).resolve().parent.parent.parent
_salesflow_root = _resolve_salesflow_root()
_marketing_dir = _salesflow_root / "sales_assets"
_presentations_dir = _salesflow_root / "presentations" / "dealix-2026-sectors"
if settings.MARKETING_STATIC_ENABLED:
if _marketing_dir.is_dir():
app.mount(
"/dealix-marketing",
StaticFiles(directory=str(_marketing_dir), html=True),
name="dealix_marketing",
)
print(" Marketing static: /dealix-marketing/ (index, ZIP, use cases)")
if _presentations_dir.is_dir():
app.mount(
"/dealix-presentations",
StaticFiles(directory=str(_presentations_dir), html=True),
name="dealix_presentations",
)
print(" Marketing static: /dealix-presentations/ (sector HTML)")