Mise en ligne de la version 0.2.0
This commit is contained in:
0
brain/app/core/__init__.py
Normal file
0
brain/app/core/__init__.py
Normal file
59
brain/app/core/config.py
Normal file
59
brain/app/core/config.py
Normal 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())
|
||||
61
brain/app/core/settings_store.py
Normal file
61
brain/app/core/settings_store.py
Normal 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
|
||||
Reference in New Issue
Block a user