diff --git a/README.md b/README.md index 5f77c3a..615aa84 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ > **un conteneur Docker GPU autonome**, là où l'amont s'installe en binaire + > systemd sur la machine hôte. -Loki fait tourner un modèle de langage **100 % en local** : tchat, mémoire -persistante, accès internet, outils MCP, agent (shell, fichiers), accès distant -chiffré — serveur d'inférence llama.cpp compris, dans une seule image. +Loki fait tourner un modèle de langage **100 % en local** : discussions +multiples, mémoire persistante, accès internet, captures de pages web, outils +MCP, agent (shell, fichiers) — serveur d'inférence llama.cpp compris, dans une +seule image. ## Architecture @@ -19,7 +20,9 @@ chiffré — serveur d'inférence llama.cpp compris, dans une seule image. │ │ pilote (fichier PID — pas de systemd) │ │ loki serve ──exec──► llama-server (CUDA, :8080) │ │ ▲ modèles .gguf │ -│ /data (config, bbolt, mémoire, workspace) │ +│ chromium (Playwright) — captures de pages web │ +│ /data (config, bbolt, presets, mémoire, │ +│ workspace + captures, modèles installés) │ │ /models (GGUF déposés à la main) │ └────────────────────────────────────────────────────┘ ``` @@ -102,36 +105,117 @@ redémarrages (volume `/data`). Variables d'environnement du conteneur : | `LOKI_NGL` | couches GPU initiales | `999` (tout) | | `LOKI_HOME` | données (volume) | `/data` | | `LOKI_MODEL_DIRS` | dossiers .gguf additionnels | `/models` | +| `HF_TOKEN` | jeton Hugging Face, pour les dépôts à accès restreint | — | En CLI dans le conteneur : `docker exec -it loki loki status` (aussi : `logs`, `restart`, `config`, `bench`, `test`…). -## Fonctionnalités (héritées d'AJEAN) +### Données et persistance -- **Tchat** avec streaming, raisonnement visible, pièces jointes, vision - (selon modèle), export de conversations. +**Tout** l'état vit sous `/data` (= `LOKI_HOME`). Si ce chemin n'est pas un +volume monté sur l'hôte, il disparaît au premier `docker compose down` — presets +et modèles compris. + +| Chemin | Contenu | +|---|---| +| `/data/loki.db` | base bbolt : préférences, discussions, mémoire des réglages | +| `/data/presets/` | un `.env` par preset (modèle, contexte, NGL, vision…) | +| `/data/models/` | modèles téléchargés depuis l'interface | +| `/data/memory/` | pages de mémoire persistante (`.md`) | +| `/data/workspace/` | dossier de travail de l'agent | +| `/data/workspace/captures//` | captures d'écran, un dossier par discussion | +| `/data/loki-engine.log` | journal de `llama-server` (aussi via `loki logs`) | +| `/models` | GGUF déposés à la main depuis l'hôte (volume séparé, `LOKI_MODEL_DIRS`) | + +Vérifier que le volume est bien là : + +```bash +docker inspect loki --format '{{range .Mounts}}{{.Source}} → {{.Destination}}{{println}}{{end}}' +``` + +Sur Unraid, garde le **même** chemin hôte d'un lancement à l'autre : `/mnt/user/…` +(partage, via FUSE) et `/mnt/cache/…` (disque de cache) désignent des +emplacements différents dès que le partage n'est pas en cache-only ou que le +*mover* est passé. Le compose fourni utilise `/mnt/user/appdata/loki/…`. + +Autre piège au redémarrage du serveur : Docker relance les conteneurs +`restart: unless-stopped` **avant** que le plugin Nvidia Driver ait chargé ses +modules. Le journal montre alors des `ERROR: init … result=11` et le moteur +démarre sans GPU. Un `docker restart loki` une fois le pilote prêt suffit. + +## Fonctionnalités + +Héritées d'AJEAN : + +- **Tchat** avec streaming, raisonnement visible, pièces jointes, export de + conversations. La **vision** demande un modèle multimodal *et* son projecteur + `mmproj` — voir [Installer un modèle](#installer-un-modèle). - **Mémoire persistante** (`memory off|ondemand|always`). - **Accès internet** : recherche + lecture de pages, moteur Go intégré ou [Crawl4AI](https://github.com/unclecode/crawl4ai) pour les pages JS. - **Agent** : shell, fichiers, workspace (`agent on`). - **Serveurs MCP** : Node.js est inclus dans l'image pour les serveurs `npx`. - **Presets** de configuration par modèle, bench, auto-détection GPU. -- **Accès distant chiffré** via le relais [ajean.link](https://ajean.link) - (service opéré par l'auteur de l'amont). - **API OpenAI-compatible** exposable (`network on`, protégée par clé). +Ajoutées par ce fork : + +- **Discussions multiples** : historique complet dans la barre latérale, titre + repris du premier message (renommable), suppression. Supprimer une discussion + efface aussi ses captures d'écran (`workspace/captures//`), pour que le + disque ne se remplisse pas en silence. +- **Recherche Hugging Face** intégrée avec verdict mémoire et installation liée + du projecteur vision (voir [Installer un modèle](#installer-un-modèle)). +- **Captures de pages web** : l'agent dispose de l'outil `web_screenshot` + (Chromium via Playwright, inclus dans l'image). Les captures partent en JPEG + et sont plafonnées à 20 fichiers / 40 Mo par discussion. La description de + l'outil suit la capacité **réelle** du moteur, sondée sur `/props` : sans + vision effective, elle dit au modèle « tu ne vois pas l'image » plutôt que de + lui promettre des yeux qu'il n'a pas — il peut toujours prendre la capture et + la montrer, sans prétendre la décrire. L'image relayée au moteur reste + éphémère : la persister gonflait le contexte jusqu'à le faire déborder. +- **Panneau Fichiers** (bouton dossier du pied de carte) : ce que l'agent a + écrit dans son dossier de travail, avec navigation dans les sous-dossiers, + téléchargement et suppression. Un dossier affiche la taille de **tout** son + contenu — c'est ce qu'on libère en le supprimant. Le pied donne l'occupation + disque totale de l'agent. Les chemins sont bornés au dossier de travail, liens + symboliques résolus des deux côtés : rien du reste du disque n'est atteignable. +- **Identité** : ton prénom et un avatar emoji pour toi et pour Loki, affichés + dans le fil. +- **Paramètres** : les réglages d'application (identité, apparence, accès + OpenAI, actions) sont regroupés à part des réglages d'IA. + +Retirées par ce fork : + +- **Accès distant via [ajean.link](https://ajean.link)** : la section de + l'interface et son module JS sont supprimés — un conteneur derrière son + propre réseau n'en a pas l'usage. Le code serveur du relais reste en place + mais **inerte** (aucun jeton, aucune section pour en fournir un) : le retirer + créerait un conflit à chaque reprise de l'amont. +- **Postes distants** (faire agir l'agent sur un autre PC appairé) : bouton du + composeur, modales d'appairage et module JS supprimés. Même traitement que + ci-dessus — les routes `/api/node/*` subsistent mais plus rien ne peut + générer de code d'appairage, donc aucun poste ne peut se connecter. +- **Catalogue de modèles distant** : il interrogeait `ajean.link/models.json`, + sa route n'avait aucun consommateur et son repli embarqué datait de 2024. La + recherche Hugging Face le remplace. + ## Différences avec l'amont | | AJEAN (amont) | Loki (ce fork) | |---|---|---| | Installation | binaire + `sudo ajean install` (systemd) | `docker compose up` | | Moteur llama.cpp | compilé sur la machine (`ajean llamacpp install`) | image officielle llama.cpp (`server-cuda`), précompilée | +| Panneau « Moteur » | propose d'installer/compiler | annonce le moteur fourni par l'image, sans proposer d'install | | Supervision moteur | systemd / launchd / PID (Windows) | fichier PID (`LOKI_CONTAINER=1`) | | Configuration initiale | `ajean edit` ($EDITOR) | entrypoint + `loki config set` | +| Choix du modèle | lien Hugging Face collé à la main | recherche intégrée + verdict VRAM + projecteur lié | +| Historique de tchat | conversation unique | discussions multiples, titrées et persistées | +| Accès distant | relais chiffré ajean.link | retiré de l'interface | | Mise à jour | `ajean update` (binaire GitHub) | `docker compose pull` | -Le reste — UI, mémoire, outils, protocole — est celui d'AJEAN. Pour récupérer -les évolutions de l'amont : +Le reste — mémoire, outils, protocole, moteur d'inférence — est celui d'AJEAN. +Pour récupérer les évolutions de l'amont : ```bash git fetch upstream && git merge upstream/main # conflits de renommage à arbitrer diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index ccfc238..e11a22f 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -1140,20 +1140,6 @@ html[data-hide-stats="1"] #chat .msg .statline{display:none} #attach:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)} #attach:active{transform:scale(.9)} #attach svg{display:block} -/* Bouton « Postes distants » du composeur : même gabarit que le trombone. L'icône - dépend de la cible active — écran (local) par défaut, ondes wifi quand un poste - distant est sélectionné, alors teinté en accent pour signaler « l'IA agit - ailleurs ». Les deux SVG coexistent ; on n'en montre qu'un via la classe. */ -#node-btn{width:18px;height:18px;min-height:18px;flex:none;margin:0;padding:0; - display:flex;align-items:center;justify-content:center;border-radius:999px; - background:transparent;border:1px solid transparent;color:var(--dim)} -#node-btn:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)} -#node-btn:active{transform:scale(.9)} -#node-btn svg{display:block} -#node-btn .ic-remote{display:none} -#node-btn.remote{color:var(--accent)} -#node-btn.remote .ic-here{display:none} -#node-btn.remote .ic-remote{display:block} /* Fichiers en attente : de petites bulles POSÉES AU-DESSUS de la carte. Le composeur réserve déjà 34 px transparents au-dessus de lui (la bande de dégradé qui fond vers le fil) : les pastilles s'y logent, et on retire d'autant @@ -1269,21 +1255,6 @@ button:hover{border-color:var(--dim);color:var(--text)} .xempty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:44px 20px;text-align:center;color:var(--text);font-size:14px} .xempty-i{font-size:30px;line-height:1;color:var(--dim);opacity:.7} -/* Postes distants : cases de capacités, code d'appairage, commande. */ -.node-caps{display:flex;flex-wrap:wrap;gap:6px} -.node-cap{display:inline-flex;align-items:center;gap:5px;padding:5px 9px;border:1px solid var(--border);border-radius:7px;font-size:12px;cursor:pointer;user-select:none} -.node-cap input{margin:0} -.node-code{font-family:var(--mono,monospace);font-size:22px;font-weight:700;letter-spacing:3px;color:var(--accent);padding:8px 0} -.node-cmd{white-space:pre-wrap;word-break:break-all;background:var(--code-bg);border:1px solid var(--border);border-radius:7px;padding:8px;margin:6px 0;font-size:12px} - -/* Poste distant : sélecteur de cible d'exécution. */ -.node-target{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:8px} -.node-target-l{font-size:12px;color:var(--dim);margin-right:2px;width:100%} -.node-target-opt{display:inline-flex;align-items:center;gap:5px;padding:5px 9px;border:1px solid var(--border);border-radius:7px;font-size:12px;cursor:pointer} -.node-target-opt.on{border-color:var(--accent);color:var(--accent)} -.node-target-opt.off{opacity:.6} -.node-target-opt input{margin:0} - /* ===== Direction « Terre » : surfaces en cartes ======================= La charte pose des CARTES arrondies séparées par une gouttière, au lieu d'une colonne à filets. Bloc en fin de feuille pour prendre le pas sur les @@ -1420,6 +1391,75 @@ button:hover{border-color:var(--dim);color:var(--text)} .hf-mm{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:11.5px; color:var(--dim);cursor:pointer} .hf-mm.muted{cursor:default} + +/* --- Panneau « Fichiers » (droite) ------------------------------------ + Symétrique des réglages à gauche : une colonne fixe qui pousse la + conversation au lieu de la recouvrir. C'est possible ici parce que la largeur + de lecture est pilotée par --measure et que #chat se recentre tout seul dans + la place qui reste — le fil se resserre, il ne passe pas dessous. + Le panneau est retiré du flux quand il est fermé (et non simplement décalé) : + sinon il conserverait sa gouttière et le fil resterait décentré. */ +#files{display:none;width:320px;flex-shrink:0;border-left:1px solid var(--border); + background:var(--panel);padding:16px;padding-bottom:calc(16px + env(safe-area-inset-bottom)); + overflow-y:auto;flex-direction:column;gap:10px} +html[data-files="1"] #files{display:flex} +#files-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:14} +.files-head{display:flex;align-items:center;gap:8px} +.files-head .stitle{flex:1} +#files-refresh,#files-close{margin:0;padding:2px 8px;min-height:0;font-size:12px; + background:transparent;border:1px solid transparent;color:var(--dim);border-radius:8px} +#files-refresh:hover,#files-close:hover{border-color:var(--border);color:var(--text)} +/* Fil d'Ariane : il doit pouvoir revenir à la ligne — un identifiant de + discussion dans le chemin dépasse largement 320 px. */ +.files-crumb{display:flex;flex-wrap:wrap;align-items:center;gap:2px;font-size:11px} +.files-crumb-b{margin:0;padding:2px 6px;min-height:0;font-size:11px;border:0;border-radius:7px; + background:transparent;color:var(--dim);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.files-crumb-b:hover:not(:disabled){background:var(--row-bg-on);color:var(--text)} +.files-crumb-b.on{color:var(--text);cursor:default} +.files-crumb-s{color:var(--dim);opacity:.5} +.files-list{display:flex;flex-direction:column;flex:1;min-height:0} +.files-empty{font-size:12px;line-height:1.5;padding:10px 2px} +/* Une ligne = pastille, nom + méta, actions. Les actions n'apparaissent qu'au + survol : au repos on veut lire des noms de fichiers, pas une grille de + boutons. Sur écran tactile (pas de survol) elles restent visibles. */ +.files-row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:12px;font-size:12px} +.files-row:hover{background:var(--row-bg-on)} +.files-ic{flex:none;width:12px;text-align:center;color:var(--dim)} +.files-row.isdir .files-ic{color:var(--accent)} +.files-mid{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} +.files-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)} +.files-meta{font-family:var(--mono);font-size:10px;color:var(--dim);white-space:nowrap} +.files-acts{flex:none;display:flex;gap:2px;opacity:0;transition:opacity .12s} +.files-row:hover .files-acts,.files-row:focus-within .files-acts{opacity:1} +@media (hover:none){.files-acts{opacity:1}} +.files-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent; + border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer;margin:0;min-height:0} +.files-acts .iconbtn:hover{border-color:var(--border);color:var(--text)} +.files-foot{font-family:var(--mono);font-size:10px;padding-top:2px;border-top:1px solid var(--border)} +/* Téléchargement en cours : downloadWorkspaceFile pose .busy sur la ligne. */ +.files-row.busy{opacity:.55;pointer-events:none} +/* Bouton dossier du composeur : même gabarit que le trombone, et teinté quand + le panneau est ouvert — l'état se lit sans regarder à droite. */ +#files-btn{width:18px;height:18px;min-height:18px;flex:none;margin:0;padding:0; + display:flex;align-items:center;justify-content:center;border-radius:999px; + background:transparent;border:1px solid transparent;color:var(--dim)} +#files-btn:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)} +#files-btn:active{transform:scale(.9)} +#files-btn svg{display:block} +html[data-files="1"] #files-btn{color:var(--accent)} +/* Petit écran : tiroir par la droite, comme les réglages par la gauche. Pousser + une colonne de 320 px sur un téléphone ne laisserait rien à la conversation. */ +@media (max-width:720px){ + #files{position:fixed;inset:0 0 0 auto;width:85%;max-width:340px;z-index:15; + background:var(--bg);padding-top:calc(16px + env(safe-area-inset-top)); + padding-right:calc(16px + env(safe-area-inset-right))} + html[data-files="1"] #files-backdrop{display:block} +} +@media (min-width:721px){ + /* Dans la charte « Terre », le panneau est une carte comme les autres. */ + #files{background:var(--panel);border-left-color:transparent;border-radius:20px; + box-shadow:var(--shadow);margin:0} +} - + + + +
+
+
+
- - - -