Mise en ligne de la version 0.2.0
All checks were successful
Build & Push Images / build (brain) (push) Successful in 46s
Build & Push Images / build (core) (push) Successful in 1m21s
Build & Push Images / build (web) (push) Successful in 1m25s

This commit is contained in:
2026-04-21 14:25:17 +02:00
parent ebee8e106b
commit ba8a503b3e
300 changed files with 35329 additions and 1 deletions

View File

59
brain/app/core/config.py Normal file
View File

@@ -0,0 +1,59 @@
"""Configuration applicative centralisée (principe 12-factor : config via env).
Équivalent Python du `application.properties` Spring Boot, avec validation
Pydantic : une variable manquante/invalide = crash au démarrage, pas une
NullPointerException surprise à la 3ème requête.
Depuis l'ecran Parametres (UI) : certains champs sont surchargeables a chaud
via `settings_store` (fichier JSON). A chaque Depends(get_settings), on relit
.env + overrides fusionnes. Pas de cache : le cout d'un read JSON local est
negligeable face a un appel LLM.
"""
from typing import Literal
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.core.settings_store import load_overrides
class Settings(BaseSettings):
"""Settings chargés depuis .env ou variables d'environnement."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
# Provider LLM actif. "ollama" = local ; "onemin" = 1min.ai (etage 2).
llm_provider: Literal["ollama", "onemin"] = "ollama"
ollama_base_url: str = "http://localhost:11434"
llm_model: str = "gemma4:26b"
llm_timeout_seconds: int = 120
# Fenêtre de contexte (num_ctx Ollama). Défaut Ollama = 2048, trop étroit
# dès que le Structural Context du Lore dépasse ~10 pages (b9). On monte
# à 16384 pour tenir ~100 pages enrichies. Coût VRAM : ~600 MB de KV cache
# supplémentaire (vs 2048) pour le modèle gemma 2B. Surchargeable via
# LLM_NUM_CTX dans .env si besoin (ex: VRAM limitée → 8192).
llm_num_ctx: int = 16384
# 1min.ai (etage 2) — la cle et le modele sont stockes via settings_store
# (modifiables depuis l'UI). Les defauts ici sont juste des placeholders.
onemin_api_key: str = ""
onemin_model: str = "gpt-4o-mini"
# Secret partage entre le Core Spring et le Brain. Le Brain n'accepte une
# requete que si l'entete X-Internal-Secret correspond. Volontairement
# non-surchargeable via settings_store (securite critique, .env-only).
internal_shared_secret: str = ""
def get_settings() -> Settings:
"""Fabrique des Settings merges (.env -> overrides runtime).
Relu a chaque requete HTTP (via Depends). Permet a l'UI de changer
le modele / provider sans redemarrer le Brain.
"""
return Settings(**load_overrides())

View File

@@ -0,0 +1,61 @@
"""Overrides runtime persistés sur disque pour les Settings.
Les Settings par defaut viennent de .env (12-factor). L'utilisateur peut
surcharger certains champs depuis l'UI (ex: modele Ollama choisi) — ces
overrides sont stockes dans un fichier JSON local, relus a chaque requete.
Thread-safe via un lock simple : suffisant pour un deploiement mono-process
(usage local). Si un jour on passe en multi-worker, migrer vers SQLite.
"""
from __future__ import annotations
import json
import threading
from pathlib import Path
from typing import Any
_LOCK = threading.Lock()
_OVERRIDES_PATH = Path("data/settings.json")
# Allow-list stricte des cles persistables via l'API. Toute autre cle est
# silencieusement ignoree — empeche un appelant de polluer settings.json
# avec des champs arbitraires (ex: `internal_shared_secret`) ou d'exposer
# un vecteur SSRF/credential-swap via un champ non-documente.
_ALLOWED_KEYS = frozenset({
"llm_provider",
"ollama_base_url",
"llm_model",
"llm_timeout_seconds",
"llm_num_ctx",
"onemin_api_key",
"onemin_model",
})
def load_overrides() -> dict[str, Any]:
"""Retourne le dict d'overrides, ou {} si le fichier n'existe pas / est corrompu."""
if not _OVERRIDES_PATH.exists():
return {}
try:
raw = json.loads(_OVERRIDES_PATH.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
if not isinstance(raw, dict):
return {}
# Defense en profondeur au chargement : si settings.json contient des
# cles hors allow-list (heritage d'un ancien binaire), on les ignore.
return {k: v for k, v in raw.items() if k in _ALLOWED_KEYS}
def save_overrides(patch: dict[str, Any]) -> dict[str, Any]:
"""Fusionne `patch` (cles allow-listees uniquement) et persiste."""
filtered = {k: v for k, v in patch.items() if k in _ALLOWED_KEYS}
with _LOCK:
current = load_overrides()
current.update(filtered)
_OVERRIDES_PATH.parent.mkdir(parents=True, exist_ok=True)
_OVERRIDES_PATH.write_text(
json.dumps(current, indent=2, ensure_ascii=False),
encoding="utf-8",
)
return current