File size: 3,168 Bytes
07c2476
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""FastAPI app factory.



Mounts all routers under ``/api/*`` plus (in production) serves the built

frontend bundle from ``FRONTEND_DIST_DIR`` (default ``src/frontend/dist``).

"""

from __future__ import annotations

import logging
import os
from pathlib import Path

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles

from ai_agent.agent.tools import ensure_tools_registered
from ai_agent.api.deps import get_pipeline
from ai_agent.api.routers import auth, catalog, chat, files, health, models

log = logging.getLogger("api.server")


def create_app() -> FastAPI:
    app = FastAPI(
        title="AI Imaging Agent",
        version="2.0.0",
        docs_url="/api/docs",
        openapi_url="/api/openapi.json",
    )

    # CORS — permissive in dev so Vite (5173) can call :8000. In prod the
    # frontend is served from the same origin, so this is a no-op.
    dev_origins = os.getenv(
        "DEV_CORS_ORIGINS",
        "http://localhost:5173,http://127.0.0.1:5173",
    ).split(",")
    app.add_middleware(
        CORSMiddleware,
        allow_origins=[o.strip() for o in dev_origins if o.strip()],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    app.include_router(health.router)
    app.include_router(auth.router)
    app.include_router(models.router)
    app.include_router(catalog.router)
    app.include_router(files.router)
    app.include_router(chat.router)

    @app.on_event("startup")
    async def _startup() -> None:
        ensure_tools_registered()
        # Touch the pipeline once so first request doesn't pay the boot cost.
        try:
            pipe = get_pipeline()
            log.info("Pipeline ready with %d docs", len(pipe.index.docs))
        except Exception:
            log.exception("Pipeline initialization failed")

    # Production: serve the React bundle. In dev, Vite serves it.
    dist_dir = Path(os.getenv("FRONTEND_DIST_DIR") or "src/frontend/dist")
    if dist_dir.exists() and (dist_dir / "index.html").exists():
        # Static assets first (so JS/CSS hits return immediately)
        app.mount("/assets", StaticFiles(directory=dist_dir / "assets"), name="assets")

        @app.get("/{full_path:path}", include_in_schema=False)
        async def spa_fallback(full_path: str):
            # Never shadow /api/* — FastAPI matches routers first, but be explicit.
            if full_path.startswith("api/"):
                return JSONResponse({"detail": "not_found"}, status_code=404)
            candidate = dist_dir / full_path
            if full_path and candidate.is_file():
                return FileResponse(candidate)
            return FileResponse(dist_dir / "index.html")

        log.info("Serving frontend from %s", dist_dir)
    else:
        log.info(
            "No frontend bundle at %s — assuming dev mode (Vite on :5173)", dist_dir
        )

    return app


app = create_app()


__all__ = ["app", "create_app"]