feat: add settings service and config API routes
Add a key-value settings table (Setting model) with DB > env > default resolution chain. Replace hardcoded Ollama constants in ollama_service with dynamic _get_config() lookups. Add /api/config/ endpoints for settings CRUD and tag management (create, rename). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,21 +2,31 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
from .sql_executor import execute_readonly_sql, get_schema_description
|
||||
from .settings_service import get_setting
|
||||
from ..database.connection import get_session_factory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration via variables d'environnement
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "qwen2.5")
|
||||
OLLAMA_TIMEOUT = int(os.environ.get("OLLAMA_TIMEOUT", "120"))
|
||||
|
||||
MAX_TOOL_ITERATIONS = 10
|
||||
|
||||
|
||||
def _get_config() -> dict:
|
||||
"""Récupère la config Ollama via le service settings (DB > env > default)."""
|
||||
SessionLocal = get_session_factory()
|
||||
session = SessionLocal()
|
||||
try:
|
||||
return {
|
||||
"url": get_setting(session, "ollama_url"),
|
||||
"model": get_setting(session, "ollama_model"),
|
||||
"timeout": int(get_setting(session, "ollama_timeout")),
|
||||
}
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# Définition des tools pour l'API Ollama
|
||||
TOOLS = [
|
||||
{
|
||||
@@ -185,17 +195,18 @@ async def check_ollama_health() -> dict:
|
||||
Returns:
|
||||
dict avec ollama_available (bool) et model (str).
|
||||
"""
|
||||
config = _get_config()
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
resp = await client.get(f"{OLLAMA_URL}/api/tags")
|
||||
resp = await client.get(f"{config['url']}/api/tags")
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
models = [m.get("name", "") for m in data.get("models", [])]
|
||||
available = any(OLLAMA_MODEL in m for m in models)
|
||||
return {"ollama_available": available, "model": OLLAMA_MODEL}
|
||||
available = any(config["model"] in m for m in models)
|
||||
return {"ollama_available": available, "model": config["model"]}
|
||||
except Exception:
|
||||
pass
|
||||
return {"ollama_available": False, "model": OLLAMA_MODEL}
|
||||
return {"ollama_available": False, "model": config["model"]}
|
||||
|
||||
|
||||
async def ask_ollama(question: str, chart_type: str = "auto") -> dict:
|
||||
@@ -216,22 +227,24 @@ async def ask_ollama(question: str, chart_type: str = "auto") -> dict:
|
||||
if chart_type != "auto":
|
||||
user_content += f"\n\nIMPORTANT : Utilise obligatoirement un graphique de type '{chart_type}' (ne choisis pas un autre type)."
|
||||
|
||||
config = _get_config()
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_content},
|
||||
]
|
||||
|
||||
async with httpx.AsyncClient(timeout=OLLAMA_TIMEOUT) as client:
|
||||
async with httpx.AsyncClient(timeout=config["timeout"]) as client:
|
||||
for iteration in range(MAX_TOOL_ITERATIONS):
|
||||
payload = {
|
||||
"model": OLLAMA_MODEL,
|
||||
"model": config["model"],
|
||||
"messages": messages,
|
||||
"tools": TOOLS,
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
logger.info(f"Ollama iteration {iteration + 1}/{MAX_TOOL_ITERATIONS}")
|
||||
resp = await client.post(f"{OLLAMA_URL}/api/chat", json=payload)
|
||||
resp = await client.post(f"{config['url']}/api/chat", json=payload)
|
||||
resp.raise_for_status()
|
||||
|
||||
data = resp.json()
|
||||
|
||||
103
src/plesna_gerance/services/settings_service.py
Normal file
103
src/plesna_gerance/services/settings_service.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Service de gestion des paramètres de configuration.
|
||||
|
||||
Chaîne de résolution : valeur DB > variable d'environnement > valeur par défaut.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database.models import Setting
|
||||
|
||||
# Registry: key -> (env_var, default_value)
|
||||
SETTINGS_REGISTRY: dict[str, tuple[str, str]] = {
|
||||
"ollama_url": ("OLLAMA_URL", "http://localhost:11434"),
|
||||
"ollama_model": ("OLLAMA_MODEL", "qwen2.5"),
|
||||
"ollama_timeout": ("OLLAMA_TIMEOUT", "120"),
|
||||
}
|
||||
|
||||
|
||||
def get_setting(session: Session, key: str) -> str:
|
||||
"""Résout la valeur d'un setting : DB > env > default.
|
||||
|
||||
Raises:
|
||||
KeyError: si la clé n'est pas dans le registry.
|
||||
"""
|
||||
if key not in SETTINGS_REGISTRY:
|
||||
raise KeyError(f"Setting inconnu : {key}")
|
||||
|
||||
env_var, default = SETTINGS_REGISTRY[key]
|
||||
|
||||
# 1. Check DB
|
||||
row = session.query(Setting).filter(Setting.key == key).first()
|
||||
if row is not None:
|
||||
return row.value
|
||||
|
||||
# 2. Check env var
|
||||
env_value = os.environ.get(env_var)
|
||||
if env_value is not None:
|
||||
return env_value
|
||||
|
||||
# 3. Default
|
||||
return default
|
||||
|
||||
|
||||
def get_all_settings(session: Session) -> dict[str, dict]:
|
||||
"""Retourne tous les settings avec valeur et source.
|
||||
|
||||
Returns:
|
||||
dict de la forme {key: {"value": str, "source": "database"|"env"|"default"}}
|
||||
"""
|
||||
db_rows = {row.key: row.value for row in session.query(Setting).all()}
|
||||
result = {}
|
||||
|
||||
for key, (env_var, default) in SETTINGS_REGISTRY.items():
|
||||
if key in db_rows:
|
||||
result[key] = {"value": db_rows[key], "source": "database"}
|
||||
elif os.environ.get(env_var) is not None:
|
||||
result[key] = {"value": os.environ[env_var], "source": "env"}
|
||||
else:
|
||||
result[key] = {"value": default, "source": "default"}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def set_setting(session: Session, key: str, value: str) -> Setting:
|
||||
"""Upsert un setting en base.
|
||||
|
||||
Raises:
|
||||
KeyError: si la clé n'est pas dans le registry.
|
||||
"""
|
||||
if key not in SETTINGS_REGISTRY:
|
||||
raise KeyError(f"Setting inconnu : {key}")
|
||||
|
||||
row = session.query(Setting).filter(Setting.key == key).first()
|
||||
if row is not None:
|
||||
row.value = value
|
||||
else:
|
||||
row = Setting(key=key, value=value)
|
||||
session.add(row)
|
||||
|
||||
session.commit()
|
||||
session.refresh(row)
|
||||
return row
|
||||
|
||||
|
||||
def delete_setting(session: Session, key: str) -> bool:
|
||||
"""Supprime l'override DB pour un setting (retour au défaut).
|
||||
|
||||
Returns:
|
||||
True si un override existait, False sinon.
|
||||
|
||||
Raises:
|
||||
KeyError: si la clé n'est pas dans le registry.
|
||||
"""
|
||||
if key not in SETTINGS_REGISTRY:
|
||||
raise KeyError(f"Setting inconnu : {key}")
|
||||
|
||||
row = session.query(Setting).filter(Setting.key == key).first()
|
||||
if row is not None:
|
||||
session.delete(row)
|
||||
session.commit()
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user