File size: 4,577 Bytes
d2d4409
 
02427e3
0358051
d2d4409
 
bc2b07b
0358051
 
 
cc78da3
0358051
02427e3
 
49206fb
0358051
d2d4409
f7d6cd3
 
 
 
 
 
 
 
0358051
 
 
 
 
 
bc2b07b
 
 
 
 
 
 
 
 
0358051
 
 
bc2b07b
0358051
f7d6cd3
148b563
3d08a7e
b13c030
 
d2d4409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148b563
d2d4409
 
 
 
 
 
 
 
cc78da3
b07d245
 
 
 
 
d2d4409
b07d245
 
 
 
 
cc78da3
f7d6cd3
 
 
02427e3
 
 
 
f7d6cd3
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import uuid
import logging
from fastapi import FastAPI, Request, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
import app
from transparent_fastapi import install

from app.config import settings
from app.routers import auth, hubs, documents
from app.errors import AppError, error_response
from app.deps import get_current_user
from app.models import User
from app.services.groq_service import AiUnavailable

logger = logging.getLogger("uvicorn.error")

def create_app() -> FastAPI:
    app = FastAPI(
        title="ContextQuery AI",
        version="0.1.0",
        docs_url="/docs",
        redoc_url=None,
    )
    
    # Initialize transparent-fastapi for /metrics
    install(app)

    app.add_middleware(
        CORSMiddleware,
        allow_origins=[
            "http://localhost:3000",
            "http://127.0.0.1:3000",
            "http://localhost:8000",
            "http://127.0.0.1:8000",
            "http://localhost:5500",    # <-- Add this (Live Server localhost)
            "http://127.0.0.1:5500",
            "null",  # <--- THIS IS CRITICAL FOR LOCAL FILE TESTING
        ],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
        expose_headers=["X-Request-ID", "Idempotent-Replayed", "Retry-After"],
    )

    app.include_router(auth.router, prefix="/api/v1")
    app.include_router(hubs.router, prefix="/api/v1")
    app.include_router(documents.router, prefix="/api/v1")

    @app.middleware("http")
    async def catch_unhandled(request: Request, call_next):
        try:
            return await call_next(request)
        except Exception:
            logger.exception("unhandled error", extra={"request_id": getattr(request.state, "request_id", "unknown")})
            return error_response(request, 500, "INTERNAL_ERROR", "Unexpected server error.")

    @app.middleware("http")
    async def add_request_id(request: Request, call_next):
        request.state.request_id = str(uuid.uuid4())
        response = await call_next(request)
        response.headers["X-Request-ID"] = request.state.request_id
        return response

    @app.exception_handler(AppError)
    async def app_error_handler(request: Request, exc: AppError):
        return error_response(request, exc.status_code, exc.code, exc.message, exc.details, exc.headers)

    @app.exception_handler(RequestValidationError)
    async def validation_error_handler(request: Request, exc: RequestValidationError):
        details = []
        for error in exc.errors():
            loc = error.get("loc", [])
            field = ".".join(str(x) for x in loc) if loc else "unknown"
            details.append({
                "field": field,
                "reason": error.get("type", "invalid"),
                "message": error.get("msg", "Invalid value")
            })
        return error_response(request, 422, "VALIDATION_ERROR", "Request validation failed.", details=details)

    @app.exception_handler(StarletteHTTPException)
    async def http_exception_handler(request: Request, exc: StarletteHTTPException):
        if exc.status_code == 404:
            return error_response(request, 404, "ROUTE_NOT_FOUND", "The requested route does not exist.")
        if exc.status_code == 405:
            return error_response(request, 405, "METHOD_NOT_ALLOWED", "Method not allowed for this route.")
        return error_response(request, exc.status_code, "HTTP_ERROR", str(exc.detail))

    AI_ERRORS = {
        "rate_limited": (429, "AI_RATE_LIMITED", "The AI provider's free quota is used up. Try again later."),
        "timeout": (504, "AI_TIMEOUT", "The AI provider took too long to answer."),
        "provider": (502, "AI_PROVIDER_ERROR", "The AI provider returned an error."),
    }

    @app.exception_handler(AiUnavailable)
    async def ai_unavailable_handler(request: Request, exc: AiUnavailable):
        status, code, message = AI_ERRORS[exc.kind]
        headers = {"Retry-After": "30"} if exc.kind == "rate_limited" else None
        return error_response(request, status, code, message, headers=headers)

    @app.get("/healthz", tags=["system"])
    def healthz() -> dict[str, str]:
        return {"status": "ok"}
        
    @app.get("/api/v1/protected", tags=["system"])
    def protected_route(current_user: User = Depends(get_current_user)):
        return {"message": "You are authenticated!", "user_id": str(current_user.id)}

    return app

app = create_app()