#!/usr/bin/env python3 """Génère un PDF expliquant l'architecture Honcho + Hermes""" from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import mm, cm from reportlab.lib.colors import HexColor, black, white from reportlab.lib.enums import TA_LEFT, TA_CENTER from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle, ListFlowable, ListItem, KeepTogether ) from reportlab.platypus.flowables import HRFlowable from reportlab.lib import colors import os OUTPUT = "/root/workspace/honcho-hermes-architecture.pdf" # Couleurs DARK = HexColor("#0f172a") ACCENT = HexColor("#3b82f6") GREEN = HexColor("#22c55e") GRAY = HexColor("#64748b") LIGHT = HexColor("#f1f5f9") ORANGE = HexColor("#f59e0b") doc = SimpleDocTemplate( OUTPUT, pagesize=A4, topMargin=2*cm, bottomMargin=2*cm, leftMargin=2.5*cm, rightMargin=2.5*cm, ) styles = getSampleStyleSheet() # Styles personnalisés styles.add(ParagraphStyle( "CoverTitle", parent=styles["Title"], fontSize=28, leading=34, textColor=DARK, spaceAfter=6*mm, alignment=TA_CENTER, fontName="Helvetica-Bold" )) styles.add(ParagraphStyle( "CoverSub", parent=styles["Normal"], fontSize=14, leading=18, textColor=GRAY, spaceAfter=4*mm, alignment=TA_CENTER, fontName="Helvetica" )) styles.add(ParagraphStyle( "SectionTitle", parent=styles["Heading1"], fontSize=18, leading=22, textColor=DARK, spaceBefore=10*mm, spaceAfter=4*mm, fontName="Helvetica-Bold" )) styles.add(ParagraphStyle( "SubTitle", parent=styles["Heading2"], fontSize=14, leading=18, textColor=ACCENT, spaceBefore=6*mm, spaceAfter=3*mm, fontName="Helvetica-Bold" )) styles.add(ParagraphStyle( "Body", parent=styles["Normal"], fontSize=10, leading=14, textColor=HexColor("#334155"), spaceAfter=3*mm, fontName="Helvetica" )) # "Code" style already exists in default stylesheet styles.add(ParagraphStyle( "MyBullet", parent=styles["Normal"], fontSize=10, leading=14, textColor=HexColor("#334155"), leftIndent=8*mm, spaceAfter=2*mm, fontName="Helvetica" )) styles.add(ParagraphStyle( "MySmallNote", parent=styles["Normal"], fontSize=8, leading=10, textColor=GRAY, spaceAfter=2*mm, fontName="Helvetica-Oblique" )) story = [] # ===== PAGE DE COUVERTURE ===== story.append(Spacer(1, 4*cm)) story.append(Paragraph("Honcho + Hermes", styles["CoverTitle"])) story.append(Paragraph("Architecture & Déploiement", styles["CoverSub"])) story.append(Spacer(1, 3*mm)) story.append(HRFlowable(width="60%", thickness=1, color=ACCENT)) story.append(Spacer(1, 5*mm)) story.append(Paragraph("Mémoire persistante cross-session pour Hermes Agent", styles["CoverSub"])) story.append(Spacer(1, 2*cm)) story.append(Paragraph("Laurent Keller — Homelab 2026", styles["MySmallNote"])) story.append(PageBreak()) # ===== TABLE DES MATIÈRES ===== story.append(Paragraph("Table des matières", styles["SectionTitle"])) story.append(Spacer(1, 3*mm)) toc_items = [ "1. Qu'est-ce que Honcho ?", "2. Architecture globale", "3. Composants déployés", "4. Flux de fonctionnement", "5. Configuration Hermes", "6. Commandes utiles", "7. Schéma réseau", "8. Dépannage", ] for item in toc_items: story.append(Paragraph(item, styles["MyBullet"])) story.append(PageBreak()) # ===== 1. QU'EST-CE QUE HONCHO ===== story.append(Paragraph("1. Qu'est-ce que Honcho ?", styles["SectionTitle"])) story.append(HRFlowable(width="100%", thickness=0.5, color=ACCENT)) story.append(Spacer(1, 3*mm)) story.append(Paragraph( "Honcho est une infrastructure mémoire open-source (AGPL-3.0) développée par Plastic Labs. " "Elle permet aux agents AI de construire une représentation persistante de l'utilisateur " "à travers les sessions — contrairement à une mémoire fichier classique qui ne fait que " "stocker du texte brut.", styles["Body"] )) story.append(Paragraph("Ce que Honcho apporte à Hermes :", styles["SubTitle"])) bullets = [ "Mémoire cross-session — l'agent se souvient de qui vous êtes entre deux conversations", "Représentation utilisateur — Honcho analyse les conversations et extrait des conclusions (préférences, patterns, style de communication)", "Dialectic engine — un moteur de raisonnement en arrière-plan qui synthétise la mémoire", "Recherche sémantique — retrouver des informations par similarité de sens, pas par mot-clé", "Multi-peers — chaque profil Hermes a sa propre identité AI tout en partageant la vue utilisateur", ] for b in bullets: story.append(Paragraph(b, styles["MyBullet"])) story.append(Spacer(1, 3*mm)) story.append(Paragraph("Le cœur du système :", styles["SubTitle"])) story.append(Paragraph( "1. Store — les conversations sont stockées dans PostgreSQL avec embeddings vectoriels
" "2. Reason — le Deriver (worker background) analyse les messages et met à jour la représentation
" "3. Query — Hermes interroge Honcho via 5 outils pour récupérer le contexte
" "4. Inject — le contexte est injecté dans le system prompt à chaque tour", styles["Body"] )) story.append(PageBreak()) # ===== 2. ARCHITECTURE GLOBALE ===== story.append(Paragraph("2. Architecture globale", styles["SectionTitle"])) story.append(HRFlowable(width="100%", thickness=0.5, color=ACCENT)) story.append(Spacer(1, 3*mm)) story.append(Paragraph( "Le déploiement s'appuie sur 3 VMs du homelab, interconnectées via le réseau 192.168.30.0/24 :", styles["Body"] )) # Tableau des VMs vm_data = [ ["VM", "IP", "Rôle", "Services"], ["Kellerflix\n(VM 101)", "192.168.30.10", "LLM & Embeddings", "Ollama\nnemotron-mini:4b\nnomic-embed-text"], ["postgres\n(LXC/VM)", "192.168.30.35", "Honcho Server", "Honcho API (:8000)\nHoncho Deriver\nPostgreSQL pgvector (:5434)"], ["redis\n(LXC/VM)", "192.168.30.36", "Cache", "Redis (:6379)"], ] vm_table = Table(vm_data, colWidths=[3.5*cm, 3*cm, 3*cm, 5.5*cm]) vm_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), DARK), ("TEXTCOLOR", (0, 0), (-1, 0), white), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 9), ("ALIGN", (0, 0), (-1, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("GRID", (0, 0), (-1, -1), 0.5, GRAY), ("BACKGROUND", (0, 1), (-1, -1), LIGHT), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), ])) story.append(vm_table) story.append(Spacer(1, 5*mm)) story.append(Paragraph("Flux des données :", styles["SubTitle"])) story.append(Paragraph( "Hermes (LXC 112) → Honcho API (192.168.30.35:8000) → PostgreSQL pgvector
" "Honcho Deriver → Ollama (192.168.30.10:11434) pour LLM + embeddings
" "Honcho API → Redis (192.168.30.36:6379) pour cache", styles["Body"] )) story.append(PageBreak()) # ===== 3. COMPOSANTS DÉPLOYÉS ===== story.append(Paragraph("3. Composants déployés", styles["SectionTitle"])) story.append(HRFlowable(width="100%", thickness=0.5, color=ACCENT)) story.append(Spacer(1, 3*mm)) # 3.1 Honcho API story.append(Paragraph("3.1 Honcho API (FastAPI)", styles["SubTitle"])) story.append(Paragraph( "Serveur HTTP qui expose l'API REST de Honcho. Point d'entrée pour Hermes et le SDK Python. " "Gère les sessions, peers, messages, et la recherche contextuelle.", styles["Body"] )) story.append(Paragraph( "Port : 8000
" "Healthcheck : GET /health → {\"status\":\"ok\"}
" "Docker : honcho-api (build local depuis le Dockerfile du repo plastic-labs/honcho)", styles["Body"] )) # 3.2 Honcho Deriver story.append(Paragraph("3.2 Honcho Deriver (Worker background)", styles["SubTitle"])) story.append(Paragraph( "Processus qui tourne en arrière-plan et analyse les messages stockés. " "Il utilise le LLM configuré (nemotron-mini:4b) pour :", styles["Body"] )) bullets = [ "Extraire des conclusions sur l'utilisateur (préférences, habitudes)", "Générer des résumés de sessions", "Mettre à jour la représentation du peer", "Alimenter le moteur dialectic pour honcho_reasoning", ] for b in bullets: story.append(Paragraph(b, styles["MyBullet"])) # 3.3 PostgreSQL pgvector story.append(Paragraph("3.3 PostgreSQL + pgvector", styles["SubTitle"])) story.append(Paragraph( "Base de données vectorielle. Stocke les messages, sessions, peers, et les embeddings " "(vecteurs à 768 dimensions pour nomic-embed-text). L'extension pgvector permet la " "recherche par similarité sémantique.", styles["Body"] )) story.append(Paragraph( "Image : pgvector/pgvector:pg15
" "Port interne : 5434 (évite conflit avec le PostgreSQL existant sur 5432)
" "Volume : honcho-pgdata (persistant)", styles["Body"] )) # 3.4 Redis story.append(Paragraph("3.4 Redis (Cache)", styles["SubTitle"])) story.append(Paragraph( "Cache pour accélérer les requêtes répétées. Redis existant sur le homelab, " "réutilisé sans conteneur dédié dans le stack Honcho.", styles["Body"] )) story.append(Paragraph( "Hôte : 192.168.30.36:6379
" "Pas de mot de passe (réseau interne)", styles["Body"] )) # 3.5 Ollama story.append(Paragraph("3.5 Ollama (LLM & Embeddings)", styles["SubTitle"])) story.append(Paragraph( "Serveur de modèles LLM locaux sur Kellerflix (GTX 1070, 8GB VRAM). " "Deux modèles utilisés :", styles["Body"] )) bullets = [ "nemotron-mini:4b (Q4_K_M, 2.7GB) — LLM pour le dialectic engine et le deriver. Supporte le tool calling.", "nomic-embed-text (274MB) — modèle d'embeddings pour la recherche vectorielle (768 dimensions).", ] for b in bullets: story.append(Paragraph(b, styles["MyBullet"])) story.append(PageBreak()) # ===== 4. FLUX DE FONCTIONNEMENT ===== story.append(Paragraph("4. Flux de fonctionnement", styles["SectionTitle"])) story.append(HRFlowable(width="100%", thickness=0.5, color=ACCENT)) story.append(Spacer(1, 3*mm)) story.append(Paragraph("4.1 À chaque message utilisateur", styles["SubTitle"])) steps = [ "1. Hermes reçoit le message de l'utilisateur", "2. Honcho injecte automatiquement le contexte dans le system prompt (représentation utilisateur + résumé de session + carte AI peer)", "3. Hermes traite la requête avec ce contexte", "4. La réponse est envoyée à l'utilisateur", "5. Le message est stocké dans Honcho (via l'API)", "6. Le Deriver analyse le message en arrière-plan et met à jour la représentation", ] for s in steps: story.append(Paragraph(s, styles["MyBullet"])) story.append(Paragraph("4.2 Modes de rappel (Recall Modes)", styles["SubTitle"])) recall_data = [ ["Mode", "Auto-injection", "Outils mémoire", "Usage"], ["hybrid\n(default)", "Oui", "Oui (5 outils)", "L'agent décide quand utiliser les outils vs le contexte auto-injecté"], ["context", "Oui", "Non (cachés)", "Économique en tokens, pas d'appels outils"], ["tools", "Non", "Oui", "L'agent contrôle tout explicitement"], ] recall_table = Table(recall_data, colWidths=[3*cm, 2.5*cm, 2.5*cm, 6*cm]) recall_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), DARK), ("TEXTCOLOR", (0, 0), (-1, 0), white), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 9), ("ALIGN", (0, 0), (-1, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("GRID", (0, 0), (-1, -1), 0.5, GRAY), ("BACKGROUND", (0, 1), (-1, -1), LIGHT), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), ])) story.append(recall_table) story.append(Spacer(1, 3*mm)) story.append(Paragraph("4.3 Les 5 outils mémoire", styles["SubTitle"])) tools_data = [ ["Outil", "Appel LLM ?", "Coût", "Usage"], ["honcho_profile", "Non", "minimal", "Lire/mettre à jour la carte du peer (nom, rôle, préférences)"], ["honcho_search", "Non", "faible", "Recherche sémantique dans l'historique"], ["honcho_context", "Non", "faible", "Snapshot complet : résumé + représentation + messages récents"], ["honcho_reasoning", "Oui", "moyen-élevé", "Question synthétisée par le dialectic engine"], ["honcho_conclude", "Non", "minimal", "Écrire/supprimer une conclusion persistante"], ] tools_table = Table(tools_data, colWidths=[3*cm, 2*cm, 2*cm, 7*cm]) tools_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), DARK), ("TEXTCOLOR", (0, 0), (-1, 0), white), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 9), ("ALIGN", (0, 0), (-1, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("GRID", (0, 0), (-1, -1), 0.5, GRAY), ("BACKGROUND", (0, 1), (-1, -1), LIGHT), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), ])) story.append(tools_table) story.append(PageBreak()) # ===== 5. CONFIGURATION HERMES ===== story.append(Paragraph("5. Configuration Hermes", styles["SectionTitle"])) story.append(HRFlowable(width="100%", thickness=0.5, color=ACCENT)) story.append(Spacer(1, 3*mm)) story.append(Paragraph("5.1 Fichier honcho.json", styles["SubTitle"])) story.append(Paragraph( "Chemin : /root/.hermes/honcho.json", styles["Body"] )) story.append(Paragraph( "{
" " \"baseUrl\": \"http://192.168.30.35:8000\",
" " \"peerName\": \"laurent\",
" " \"aiPeer\": \"hermes\",
" " \"workspace\": \"default\",
" " \"recallMode\": \"hybrid\",
" " \"writeFrequency\": \"async\",
" " \"sessionStrategy\": \"per-directory\",
" " \"observation\": {
" " \"user\": { \"observeMe\": true, \"observeOthers\": true },
" " \"ai\": { \"observeMe\": true, \"observeOthers\": true }
" " },
" " \"dialecticReasoningLevel\": \"low\",
" " \"dialecticDepth\": 1,
" " \"contextCadence\": 1,
" " \"dialecticCadence\": 2
" "}", styles["Code"] )) story.append(Paragraph("5.2 Config Hermes", styles["SubTitle"])) story.append(Paragraph( "Dans /root/.hermes/config.yaml :", styles["Body"] )) story.append(Paragraph( "memory:
" " provider: honcho", styles["Code"] )) story.append(Paragraph("5.3 Fichier .env Honcho", styles["SubTitle"])) story.append(Paragraph( "Chemin sur la VM postgres : /root/docker/honcho/.env", styles["Body"] )) story.append(Paragraph( "LOG_LEVEL=INFO
" "AUTH_USE_AUTH=false
" "VECTOR_STORE_TYPE=pgvector
" "LLM_OPENAI_API_KEY=ollama
" "DERIVER_MODEL_CONFIG__TRANSPORT=openai
" "DERIVER_MODEL_CONFIG__MODEL=nemotron-mini:4b
" "DERIVER_MODEL_CONFIG__OVERRIDES__BASE_URL=http://192.168.30.10:11434/v1
" "DERIVER_MODEL_CONFIG__OVERRIDES__API_KEY_ENV=ollama
" "EMBEDDING_MODEL_CONFIG__TRANSPORT=openai
" "EMBEDDING_MODEL_CONFIG__MODEL=nomic-embed-text
" "EMBEDDING_MODEL_CONFIG__OVERRIDES__BASE_URL=http://192.168.30.10:11434/v1
" "EMBEDDING_MODEL_CONFIG__OVERRIDES__API_KEY_ENV=ollama
" "EMBED_MESSAGES=true
" "EMBEDDING_VECTOR_DIMENSIONS=768", styles["Code"] )) story.append(PageBreak()) # ===== 6. COMMANDES UTILES ===== story.append(Paragraph("6. Commandes utiles", styles["SectionTitle"])) story.append(HRFlowable(width="100%", thickness=0.5, color=ACCENT)) story.append(Spacer(1, 3*mm)) story.append(Paragraph("6.1 Sur Hermes (LXC 112)", styles["SubTitle"])) cmds = [ ("hermes honcho status", "Voir l'état de la connexion Honcho"), ("hermes honcho setup", "Lancer le wizard de configuration"), ("hermes honcho enable/disable", "Activer/désactiver Honcho"), ("hermes honcho peer --user ", "Définir le nom du peer utilisateur"), ("hermes honcho mode hybrid|context|tools", "Changer le mode de rappel"), ("hermes honcho sessions", "Lister les sessions connues"), ("hermes honcho map ", "Mapper le répertoire courant à une session"), ("hermes honcho sync", "Créer les host blocks pour tous les profils"), ("hermes memory status", "Voir le provider mémoire actif"), ("hermes memory off", "Désactiver la mémoire externe"), ] for cmd, desc in cmds: story.append(Paragraph(f"{cmd} — {desc}", styles["MyBullet"])) story.append(Paragraph("6.2 Sur la VM postgres (192.168.30.35)", styles["SubTitle"])) cmds2 = [ ("docker compose ps", "Voir l'état des conteneurs"), ("docker compose logs api --tail 30", "Logs de l'API Honcho"), ("docker compose logs deriver --tail 30", "Logs du Deriver"), ("docker compose down && docker compose up -d", "Redémarrer le stack"), ("docker compose down -v && docker compose up -d --build", "Reset complet + rebuild"), ("curl http://localhost:8000/health", "Healthcheck"), ] for cmd, desc in cmds2: story.append(Paragraph(f"{cmd} — {desc}", styles["MyBullet"])) story.append(Paragraph("6.3 Sur Kellerflix (192.168.30.10)", styles["SubTitle"])) cmds3 = [ ("ollama list", "Lister les modèles disponibles"), ("ollama pull ", "Télécharger un modèle"), ("ollama run ", "Tester un modèle en interactif"), ("curl http://localhost:11434/api/tags", "Lister les modèles via API"), ] for cmd, desc in cmds3: story.append(Paragraph(f"{cmd} — {desc}", styles["MyBullet"])) story.append(PageBreak()) # ===== 7. SCHÉMA RÉSEAU ===== story.append(Paragraph("7. Schéma réseau", styles["SectionTitle"])) story.append(HRFlowable(width="100%", thickness=0.5, color=ACCENT)) story.append(Spacer(1, 3*mm)) story.append(Paragraph( "Le schéma ci-dessous représente les flux entre les composants :", styles["Body"] )) story.append(Spacer(1, 3*mm)) # Schéma ASCII schema = """ ┌─────────────────────────────────────────────────────────────┐ │ RÉSEAU 192.168.30.0/24 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────┐ ┌──────────────────────────────┐ │ │ │ Hermes Agent │ │ VM postgres (30.35) │ │ │ │ (LXC 112) │ │ │ │ │ │ │ │ ┌──────────────────────┐ │ │ │ │ honcho-ai SDK │────▶│ │ Honcho API (:8000) │ │ │ │ │ hermes honcho │ │ │ (FastAPI) │ │ │ │ │ │ │ └──────┬───────────────┘ │ │ │ └──────────────────┘ │ │ │ │ │ │ ▼ │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ PostgreSQL pgvector │ │ │ │ │ │ (:5434) │ │ │ │ │ └──────────────────────┘ │ │ │ │ ▲ │ │ │ │ ┌──────┴───────────────┐ │ │ │ │ │ Honcho Deriver │ │ │ │ │ │ (worker background) │ │ │ │ │ └──────┬───────────────┘ │ │ │ └─────────┼────────────────────┘ │ │ │ │ │ ┌─────────┼────────────┐ │ │ │ ▼ │ │ │ │ ┌──────────────┐ │ │ │ │ │ Redis │ │ │ │ │ │ (30.36:6379) │ │ │ │ │ └──────────────┘ │ │ │ └──────────────────────┘ │ │ │ │ │ ┌─────────┼────────────┐ │ │ │ ▼ │ │ │ │ ┌──────────────────┐ │ │ │ │ │ Kellerflix │ │ │ │ │ │ (30.10:11434) │ │ │ │ │ │ Ollama │ │ │ │ │ │ ├─ nemotron-mini│ │ │ │ │ │ └─ nomic-embed │ │ │ │ │ └──────────────────┘ │ │ │ └────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ """ story.append(Paragraph(schema, styles["Body"])) story.append(PageBreak()) # ===== 8. DÉPANNAGE ===== story.append(Paragraph("8. Dépannage", styles["SectionTitle"])) story.append(HRFlowable(width="100%", thickness=0.5, color=ACCENT)) story.append(Spacer(1, 3*mm)) issues = [ ("Problème", "Cause probable", "Solution"), ("Honcho not configured", "memory.provider pas défini", "hermes config set memory.provider honcho"), ("API Honcho injoignable", "Conteneur down", "docker compose ps && docker compose logs api"), ("OpenAI API key required", "LLM_OPENAI_API_KEY manquant", "Ajouter LLM_OPENAI_API_KEY=ollama dans .env"), ("Embedding dim mismatch", "Dimensions changées après init", "docker compose down -v && docker compose up -d --build"), ("Deriver ne tourne pas", "Dépendance api pas healthy", "Vérifier les logs : docker compose logs deriver"), ("Peer conflict", "Peer déjà créé", "Utiliser un nom différent ou supprimer l'ancien"), ("Ollama inaccessible", "OLLAMA_HOST=127.0.0.1", "Vérifier ss -tlnp | grep 11434"), ("Mémoire non persistante", "writeFrequency = session", "Passer à async dans honcho.json"), ] issues_table = Table(issues, colWidths=[3.5*cm, 4*cm, 6.5*cm]) issues_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), DARK), ("TEXTCOLOR", (0, 0), (-1, 0), white), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 8), ("ALIGN", (0, 0), (-1, -1), "LEFT"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("GRID", (0, 0), (-1, -1), 0.5, GRAY), ("BACKGROUND", (0, 1), (-1, -1), LIGHT), ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5), ("LEFTPADDING", (0, 0), (-1, -1), 4), ])) story.append(issues_table) story.append(Spacer(1, 5*mm)) story.append(Paragraph("Liens utiles :", styles["SubTitle"])) story.append(Paragraph( "Documentation Honcho : https://docs.honcho.dev
" "Repo Honcho : https://github.com/plastic-labs/honcho
" "Intégration Hermes : https://docs.honcho.dev/v3/guides/integrations/hermes
" "App Honcho Cloud : https://app.honcho.dev
" "Ollama : https://ollama.com", styles["Body"] )) # Build doc.build(story) print(f"PDF généré : {OUTPUT}") print(f"Taille : {os.path.getsize(OUTPUT)} bytes")