"""FastAPI backend for Railway — word complexity inference.""" from __future__ import annotations import os from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from app.config import CORS_ORIGINS, MODEL_PATH from app.schemas import ( AlterComplexityRequest, AlterComplexityResponse, EfficiencyRequest, EfficiencyResponse, HealthResponse, InterveneAllRequest, InterveneAllResponse, InterveneRequest, InterveneResponse, PredictRequest, PredictResponse, ) from app.services.efficiency import compare_efficiency from app.services.interventions import run_intervention from app.services.model import get_predictor, model_status, predict @asynccontextmanager async def lifespan(_app: FastAPI): if os.environ.get("EAGER_LOAD_MODEL", "1") == "1": try: get_predictor() except RuntimeError as exc: print(f"WARN: model not loaded at startup: {exc}") yield app = FastAPI(title="Word Complexity API", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"] if os.environ.get("CORS_ALLOW_ALL") == "1" else CORS_ORIGINS, allow_credentials=os.environ.get("CORS_ALLOW_ALL") != "1", allow_methods=["*"], allow_headers=["*"], ) @app.get("/") def root(): ready, err = model_status() return { "service": "Word Complexity API", "health": "/health", "docs": "/docs", "model_ready": ready, "model_error": err, } @app.get("/health", response_model=HealthResponse) def health(): ready, err = model_status() return HealthResponse( status="ok" if ready else "degraded", model_path=str(MODEL_PATH), model_ready=ready, model_error=err, ) @app.post("/api/predict", response_model=PredictResponse) def api_predict(body: PredictRequest): try: data, latency_ms, target_in = predict(body.sentence.strip(), body.target_word.strip()) except RuntimeError as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc return PredictResponse(**data, latency_ms=latency_ms, target_in_sentence=target_in) @app.post("/api/intervene", response_model=InterveneResponse) def api_intervene(body: InterveneRequest): try: predictor = get_predictor() detail = run_intervention( predictor, body.sentence.strip(), body.target_word.strip(), body.reason, body.use_llm, ) except RuntimeError as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return InterveneResponse(intervention=detail) @app.post("/api/intervene/all", response_model=InterveneAllResponse) def api_intervene_all(body: InterveneAllRequest): try: predictor = get_predictor() base = predictor.predict(body.sentence.strip(), body.target_word.strip()) from utils import REASON_ORDER results = [ run_intervention( predictor, body.sentence.strip(), body.target_word.strip(), reason, body.use_llm, predicted_reason=body.predicted_reason, ) for reason in REASON_ORDER ] except RuntimeError as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc best = max(results, key=lambda r: r.hardness_drop) faithful = None if body.predicted_reason: faithful = best.reason == body.predicted_reason return InterveneAllResponse( base_level=base.complexity_level, results=results, best_reason=best.reason, faithful=faithful, ) @app.post("/api/efficiency", response_model=EfficiencyResponse) def api_efficiency(body: EfficiencyRequest): return compare_efficiency(body.sentence.strip(), body.target_word.strip(), body.local_latency_ms) @app.post("/api/alter", response_model=AlterComplexityResponse) def api_alter(body: AlterComplexityRequest): from reason_interventions import alter_complexity try: result = alter_complexity( body.reason, body.sentence.strip(), body.target_word.strip(), body.direction, ) except RuntimeError as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc if result.reason == "Lexical Rarity": updates = "Updated sentence and target word above — click Predict again." else: updates = "Updated sentence above (target word unchanged) — click Predict again." return AlterComplexityResponse( reason=result.reason, direction=result.direction, original_sentence=result.original_sentence, original_target_word=result.original_target, new_sentence=result.new_sentence, new_target_word=result.new_target, edit_method=result.edit_method, explanation=result.explanation, updates=updates, )