mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
- edit_file : correspondance tolérante (indentation/espaces ignorés, repli ligne à ligne + réindentation) pour que les petits modèles éditent sans devoir tout réécrire ; message d'erreur plus utile. - Prompt système : privilégier edit_file pour modifier, write_file seulement pour créer ; rappel de rester dans le workspace. - Suppression de fichiers/dossiers du workspace : route DELETE /api/files + boutons × dans l'arborescence (fichiers et dossiers). - Confinement renforcé : garde-fou run_shell (refus des chemins absolus hors workspace, ~ et remontées ../) et code_task/Aider (fnames résolus et confinés au workspace). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
161 lines
5.2 KiB
Python
161 lines
5.2 KiB
Python
"""Moteur code : enveloppe Aider pour l'édition multi-fichiers fiable.
|
|
|
|
Aider apporte ce qui fait la force de Claude Code/Codex : formats d'édition
|
|
diff/search-replace robustes (même avec de petits modèles), repo map, et
|
|
commits git automatiques dans le workspace.
|
|
|
|
L'API Python d'Aider n'étant pas officiellement stable, la version est FIGÉE
|
|
dans requirements.txt (aider-chat==0.86.2) et tout l'import est local à la
|
|
fonction (l'app démarre même si Aider est absent).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import threading
|
|
|
|
from .config import settings
|
|
|
|
# Aider utilise le répertoire courant : un seul run à la fois.
|
|
_RUN_LOCK = threading.Lock()
|
|
|
|
|
|
def _confine(root: str, files: list[str] | None) -> list[str]:
|
|
"""Résout les fichiers demandés en restant confiné au workspace.
|
|
|
|
Écarte silencieusement tout chemin absolu ou toute remontée `../` qui
|
|
sortirait du workspace : le moteur code ne doit jamais toucher au disque
|
|
hors du workspace de la discussion.
|
|
"""
|
|
confined: list[str] = []
|
|
for f in files or []:
|
|
target = os.path.abspath(os.path.join(root, f))
|
|
if target == root or target.startswith(root + os.sep):
|
|
confined.append(target)
|
|
return confined
|
|
|
|
|
|
def ensure_git(root: str) -> None:
|
|
"""Initialise un dépôt git dans le workspace (requis pour les commits Aider)."""
|
|
os.makedirs(root, exist_ok=True)
|
|
if os.path.isdir(os.path.join(root, ".git")):
|
|
return
|
|
subprocess.run(["git", "init", "-q"], cwd=root, check=False)
|
|
subprocess.run(["git", "config", "user.email", "loki@local"], cwd=root, check=False)
|
|
subprocess.run(["git", "config", "user.name", "Loki"], cwd=root, check=False)
|
|
|
|
|
|
def available() -> bool:
|
|
"""Aider est-il installé ?"""
|
|
try:
|
|
import aider # noqa: F401
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
|
|
# Familles spécialisées code, par ordre de préférence.
|
|
_CODE_MODEL_HINTS = (
|
|
"qwen3-coder", "qwen2.5-coder", "deepseek-coder", "codestral", "devstral",
|
|
"codegemma", "codellama", "starcoder", "coder",
|
|
)
|
|
|
|
|
|
async def pick_code_model(current: str, preference: str | None = None) -> str:
|
|
"""Choisit le modèle pour les tâches de code.
|
|
|
|
- préférence explicite (config code_model != "auto") -> respectée ;
|
|
- sinon, si un modèle spécialisé code est installé, on le prend (le plus
|
|
gros d'abord) : un petit modèle code bat un généraliste sur ce terrain ;
|
|
- sinon, on garde le modèle courant.
|
|
"""
|
|
if preference and preference != "auto":
|
|
return preference
|
|
|
|
from .ollama_client import ollama
|
|
try:
|
|
installed = await ollama.list_models_cached()
|
|
except Exception:
|
|
return current
|
|
|
|
candidates: list[tuple[int, int, str]] = [] # (rang_hint, -taille, nom)
|
|
for m in installed:
|
|
name = (m.get("name") or "").lower()
|
|
for rank, hint in enumerate(_CODE_MODEL_HINTS):
|
|
if hint in name:
|
|
candidates.append((rank, -(m.get("size") or 0), m["name"]))
|
|
break
|
|
|
|
if not candidates:
|
|
return current
|
|
candidates.sort()
|
|
return candidates[0][2]
|
|
|
|
|
|
def run_code_task(
|
|
instruction: str,
|
|
model: str,
|
|
files: list[str] | None = None,
|
|
root: str | None = None,
|
|
) -> dict:
|
|
"""Exécute une tâche de code via Aider (synchrone — lancer dans un thread).
|
|
|
|
Renvoie {ok, text, files, commit, commit_message, summary}.
|
|
"""
|
|
if not available():
|
|
return {
|
|
"ok": False,
|
|
"summary": "moteur code indisponible (aider non installé)",
|
|
"text": "", "files": [], "commit": None,
|
|
}
|
|
|
|
from aider.coders import Coder
|
|
from aider.io import InputOutput
|
|
from aider.models import Model
|
|
|
|
root = os.path.abspath(root or settings.workspace_dir)
|
|
ensure_git(root)
|
|
|
|
# Aider parle à Ollama via litellm : on pointe vers notre instance.
|
|
os.environ["OLLAMA_API_BASE"] = settings.ollama_host
|
|
|
|
with _RUN_LOCK:
|
|
prev_cwd = os.getcwd()
|
|
os.chdir(root)
|
|
try:
|
|
io = InputOutput(yes=True, pretty=False, fancy_input=False)
|
|
coder = Coder.create(
|
|
main_model=Model(f"ollama_chat/{model}"),
|
|
io=io,
|
|
fnames=_confine(root, files),
|
|
auto_commits=True,
|
|
stream=False,
|
|
use_git=True,
|
|
suggest_shell_commands=False,
|
|
detect_urls=False,
|
|
)
|
|
text = coder.run(instruction) or ""
|
|
edited = sorted(coder.aider_edited_files or [])
|
|
commit = getattr(coder, "last_aider_commit_hash", None)
|
|
commit_msg = getattr(coder, "last_aider_commit_message", None)
|
|
except Exception as exc: # aider peut lever des erreurs variées
|
|
return {
|
|
"ok": False,
|
|
"summary": f"échec moteur code : {exc}",
|
|
"text": "", "files": [], "commit": None,
|
|
}
|
|
finally:
|
|
os.chdir(prev_cwd)
|
|
|
|
summary = f"{len(edited)} fichier(s) modifié(s)"
|
|
if commit:
|
|
summary += f" · commit {str(commit)[:7]}"
|
|
return {
|
|
"ok": True,
|
|
"text": text.strip(),
|
|
"files": edited,
|
|
"commit": commit,
|
|
"commit_message": commit_msg,
|
|
"summary": summary,
|
|
}
|