// ObsiGate AI Agent — flux de traitement d'une demande (b89af91) digraph ObsiGateAgent { rankdir=TB; splines=polyline; nodesep=0.35; ranksep=0.45; graph [fontname="DejaVu Sans", fontsize=13, bgcolor="white", pad=0.3]; node [fontname="DejaVu Sans", fontsize=12, style=filled, shape=box, color="#333333", margin="0.16,0.09"]; edge [fontname="DejaVu Sans", fontsize=11, color="#555555", arrowsize=0.8]; // ── Entrée ── user [label="Demande utilisateur\n(frontend bookslm.js)", fillcolor="#dbeafe"]; deep [label="Chip « Deep Research »\n= agent mode + prompt injecté :\n« décomposer les étapes, chercher\ndans le vault, croiser,\nsynthétiser avec sources »", shape=note, fillcolor="#fef3c7"]; api [label="POST /api/bookslm/agent\n→ réponse SSE (thought · tool ·\nconfirmation · message · done)", fillcolor="#e0f2fe"]; prompt [label="Construction du system prompt\nmode directory / documents / general\ncontexte vault pré-chargé (BooksLM :\n≤200 fichiers, ≤200k chars, cache 5 min)\nprotocole agent = function calling natif", fillcolor="#f1f5f9"]; user -> api; deep -> user [style=dashed, label="active", fontsize=10]; api -> prompt; // ── Boucle agent ── subgraph cluster_loop { label="BOUCLE AGENT — backend/agent/loop.py · run_agent()"; style=rounded; color="#e11d48"; penwidth=2; fontsize=13; fontcolor="#e11d48"; llm [label="Appel LLM — chat_completion()\nfournisseur OpenAI-compatible / Gemini\ntemp=0.3 · max_tokens 8192\nschemas des 50 outils exposés\n(si modèle sans tools → retombe chat simple)", fillcolor="#fee2e2"]; decide [label="Le LLM demande des\noutils (tool_calls) ?", shape=diamond, fillcolor="#fff7ed"]; thought [label="note de raisonnement\nintermédiaire → step visible « pensée »", fillcolor="#fef9c3"]; tools [label="Exécution de chaque tool call\nvia call_tool() (voir pipeline)", fillcolor="#dcfce7"]; back [label="résultat (JSON, tronqué ≤100k chars)\najouté à la conversation →\nitération suivante", shape=plaintext]; llm -> decide; decide -> thought [label="oui", color="#e11d48"]; thought -> tools; tools -> llm [label="itération ≤ 10\nquota tools ≤ 25\n(BOOKSLM_MAX_TOOL_CALLS)", color="#e11d48"]; } prompt -> llm; // ── Pipeline call_tool ── subgraph cluster_pipe { label="PIPELINE call_tool() — backend/tools/registry.py · chaque appel passe par là"; style=rounded; color="#7c3aed"; fontsize=13; fontcolor="#7c3aed"; p1 [label="1 · Schéma Pydantic\n+ validation arguments", fillcolor="#ede9fe"]; p2 [label="2 · Rate limit\npar identité + outil", fillcolor="#ede9fe"]; p3 [label="3 · Permission vault\n(+ déstructif si risque\nDANGEROUS)", fillcolor="#ede9fe"]; p4 [label="4 · Risque READ ?\n→ exécution directe", shape=diamond, fillcolor="#f5f3ff"]; p5 [label="PAUSE CONFIRMATION\noutil WRITE/DANGEROUS → event SSE\n« confirmation » avec le plan complet\nbatché (BUG-075) : une seule\napprobation applique toutes les mutations\n(resume via confirm / confirm_all)", fillcolor="#fde68a"]; p6 [label="5 · Handler exécuté\n6 · Audit JSON-lines\n7 · Redaction secrets (redact_payload)", fillcolor="#dcfce7"]; p1 -> p2 -> p3 -> p4; p4 -> p6 [label="read"]; p4 -> p5 [label="mutation", color="#d97706"]; p5 -> p6 [label="approuvé", style=dashed]; } tools -> p1; p6 -> back [label="tool result", style=invis]; p6 -> tools [style=invis]; back -> p1 [style=invis]; // ── Sorties ── done [label="Réponse finale\nréponse sans tool_calls\n→ SSE « message »\n(Markdown + sources citées)", fillcolor="#bbf7d0"]; final [label="Budget épuisé (10 itérations\nou 25 appels) → _finalize_answer :\ndernier appel SANS outils pour forcer\nune synthèse, sinon liste des sources\ndéterministe (jamais de réponse vide)", fillcolor="#fed7aa"]; decide -> done [label="non", color="#059669", penwidth=2]; decide -> final [label="budget atteint", color="#ea580c"]; answer [label="Affichage UI : bloc\n« N étapes » Notion-style\n(pensées + outils dépliables)", fillcolor="#dbeafe"]; done -> answer; final -> answer; // ── Boîte à outils ── subgraph cluster_tools { label="50 OUTILS — backend/tools/*.py · risque READ / WRITE / DANGEROUS"; style=rounded; color="#0369a1"; fontsize=13; fontcolor="#0369a1"; node [fillcolor="#f0f9ff", fontsize=10]; fam_vault [label="VAULT · service.py (22)\nlecture : list_vaults, list_directory,\nlist_all_files, read_file, read_file_raw,\nsearch_fulltext, search_advanced,\nsearch_paths, get_graph, get_backlinks,\nlist_tags, suggest_tags, list_recent,\nlist_backups, diff_backup\nécriture : create_file, create_directory,\nappend_to_file, edit_file, restore_backup\nDANGEROUS : delete_file, delete_directory,\nmove_path, rename_file, rename_directory,\nreplace_in_files"]; fam_web [label="WEB · web.py + crawler.py\nweb_search · fetch_url\ncrawl_site (≤20 pages, même hôte)"]; fam_doc [label="DOCUMENTS · documents.py\ncreate_pdf · create_docx\ncreate_xlsx · create_csv"]; fam_xlsx [label="TABLEURS · spreadsheets.py\nlist_xlsx_sheets · xlsx_to_markdown\nsearch_workbook · analyze_range\nupdate_xlsx_cells · append_xlsx_rows\nedit_xlsx_structure"]; fam_dup [label="DOUBLONS · duplicates.py\nfind_duplicates\nmerge_duplicate_notes (DANG.)"]; fam_git [label="GIT CONNECTÉ · connected.py\ngit_list_repos · git_get_file\ngit_search_issues (Gitea/GitHub)"]; fam_sched [label="SCHEDULER · scheduled.py\nlist/create/delete/run_scheduled_task"]; fam_notify [label="NOTIFICATIONS · notify.py\nnotify_external (Discord,\nTelegram, SMTP, webhook)"]; } p6 -> cluster_tools [style=invis]; fam_vault -> fam_web [style=invis]; fam_web -> fam_doc [style=invis]; fam_doc -> fam_xlsx [style=invis]; fam_xlsx -> fam_dup [style=invis]; fam_dup -> fam_git [style=invis]; fam_git -> fam_sched [style=invis]; fam_sched -> fam_notify [style=invis]; }