mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-13 02:08:07 +02:00
Ollama reloaded the chat model mid-message because plan/summary/router calls sent divergent runner options (no num_ctx/num_batch) and omitted keep_alive — the main cause of perceived slowness. - share one keep-alive httpx.AsyncClient for all Ollama calls - unify runner options (runner_options) + keep_alive on every model call, embeddings included - drop the blocking LLM routing fallback (pure lexical heuristic) - run RAG recall + plan + code-model pick in parallel inside the SSE stream, after the start event - RAG cosine scoring off the event loop; cache /api/tags 30s and nvidia-smi 5s; frontend polls 2s->5s, warm poll backoff, dedup config fetch feat: working session menu in TopBar (switch/create/rename/delete) feat: workspace file deletion (DELETE /api/files + UI trash buttons) docs: recommended Ollama env vars (KEEP_ALIVE, MAX_LOADED_MODELS...) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
3.0 KiB
Python
92 lines
3.0 KiB
Python
"""Routes du workspace : arborescence, contenu, téléchargement, suppression."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
|
|
from ..config import settings
|
|
from ..tools import ToolError, _safe_path
|
|
|
|
router = APIRouter(prefix="/api/files", tags=["files"])
|
|
|
|
|
|
def _tree(path: str) -> list[dict]:
|
|
"""Arborescence triée (dossiers d'abord) du workspace."""
|
|
items = []
|
|
for name in sorted(os.listdir(path)):
|
|
if name.startswith("."):
|
|
continue
|
|
full = os.path.join(path, name)
|
|
rel = os.path.relpath(full, os.path.abspath(settings.workspace_dir))
|
|
if os.path.isdir(full):
|
|
items.append({"name": name, "path": rel, "type": "dir",
|
|
"children": _tree(full)})
|
|
else:
|
|
items.append({"name": name, "path": rel, "type": "file",
|
|
"size": os.path.getsize(full)})
|
|
# Dossiers avant fichiers
|
|
items.sort(key=lambda x: (x["type"] != "dir", x["name"]))
|
|
return items
|
|
|
|
|
|
@router.get("")
|
|
async def list_files() -> dict:
|
|
root = os.path.abspath(settings.workspace_dir)
|
|
os.makedirs(root, exist_ok=True)
|
|
return {"tree": _tree(root)}
|
|
|
|
|
|
@router.get("/content")
|
|
async def file_content(path: str) -> dict:
|
|
try:
|
|
target = _safe_path(path)
|
|
except ToolError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
if not os.path.isfile(target):
|
|
raise HTTPException(404, "fichier introuvable")
|
|
with open(target, "r", encoding="utf-8", errors="replace") as f:
|
|
return {"path": path, "content": f.read()}
|
|
|
|
|
|
@router.get("/download")
|
|
async def download_file(path: str) -> FileResponse:
|
|
"""Télécharge un fichier en conservant le confinement au workspace."""
|
|
try:
|
|
target = _safe_path(path)
|
|
except ToolError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
if not os.path.isfile(target):
|
|
raise HTTPException(404, "fichier introuvable")
|
|
return FileResponse(
|
|
target,
|
|
filename=os.path.basename(target),
|
|
media_type="application/octet-stream",
|
|
)
|
|
|
|
|
|
@router.delete("")
|
|
async def delete_file(path: str) -> dict:
|
|
"""Supprime un fichier ou un dossier (récursif) du workspace.
|
|
|
|
Même confinement que le téléchargement (`_safe_path`) ; la racine du
|
|
workspace est refusée. Les dotfiles (.git…) ne sont jamais listés par
|
|
l'arborescence, donc inaccessibles depuis l'UI.
|
|
"""
|
|
try:
|
|
target = _safe_path(path)
|
|
except ToolError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
root = os.path.abspath(settings.workspace_dir)
|
|
if os.path.abspath(target) == root:
|
|
raise HTTPException(400, "suppression de la racine du workspace refusée")
|
|
if os.path.isdir(target):
|
|
shutil.rmtree(target)
|
|
elif os.path.isfile(target):
|
|
os.remove(target)
|
|
else:
|
|
raise HTTPException(404, "fichier introuvable")
|
|
return {"deleted": path}
|