mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 12:46:56 +02:00
Phase 6 : configuration de l'agent fonctionnelle
Backend : - agent_config.py : profil persisté (invite système, génération, outils actifs) avec valeurs par défaut et fusion robuste - routes/config.py : GET/PUT /api/config - db.py : table config clé/valeur (get/set JSON) - agent.py : run_agent filtre les outils selon enabled_tools - routes/chat.py : utilise l'invite système, les options de génération et les outils actifs issus de la config sauvegardée Frontend : - api/client.ts + store : getConfig/saveConfig, état config - SettingsView : sliders interactifs (température, top-p, top-k, jetons max), toggles d'outils réels, éditeur d'invite système, bouton Enregistrer - ChatPanel : la barre de contexte reflète température et outils actifs réels Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
1 parent
b6bf4edf97
commit
2db6beea5c
11 files changed
+499
-206
No files matched your search
+10
-2
@@ -40,9 +40,17 @@ async def run_agent(
|
||||
convo: list[dict],
|
||||
*,
|
||||
options: dict | None = None,
|
||||
tools_enabled: bool = True,
|
||||
enabled_tools: list[str] | None = None,
|
||||
) -> AsyncIterator[dict]:
|
||||
tools = TOOL_DEFINITIONS if tools_enabled else None
|
||||
# enabled_tools=None -> tous les outils ; liste vide -> aucun outil.
|
||||
if enabled_tools is None:
|
||||
tools = TOOL_DEFINITIONS
|
||||
elif enabled_tools:
|
||||
tools = [
|
||||
t for t in TOOL_DEFINITIONS if t["function"]["name"] in enabled_tools
|
||||
]
|
||||
else:
|
||||
tools = None
|
||||
collected: list[dict] = []
|
||||
text_parts: list[str] = []
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Profil de configuration de l'agent : invite système, génération, outils.
|
||||
|
||||
Persisté en base sous la clé `agent`. Fournit les valeurs par défaut et la
|
||||
fusion avec ce qui est stocké, pour rester robuste aux montées de version.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import db
|
||||
|
||||
CONFIG_KEY = "agent"
|
||||
|
||||
DEFAULT_SYSTEM_PROMPT = (
|
||||
"Tu es Loki, un assistant de développement local agentique. Tu disposes "
|
||||
"d'outils pour lire, écrire et lister des fichiers dans le workspace. "
|
||||
"Utilise-les pour accomplir les tâches concrètement, puis réponds de façon "
|
||||
"concise en français. Après avoir écrit un fichier, propose un aperçu."
|
||||
)
|
||||
|
||||
# Outils réellement disponibles (les sensibles arrivent en phase 7).
|
||||
AVAILABLE_TOOLS = ["read_file", "write_file", "list_dir"]
|
||||
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"system_prompt": DEFAULT_SYSTEM_PROMPT,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.9,
|
||||
"top_k": 40,
|
||||
"max_tokens": 2048,
|
||||
"tools": {name: True for name in AVAILABLE_TOOLS},
|
||||
}
|
||||
|
||||
|
||||
def get_config() -> dict:
|
||||
"""Config courante = défauts fusionnés avec le stockage."""
|
||||
stored = db.get_config_value(CONFIG_KEY) or {}
|
||||
cfg = {**DEFAULT_CONFIG, **stored}
|
||||
cfg["tools"] = {
|
||||
name: bool(stored.get("tools", {}).get(name, True))
|
||||
for name in AVAILABLE_TOOLS
|
||||
}
|
||||
return cfg
|
||||
|
||||
|
||||
def save_config(patch: dict) -> dict:
|
||||
"""Applique une mise à jour partielle et renvoie la config complète."""
|
||||
cfg = {**get_config(), **{k: v for k, v in patch.items() if v is not None}}
|
||||
if "tools" in patch and patch["tools"]:
|
||||
cfg["tools"] = {
|
||||
name: bool(patch["tools"].get(name, cfg["tools"].get(name, True)))
|
||||
for name in AVAILABLE_TOOLS
|
||||
}
|
||||
db.set_config_value(CONFIG_KEY, cfg)
|
||||
return get_config()
|
||||
|
||||
|
||||
def ollama_options(cfg: dict) -> dict:
|
||||
"""Traduit la config en options de génération Ollama."""
|
||||
return {
|
||||
"temperature": cfg["temperature"],
|
||||
"top_p": cfg["top_p"],
|
||||
"top_k": cfg["top_k"],
|
||||
"num_predict": cfg["max_tokens"],
|
||||
}
|
||||
|
||||
|
||||
def enabled_tool_names(cfg: dict) -> list[str]:
|
||||
return [name for name, on in cfg["tools"].items() if on]
|
||||
@@ -54,6 +54,10 @@ def init_db() -> None:
|
||||
cols = {r["name"] for r in conn.execute("PRAGMA table_info(messages)")}
|
||||
if "meta" not in cols:
|
||||
conn.execute("ALTER TABLE messages ADD COLUMN meta TEXT")
|
||||
# Table clé/valeur pour la configuration de l'agent.
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)"
|
||||
)
|
||||
|
||||
|
||||
def _now() -> float:
|
||||
@@ -160,3 +164,22 @@ def list_messages_for_model(sid: str) -> list[dict]:
|
||||
{"role": m["role"], "content": m["content"]}
|
||||
for m in list_messages(sid)
|
||||
]
|
||||
|
||||
|
||||
# ── Configuration (clé/valeur JSON) ──────────────────────────────────────
|
||||
def get_config_value(key: str) -> dict | None:
|
||||
with _LOCK, _connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT value FROM config WHERE key = ?", (key,)
|
||||
).fetchone()
|
||||
return json.loads(row["value"]) if row else None
|
||||
|
||||
|
||||
def set_config_value(key: str, value: dict) -> None:
|
||||
payload = json.dumps(value, ensure_ascii=False)
|
||||
with _LOCK, _connect() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO config (key, value) VALUES (?, ?)"
|
||||
" ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
(key, payload),
|
||||
)
|
||||
+2
-1
@@ -14,7 +14,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import db
|
||||
from .config import settings
|
||||
from .routes import chat, files, models, sessions
|
||||
from .routes import chat, config, files, models, sessions
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -40,6 +40,7 @@ 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.get("/api/health")
|
||||
|
||||
@@ -14,26 +14,17 @@ from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .. import db
|
||||
from .. import agent_config, db
|
||||
from ..agent import run_agent
|
||||
from ..config import settings
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["chat"])
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"Tu es Loki, un assistant de développement local agentique. Tu disposes "
|
||||
"d'outils pour lire, écrire et lister des fichiers dans le workspace. "
|
||||
"Utilise-les pour accomplir les tâches concrètement, puis réponds de façon "
|
||||
"concise en français. Après avoir écrit un fichier, propose un aperçu."
|
||||
)
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
session_id: str
|
||||
content: str
|
||||
model: str | None = None
|
||||
options: dict | None = None
|
||||
tools_enabled: bool = True
|
||||
|
||||
|
||||
def _sse(event: str, data: dict) -> str:
|
||||
@@ -47,6 +38,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
||||
raise HTTPException(404, "session introuvable")
|
||||
|
||||
model = req.model or session.get("model") or settings.default_model
|
||||
cfg = agent_config.get_config()
|
||||
|
||||
# Premier message : titre la session avec un extrait.
|
||||
if not db.list_messages(req.session_id):
|
||||
@@ -55,7 +47,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
||||
|
||||
db.add_message(req.session_id, "user", req.content, None)
|
||||
|
||||
convo = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
convo = [{"role": "system", "content": cfg["system_prompt"]}]
|
||||
convo += db.list_messages_for_model(req.session_id)
|
||||
|
||||
async def event_stream():
|
||||
@@ -64,7 +56,10 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
||||
tools_meta: list[dict] = []
|
||||
|
||||
async for ev in run_agent(
|
||||
model, convo, options=req.options, tools_enabled=req.tools_enabled
|
||||
model,
|
||||
convo,
|
||||
options=agent_config.ollama_options(cfg),
|
||||
enabled_tools=agent_config.enabled_tool_names(cfg),
|
||||
):
|
||||
etype = ev.pop("type")
|
||||
if etype == "token":
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Routes de configuration de l'agent (lecture / mise à jour)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .. import agent_config
|
||||
|
||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||
|
||||
|
||||
class ConfigPatch(BaseModel):
|
||||
system_prompt: str | None = None
|
||||
temperature: float | None = None
|
||||
top_p: float | None = None
|
||||
top_k: int | None = None
|
||||
max_tokens: int | None = None
|
||||
tools: dict[str, bool] | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_config() -> dict:
|
||||
return {
|
||||
"config": agent_config.get_config(),
|
||||
"available_tools": agent_config.AVAILABLE_TOOLS,
|
||||
}
|
||||
|
||||
|
||||
@router.put("")
|
||||
async def put_config(patch: ConfigPatch) -> dict:
|
||||
cfg = agent_config.save_config(patch.model_dump(exclude_none=True))
|
||||
return {"config": cfg}
|
||||
Reference in new issue
Block a user