diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..92a575b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# Contrôle de version +.git +.gitignore + +# Python +**/__pycache__ +**/*.pyc +backend/.venv +backend/venv + +# Node / build +frontend/node_modules +frontend/dist +**/.vite +frontend/tsconfig.tsbuildinfo + +# Données & workspace de runtime (montés en volume) +data +workspace + +# Environnement / IDE +.env +.env.local +.idea +.vscode +**/.DS_Store diff --git a/.env.example b/.env.example index 23c12ff..ba7e192 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,7 @@ DATA_DIR=/data # Port exposé par l'application PORT=8080 + +# Optionnel : URL d'une instance SearxNG pour l'outil web_search. +# Si vide, web_search utilise DuckDuckGo (sans clé d'API). +SEARX_URL= diff --git a/Dockerfile b/Dockerfile index de43a58..e5fa5bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,10 @@ ENV PYTHONUNBUFFERED=1 \ DATA_DIR=/data \ PORT=8080 +# curl pour le HEALTHCHECK +RUN apt-get update && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + COPY backend/requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt @@ -23,8 +27,16 @@ COPY backend/ ./backend/ # Frontend compilé servi en statique par FastAPI COPY --from=frontend /app/frontend/dist ./backend/static -RUN mkdir -p /workspace /data +# Utilisateur non-root + dossiers de runtime lui appartenant +RUN useradd --create-home --uid 10001 loki \ + && mkdir -p /workspace /data \ + && chown -R loki:loki /workspace /data /app +USER loki EXPOSE 8080 WORKDIR /app/backend + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -fsS "http://localhost:${PORT}/api/health" || exit 1 + CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"] diff --git a/README.md b/README.md index 7093093..19dbe6d 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,44 @@ npm run dev # http://localhost:5173 | `OLLAMA_HOST` | `http://host.docker.internal:11434` | URL de l'instance Ollama | | `DEFAULT_MODEL` | `llama3.1:8b` | Modèle sélectionné au démarrage | | `WORKSPACE_DIR` | `/workspace` | Dossier de travail de l'agent | -| `DATA_DIR` | `/data` | Base SQLite (sessions) | +| `DATA_DIR` | `/data` | Base SQLite (sessions + config) | | `PORT` | `8080` | Port exposé | +| `SEARX_URL` | *(vide)* | Instance SearxNG pour `web_search` (sinon DuckDuckGo) | + +## Utilisation + +1. Vérifie la pastille **Ollama** (verte = connecté) en haut à droite, et choisis + un modèle qui supporte le *function calling* (ex. `llama3.1:8b`, + `qwen2.5-coder`). +2. Décris une tâche dans le tchat, p. ex. *« Crée une landing page pour un café + nommé Café Lumière, avec menu et horaires »*. +3. L'agent lit/écrit des fichiers dans le **workspace** ; chaque appel d'outil + s'affiche dans le fil, et l'aperçu HTML apparaît à droite (onglets **Aperçu / + Code / Logs**). +4. Règle le comportement dans **Configuration** (modèle, température/top-p/top-k, + jetons max, outils actifs, invite système). + +## Outils de l'agent + +| Outil | Rôle | Par défaut | +| ------------- | -------------------------------------- | ---------- | +| `read_file` | Lire un fichier du workspace | activé | +| `write_file` | Créer / modifier un fichier | activé | +| `list_dir` | Lister un répertoire | activé | +| `web_search` | Recherche web (DuckDuckGo / SearxNG) | désactivé | +| `run_shell` | Exécuter une commande **(sensible)** | désactivé | + +## Sécurité + +- **Confinement** : toutes les opérations fichier (`read_file`, `write_file`, + `list_dir`) et `run_shell` sont strictement confinées au `WORKSPACE_DIR`. Toute + tentative de sortie (`../`, chemin absolu) est rejetée. +- **`run_shell`** est désactivé par défaut. Une fois activé, chaque commande + proposée par l'agent demande une **validation explicite** dans l'interface + (option *confirm_shell*, activée par défaut) avant exécution. +- Le conteneur tourne en **utilisateur non-root** et expose un **HEALTHCHECK**. +- Loki est conçu pour un usage **local** : n'expose pas le port publiquement sans + ajouter ta propre couche d'authentification. ## Feuille de route diff --git a/docker-compose.yml b/docker-compose.yml index bac02ef..7469e33 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,12 +10,20 @@ services: - WORKSPACE_DIR=/workspace - DATA_DIR=/data - PORT=8080 + # Optionnel : instance SearxNG pour web_search (sinon DuckDuckGo) + - SEARX_URL=${SEARX_URL:-} volumes: - ./workspace:/workspace - ./data:/data extra_hosts: # Permet d'atteindre un Ollama installé sur la machine hôte - "host.docker.internal:host-gateway" + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8080/api/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s restart: unless-stopped # ── Ollama optionnel ────────────────────────────────────────────────── diff --git a/frontend/src/panels/PreviewPanel.tsx b/frontend/src/panels/PreviewPanel.tsx index 2d5fed9..24a2ba5 100644 --- a/frontend/src/panels/PreviewPanel.tsx +++ b/frontend/src/panels/PreviewPanel.tsx @@ -1,15 +1,22 @@ import { useState } from "react"; import { useStore } from "../store/useStore"; +import type { ToolCall } from "../api/client"; -type TabId = "preview" | "code"; +type TabId = "preview" | "code" | "logs"; -/** Panneau droit : onglets Aperçu / Code + rendu du fichier sélectionné. */ +/** Panneau droit : onglets Aperçu / Code / Logs. */ export function PreviewPanel() { - const { previewPath, previewContent } = useStore(); + const { previewPath, previewContent, messages, streamTools } = useStore(); const [tab, setTab] = useState("preview"); const isHtml = previewPath ? /\.html?$/.test(previewPath) : false; + // Journal d'activité : tous les appels d'outils de la session + en cours. + const logs: ToolCall[] = [ + ...messages.flatMap((m) => m.meta?.tools ?? []), + ...streamTools, + ]; + return (
{/* Onglets */} @@ -21,6 +28,12 @@ export function PreviewPanel() { setTab("code")}> Code + setTab("logs")}> + Logs + + {logs.length} + +
@@ -33,9 +46,54 @@ export function PreviewPanel() { - {/* Viewport */} -
- {!previewPath ? ( + {/* Onglet Logs (fond sombre) */} + {tab === "logs" ? ( +
+ {logs.length === 0 ? ( +
+ Aucune activité d'outil pour cette session. +
+ ) : ( + logs.map((l, i) => ( +
+ + {l.status === "error" + ? "✕" + : l.status === "pending" + ? "⏸" + : l.status === "running" + ? "…" + : "✓"} + + {l.name} + + {(l.args?.path as string) ?? + (l.args?.query as string) ?? + (l.args?.command as string) ?? + ""} + + {l.summary} +
+ )) + )} +
+ ) : ( + /* Viewport Aperçu / Code (fond clair) */ +
+ {!previewPath ? ( L'aperçu s'affichera ici dès que l'agent générera un fichier (clique aussi un fichier à gauche). @@ -51,12 +109,13 @@ export function PreviewPanel() { className="h-full w-full border-0 bg-white" sandbox="allow-same-origin" /> - ) : ( -
-            {previewContent}
-          
- )} -
+ ) : ( +
+              {previewContent}
+            
+ )} +
+ )} ); }