- Go 98.3%
- Dockerfile 1.1%
- Shell 0.6%
|
Some checks are pending
build / build (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| cmd/kvasir | ||
| deploy | ||
| internal | ||
| .dockerignore | ||
| .gitignore | ||
| CLAUDE.md | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
kvasir
Agent Go autonome qui pilote une ou plusieurs bibliothèques nxt-opds via leur serveur MCP : enrichissement automatique des métadonnées (tags, résumé, classification d'âge, intensité du piment, séries…), réponses conversationnelles sur le catalogue, exécution de gros batches de maintenance.
Un seul binaire kvasir gère plusieurs instances nxt-opds. Chacune est
configurée via un appairage à code one-time, sans copier-coller de secrets.
Sommaire
- Sous-commandes
- Installation rapide
- Appairage avec une instance nxt-opds
- Configuration YAML
- Backends LLM
- Daemon (
serve) - Maintenance en lot (
batch) - Run one-shot (
run) - Docker / docker-compose
- Logs
- Architecture interne
- Self-update
Sous-commandes
| Commande | Quand l'utiliser |
|---|---|
pair |
Associer ce librarian à une instance nxt-opds via un code généré dans l'admin UI |
unpair |
Dissocier proprement une instance des deux côtés |
serve |
Faire tourner le daemon longue durée (chat, webhooks book-event, ticker, heartbeat) |
batch |
Itérer une maintenance déterministe (pagination en Go, pas en LLM) — idéal pour « traite tous les 16+ » |
run |
Exécution one-shot pilotée par LLM (recherche par titre, prompt ad-hoc) |
update |
Self-update vers la dernière release Forgejo |
version |
Affiche la version installée |
kvasir help pour l'aide complète, kvasir <cmd> --help pour les flags d'une commande.
Installation rapide
go install git.helheim.net/mimisbrunnr/kvasir-agent/cmd/kvasir@latest
# ou télécharger la release pré-compilée :
# https://git.helheim.net/mimisbrunnr/kvasir-agent/releases
Vérifier :
kvasir version
Appairage
L'appairage utilise un code one-time généré depuis l'UI admin nxt-opds.
Aucun secret ne transite par la ligne de commande ; les chat_secret et
webhook_secret sont négociés entre les deux services pendant l'échange.
Étapes
-
Dans l'admin nxt-opds : cliquer « Associer un librarian ». Un code
XXXX-XXXXvalide 10 minutes s'affiche. -
Sur la machine du librarian :
kvasir pair \ --nxt-opds https://books.example.com \ --code K4Q9-PN2X \ --name example \ --label "Bibliothèque Example"Le YAML
~/.config/kvasir/config.yamlest créé / mis à jour ; nxt-opds stockelibrarian_urlcôté DB. La chat box devient active immédiatement.
Flags utiles
| Flag | Description |
|---|---|
--librarian-url <url> |
URL publique du librarian que nxt-opds doit appeler. Défaut : déduit du champ listen du YAML (http://localhost:8080) ou de public_url. |
--rotate |
Régénère chat_secret + webhook_secret sans nouveau code (utilise le chat_secret actuel pour s'authentifier). |
--force |
Écrase une association existante côté nxt-opds (sinon 409). |
--print-only |
N'écrit rien, affiche juste les blocs YAML à copier-coller. |
Dissocier
kvasir unpair --name example --nxt-opds https://books.example.com
Efface l'entrée du YAML local et appelle (best-effort) nxt-opds pour nettoyer l'association côté DB.
Configuration YAML
Résolution : --config <path> > KVASIR_CONFIG env > ./kvasir.yaml
~/.config/kvasir/config.yaml.
listen: ":8080" # adresse d'écoute HTTP du daemon
public_url: "http://kvasir.lan:8080" # URL annoncée à nxt-opds (sinon dérivé de listen)
interval: "6h" # cadence du ticker en mode serve
batch_limit: 10 # nb de livres traités par tick
max_steps: 200 # plafond d'étapes par job
backend: "auto" # auto | ollama | anthropic
model: "" # nom du modèle (sinon défaut du backend)
ollama_url: "http://localhost:11434" # endpoint Ollama ; surchargé par OLLAMA_HOST / --ollama-url
default_instance: "example" # utilisée quand --instance est omis
# Optionnel : clé Google Books — active l'outil google_books_search et le
# place en priorité 1 (avant Babelio / sites éditeurs) pour la recherche de
# métadonnées. Surchargée par la variable d'env GOOGLE_BOOKS_API_KEY.
# google_books_api_key: "AIza..."
instances:
- name: "example" # slug [a-z0-9-]+, unique
mcp_url: "https://books.example.com/mcp"
mcp_token: "<opds_token>" # injecté par `pair`
chat_secret: "<64-hex>" # idem
webhook_secret: "<64-hex>" # idem
label: "Bibliothèque Example"
locale: "fr"
Tout secret est en clair dans le fichier — perms 0600 appliquées
automatiquement. Les variables d'env sont expansées (${OPDS_TOKEN_EX})
au chargement.
Backends LLM
- Ollama (défaut) — local ou Ollama Cloud. Modèle par défaut :
gemma4:31b-cloud. Override :KVASIR_MODELou--model. - Anthropic (Claude) — activé si
ANTHROPIC_API_KEYest défini OU si--backend anthropic. Plus discipliné sur les boucles longues (recommandé pour de très gros batches).
Variables d'env supplémentaires :
| Variable | Rôle |
|---|---|
KVASIR_BACKEND |
auto (défaut) / ollama / anthropic |
KVASIR_MODEL |
nom de modèle |
OLLAMA_HOST |
endpoint Ollama — l'emporte sur ollama_url du YAML (défaut http://localhost:11434) |
ANTHROPIC_API_KEY |
clé API Claude |
FIRECRAWL_API_KEY |
clé Firecrawl — backend de web_fetch et active l'outil web_search (recherche web via Firecrawl, au lieu de scraper un moteur). Override le YAML |
GOOGLE_BOOKS_API_KEY |
clé Google Books — active l'outil google_books_search en source de métadonnées prioritaire (override le YAML) |
CAMOFOX_URL |
URL d'un serveur camofox-browser (Firefox stealth local, ex. http://127.0.0.1:9377) — backend de web_fetch (essayé après Firecrawl, avant obscura) et active web_search via la macro @google_search quand aucune clé Firecrawl n'est posée. Override le YAML |
CAMOFOX_ACCESS_KEY |
bearer attendu par camofox uniquement s'il est lancé avec CAMOFOX_ACCESS_KEY (exposé hors loopback). Override le YAML |
KVASIR_CONFIG |
chemin du YAML |
Daemon (serve)
kvasir serve --listen :8080 --interval 6h
À démarrage, le daemon :
- Charge le YAML, instancie un Registry d'instances (clients MCP +
Agentparesseusement initialisés). - Announce : POST
/api/librarian/announcesur chaque nxt-opds appairé avec lepublic_urlcourant — auto-réparation après changement de port/hostname/docker network. - Heartbeat : ticker 60 s qui POST
/api/librarian/heartbeatpour que l'admin UI nxt-opds montre la fraîcheur de la liaison. - Ticker batch : toutes les
interval(défaut 6 h), enqueue un jobsearch_books(not_indexed:true, limit=batch_limit)sur chaque instance.
Routes exposées
| Route | Méthode | Description |
|---|---|---|
/healthz |
GET | OK plaintext |
/instances |
GET | JSON public : [{name, label, locale}] |
/chat |
POST | Endpoint de chat appelé par la chat box nxt-opds. Auth : Authorization: Bearer <chat_secret>. Body : {message, history, user_token?}. Réponse JSON {reply, error?}. |
/webhooks/{instance}/book-event |
POST | Événements catalogue (book.created/updated/deleted/read) émis par nxt-opds. Signature X-Signature: sha256=<hmac> validée contre webhook_secret. |
/trigger/{instance} |
POST | Trigger manuel : body JSON {prompt} ou texte brut. Le prompt remplace l'instruction batch par défaut. |
/instances/{instance}/forget |
POST | Appelé par nxt-opds lors d'un unpair côté UI. Auth : Authorization: Bearer <chat_secret>. |
Flags clés
| Flag | Description |
|---|---|
--listen :8080 |
Adresse d'écoute (override le YAML) |
--interval 6h |
Cadence du ticker |
--batch-limit 10 |
Nb de livres traités par tick |
--max-steps 500 |
Étapes max par job (200 par défaut) |
--job-timeout 2h |
Timeout par job (1 h par défaut) — augmenter pour de gros batches |
--prompt "…" |
Prompt remplaçant l'instruction batch par défaut du ticker |
--instance <name> |
Pour run / batch ; quand plusieurs instances sont configurées |
Maintenance en lot (batch)
batch est la commande à utiliser pour les gros chantiers : « note le
piment de tous les 16+ », « enrichi tous les non-indexés », etc. La
pagination tourne dans Go, pas dans le LLM, donc même un petit modèle ne
peut pas couper court en écrivant FIN prématurément.
kvasir batch --instance example --filter age_rating_min=16
Cycle interne :
for offset := 0; ; offset += limit {
ids := search_books(filters, limit, offset)
if len(ids) < limit { break }
for id := range ids {
agent.Run(perBookPrompt(id)) // ~5-10 étapes par livre
}
}
Flags
| Flag | Description |
|---|---|
--filter k=v |
Filtre passé à search_books. Répétable. Types coercés (int/bool/string). |
--limit 50 |
Taille de page (max 100). |
--offset 100 |
Reprend à partir d'un offset arbitraire. |
--max-books 50 |
Plafond global (0 = illimité). Utile pour valider sur un échantillon. |
--max-steps 60 |
Étapes max par livre (5-10 suffisent en général). |
--prompt "…" |
Template par livre, {{ID}} est remplacé. Défaut : enrichissement complet selon le workflow standard. |
--retry-wait 1h |
Pause entre 2 retries après quota / rate-limit / réseau transitoire. |
--max-rate-retries 6 |
Nb de pauses+retry par livre avant abandon de ce livre. |
--dry-run |
Liste les IDs candidats sans invoquer l'agent. |
Exemples
# Lister les candidats sans rien modifier
kvasir batch --instance example --filter age_rating_min=16 --dry-run
# Toute la bibliothèque, en lots de 100, avec une pause d'1 h sur quota
kvasir batch --instance example --filter age_rating_min=16 \
--limit 100 --retry-wait 1h
# Reprendre un batch interrompu (le log "interrompu" donne la valeur d'offset)
kvasir batch --instance example --filter age_rating_min=16 --offset 250
# Tag spécifique
kvasir batch --instance example --filter tag="Dark Romance"
# Échantillon de 10 pour valider un prompt custom
kvasir batch --instance example --filter age_rating_min=16 \
--max-books 10 \
--prompt 'Pour {{ID}} : get_book puis web_fetch Babelio puis update_book(spice_rating:N, last_maintenance_at:-1). Termine par FIN.'
Gestion des quotas LLM
Sur une bibliothèque de plusieurs centaines de livres, Ollama Cloud ou
Anthropic finissent par limiter. Le batch détecte automatiquement HTTP 429,
503, rate limit, overloaded, quota, too many requests, ainsi que les
glitchs réseau transitoires (i/o timeout, connection reset, EOF). Sur
détection :
- log :
[batch ex] id=abc rate-limit (…) — pause 1h0m0s, reprise vers 23:42 (retry 1/6) - sleep
--retry-wait(interruptible Ctrl-C) - retry du même livre
- au-delà de
--max-rate-retries, le livre est abandonné et le loop continue
Ctrl-C pendant une pause : log interrompu — reprendre avec --offset N.
Run one-shot (run)
Pour les invocations interactives : un livre par titre, un prompt ad-hoc.
# Cibler un livre par titre (positionnel = recherche de titre)
kvasir run --instance example "Le Chevalier et la Phalène"
# Prompt verbatim
kvasir run --instance example --prompt "Liste les 10 derniers livres ajoutés."
# Maintenance pilotée par LLM (alternative au `batch` si on tient à laisser
# l'agent décider de la pagination — plus erratique sur les petits modèles)
kvasir run --instance example \
--prompt "Traite TOUS les livres non indexés un par un, sans limite. Termine par FIN." \
--max-steps 1000
Pour un vrai gros chantier, préférer batch.
Docker / docker-compose
Image distroless ~25 MB, exposée à :8080, config sur volume /config.
# docker-compose.yml fragment
services:
kvasir:
image: git.helheim.net/mimisbrunnr/kvasir-agent:latest
ports: ["8081:8080"]
volumes: ["kvasir-config:/config"]
environment:
KVASIR_CONFIG: /config/config.yaml
KVASIR_BACKEND: anthropic
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
restart: unless-stopped
volumes:
kvasir-config:
Premier lancement (paire avec un nxt-opds dans le même compose) :
docker compose run --rm kvasir pair \
--nxt-opds http://nxt-opds:8080 \
--code XXXX-XXXX --name example \
--librarian-url http://kvasir:8080
docker compose restart kvasir
Un docker-compose.yml complet (nxt-opds + librarian) est fourni dans
../../docker-compose.yml.
Pour une installation propre du stack complet (nxt-opds + librarian, Docker
ou systemd, appairage, vérifications) destinée à un agent ou à un humain qui
suit pas à pas, voir deploy/AGENT-INSTALL.md.
Logs
Le daemon log toutes les étapes du loop agent (chat et batch/webhook/ticker) :
[chat example] ◀ 192.168.1.42 (scope=user, history=4): Quel est mon dernier livre ?
[chat example] tool_call search_books {"limit":1,"sort":"added_desc"}
[chat example] tool_result search_books [ok] Trouvé 689 livre(s)…
[chat example] text: Votre dernier livre ajouté est « Roi Sorcier » de Martha Wells.
[chat example] done in 1.4s (steps=2, tools=1, stop=end_turn)
[chat example] ▶ reply (78 chars, tools=1): Votre dernier livre…
[example job webhook] start: Un nouveau livre vient d'être ajouté…
[example job webhook] tool_call get_book {"id":"abc123"}
[example job webhook] tool_result get_book [ok] **Titre…
[example job webhook] done in 8.2s (tools=4)
[batch example] page offset=100 limit=50 → 50 IDs (total estimé 689)
[batch example] ▶ 101/689 id=d34638c2c8d81822
[batch example] id=d34638c2c8d81822 rate-limit (ollama 429: …) — pause 1h0m0s, reprise vers 23:42:15 (retry 1/6)
Architecture interne
cmd/kvasir/ # CLI : sous-commandes run/serve/batch/pair/unpair/update
internal/config/ # parse YAML, validate slugs, ResolveLibrarianURL, NxtOPDSBaseURL
internal/instances/ # Registry : map nom→Entry{Client, Agent, Lock, Jobs}, lazy init
internal/mcp/ # client MCP HTTP Streamable (JSON-RPC + SSE) ; WithBearer pour le scoping user
internal/llm/ # interface Provider + backends Ollama et Anthropic
internal/agent/ # boucle tool-calling, system prompt batch + chat, Emit hook pour SSE
internal/daemon/ # ticker + serveur HTTP + workers par instance + announce + heartbeat
internal/updater/ # self-update via Forgejo releases
Points d'attention :
- Une seule goroutine worker par instance : les jobs sont sérialisés par
instance (le
transcriptdu modèle n'est pas thread-safe) mais parallèles entre instances. - Le
Modede l'agent (ModeBatchvsModeChat) choisit dynamiquement le system prompt pour ne pas confondre l'enrichissement autonome avec le chat conversationnel. - Côté chat,
mcp.WithBearer(ctx, user_token)injecte le token utilisateur du flux nxt-opds → tools per-user (list_to_read,list_wishlist, etc.) scopent automatiquement au compte connecté.
Self-update
kvasir update # télécharge et installe la dernière release
kvasir update --dry-run # voir la version cible sans rien faire
kvasir update --force # forcer la réinstallation
Cible : git.helheim.net/api/v1/repos/mimisbrunnr/kvasir-agent/releases/latest.
L'asset correspondant à GOOS-GOARCH est rename'é atomiquement sur le
binaire courant.
Auto-update horaire (systemd)
deploy/systemd/ fournit un timer qui vérifie une nouvelle release toutes
les heures et ne redémarre le daemon que si une version a réellement été
installée (un update sans nouveauté ne coupe pas un batch en cours) :
sudo install -m 0755 deploy/systemd/kvasir-autoupdate /usr/local/bin/
sudo install -m 0644 deploy/systemd/kvasir-update.service /etc/systemd/system/
sudo install -m 0644 deploy/systemd/kvasir-update.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now kvasir-update.timer
Adapter le chemin du binaire ou le nom de l'unité serve (défauts
/usr/local/bin/kvasir et kvasir.service) via Environment= dans
kvasir-update.service.
Vérifier / tester :
systemctl list-timers kvasir-update.timer # prochaine échéance
sudo systemctl start kvasir-update.service # forcer un cycle maintenant
journalctl -u kvasir-update.service # logs (version installée ou « déjà à jour »)
Le wrapper tourne en root (il écrase le binaire et appelle systemctl). La
vérification anonyme de l'API Forgejo suffit largement à une cadence
horaire.
Licence
Voir LICENSE.