#!/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")