Files
Loki/backend/app/main.py
T
MichaelandClaude Opus 4.8 7d1fa8913d fix(web): servir le favicon en production
Le fallback SPA renvoyait index.html pour /favicon.svg : l'icône
n'apparaissait jamais. Les fichiers statiques racine existants sont
maintenant servis tels quels, le reste retombe sur index.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:01:43 +02:00

123 lines
3.8 KiB
Python

"""Point d'entrée FastAPI de Loki.
Sert l'API (/api/*) et, en production, le frontend React compilé (static/).
"""
from __future__ import annotations
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from . import agent_config, coder, db, rag
from .config import settings
from .ollama_client import ollama
from .routes import (
benchmark, chat, config, files, git, mcp, models, projects, sessions,
shell, system,
)
async def _warm_default_model() -> None:
"""Précharge le modèle par défaut en VRAM au démarrage (best-effort)."""
import asyncio
import logging
await asyncio.sleep(2) # laisse le service démarrer
try:
cfg = agent_config.get_config(settings.default_model)
models.start_model_warm(
settings.default_model, cfg.get("keep_alive", "30m")
)
logging.getLogger(__name__).info(
"Préchargement du modèle %s lancé", settings.default_model
)
except Exception as exc: # best-effort
logging.getLogger(__name__).info(
"Préchargement au démarrage ignoré : %s", exc
)
@asynccontextmanager
async def lifespan(_: FastAPI):
import asyncio
db.init_db()
rag.init_table()
# Workspace en dépôt git : requis pour les commits du moteur code (Aider).
coder.ensure_git(settings.workspace_dir)
# Préchargement du modèle par défaut, sans bloquer le démarrage.
asyncio.create_task(_warm_default_model())
yield
# Ferme le pool HTTP partagé vers Ollama et les sessions MCP.
await ollama.aclose()
from .mcp_client import manager as mcp_manager
await mcp_manager.aclose()
app = FastAPI(
title="Loki", description="Agent IA local sur Ollama", lifespan=lifespan
)
# En dev, le front tourne sur Vite (5173). On autorise le CORS large ;
# en prod le front est servi par le même origin, donc sans impact.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(models.router)
app.include_router(sessions.router)
app.include_router(chat.router)
app.include_router(files.router)
app.include_router(config.router)
app.include_router(shell.router)
app.include_router(system.router)
app.include_router(benchmark.router)
app.include_router(git.router)
app.include_router(mcp.router)
app.include_router(projects.router)
@app.get("/api/health")
async def health() -> dict:
return {"status": "ok", "service": "loki", "version": settings.loki_version}
@app.get("/api/version")
async def version() -> dict:
"""Marqueur de build — pour vérifier que l'image déployée est à jour."""
return {"version": settings.loki_version}
# ── Service du frontend compilé (présent uniquement en image Docker) ─────
_STATIC_DIR = os.path.join(os.path.dirname(__file__), "..", "static")
if os.path.isdir(_STATIC_DIR):
app.mount(
"/assets",
StaticFiles(directory=os.path.join(_STATIC_DIR, "assets")),
name="assets",
)
@app.get("/{full_path:path}")
async def spa_fallback(full_path: str):
"""Fichier statique racine s'il existe (favicon…), sinon index.html.
Sans ce test, /favicon.svg recevait index.html : aucun favicon ne
s'affichait en production.
"""
static_root = os.path.abspath(_STATIC_DIR)
candidate = os.path.abspath(os.path.join(static_root, full_path))
if (
full_path
and candidate.startswith(static_root + os.sep)
and os.path.isfile(candidate)
):
return FileResponse(candidate)
return FileResponse(os.path.join(static_root, "index.html"))