docs: diagramme de flux de l'agent AI (boucle run_agent + pipeline call_tool + 50 outils)
CI / lint (push) Successful in 3m1s
CI / security (push) Successful in 2m12s
CI / test (push) Successful in 4m56s
CI / build (push) Successful in 2m5s
CI / e2e (push) Failing after 17m3s

This commit is contained in:
2026-10-05 12:59:51 -04:00
parent b89af917b1
commit 69c3817579
2 changed files with 481 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
// 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];
}
+394
View File
@@ -0,0 +1,394 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.4 (0)
-->
<!-- Title: ObsiGateAgent Pages: 1 -->
<svg width="2130pt" height="1034pt"
viewBox="0.00 0.00 2130.20 1033.70" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(21.6 1012.1)">
<title>ObsiGateAgent</title>
<polygon fill="white" stroke="transparent" points="-21.6,21.6 -21.6,-1012.1 2108.6,-1012.1 2108.6,21.6 -21.6,21.6"/>
<g id="clust1" class="cluster">
<title>cluster_loop</title>
<path fill="white" stroke="#e11d48" stroke-width="2" d="M493,-11.5C493,-11.5 1052,-11.5 1052,-11.5 1058,-11.5 1064,-17.5 1064,-23.5 1064,-23.5 1064,-471 1064,-471 1064,-477 1058,-483 1052,-483 1052,-483 493,-483 493,-483 487,-483 481,-477 481,-471 481,-471 481,-23.5 481,-23.5 481,-17.5 487,-11.5 493,-11.5"/>
<text text-anchor="middle" x="772.5" y="-468.6" font-family="DejaVu Sans" font-size="13.00" fill="#e11d48">BOUCLE AGENT — backend/agent/loop.py · run_agent()</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_pipe</title>
<path fill="white" stroke="#7c3aed" d="M1084,-102C1084,-102 1580,-102 1580,-102 1586,-102 1592,-108 1592,-114 1592,-114 1592,-761.5 1592,-761.5 1592,-767.5 1586,-773.5 1580,-773.5 1580,-773.5 1084,-773.5 1084,-773.5 1078,-773.5 1072,-767.5 1072,-761.5 1072,-761.5 1072,-114 1072,-114 1072,-108 1078,-102 1084,-102"/>
<text text-anchor="middle" x="1332" y="-759.1" font-family="DejaVu Sans" font-size="13.00" fill="#7c3aed">PIPELINE call_tool() — backend/tools/registry.py · chaque appel passe par là</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_tools</title>
<path fill="white" stroke="#0369a1" d="M1612,-8C1612,-8 2067,-8 2067,-8 2073,-8 2079,-14 2079,-20 2079,-20 2079,-970.5 2079,-970.5 2079,-976.5 2073,-982.5 2067,-982.5 2067,-982.5 1612,-982.5 1612,-982.5 1606,-982.5 1600,-976.5 1600,-970.5 1600,-970.5 1600,-20 1600,-20 1600,-14 1606,-8 1612,-8"/>
<text text-anchor="middle" x="1839.5" y="-968.1" font-family="DejaVu Sans" font-size="13.00" fill="#0369a1">50 OUTILS — backend/tools/*.py · risque READ / WRITE / DANGEROUS</text>
</g>
<!-- user -->
<g id="node1" class="node">
<title>user</title>
<polygon fill="#dbeafe" stroke="#333333" points="712.5,-743.5 561.5,-743.5 561.5,-705.5 712.5,-705.5 712.5,-743.5"/>
<text text-anchor="middle" x="637" y="-727.9" font-family="DejaVu Sans" font-size="12.00">Demande utilisateur</text>
<text text-anchor="middle" x="637" y="-714.9" font-family="DejaVu Sans" font-size="12.00">(frontend bookslm.js)</text>
</g>
<!-- api -->
<g id="node3" class="node">
<title>api</title>
<polygon fill="#e0f2fe" stroke="#333333" points="742.5,-669 531.5,-669 531.5,-618 742.5,-618 742.5,-669"/>
<text text-anchor="middle" x="637" y="-653.4" font-family="DejaVu Sans" font-size="12.00">POST /api/bookslm/agent</text>
<text text-anchor="middle" x="637" y="-640.4" font-family="DejaVu Sans" font-size="12.00">→ réponse SSE (thought · tool ·</text>
<text text-anchor="middle" x="637" y="-627.4" font-family="DejaVu Sans" font-size="12.00">confirmation · message · done)</text>
</g>
<!-- user&#45;&gt;api -->
<g id="edge1" class="edge">
<title>user&#45;&gt;api</title>
<path fill="none" stroke="#555555" d="M637,-705.18C637,-696.86 637,-686.76 637,-677.2"/>
<polygon fill="#555555" stroke="#555555" points="639.8,-677.01 637,-669.01 634.2,-677.01 639.8,-677.01"/>
</g>
<!-- deep -->
<g id="node2" class="node">
<title>deep</title>
<polygon fill="#fef3c7" stroke="#333333" points="749,-919 519,-919 519,-842 755,-842 755,-913 749,-919"/>
<polyline fill="none" stroke="#333333" points="749,-919 749,-913 "/>
<polyline fill="none" stroke="#333333" points="755,-913 749,-913 "/>
<text text-anchor="middle" x="637" y="-903.4" font-family="DejaVu Sans" font-size="12.00">Chip « Deep Research »</text>
<text text-anchor="middle" x="637" y="-890.4" font-family="DejaVu Sans" font-size="12.00">= agent mode + prompt injecté :</text>
<text text-anchor="middle" x="637" y="-877.4" font-family="DejaVu Sans" font-size="12.00">« décomposer les étapes, chercher</text>
<text text-anchor="middle" x="637" y="-864.4" font-family="DejaVu Sans" font-size="12.00">dans le vault, croiser,</text>
<text text-anchor="middle" x="637" y="-851.4" font-family="DejaVu Sans" font-size="12.00">synthétiser avec sources »</text>
</g>
<!-- deep&#45;&gt;user -->
<g id="edge2" class="edge">
<title>deep&#45;&gt;user</title>
<path fill="none" stroke="#555555" stroke-dasharray="5,2" d="M637,-841.83C637,-813.95 637,-776.67 637,-751.91"/>
<polygon fill="#555555" stroke="#555555" points="639.8,-751.71 637,-743.71 634.2,-751.71 639.8,-751.71"/>
<text text-anchor="middle" x="652.5" y="-784.5" font-family="DejaVu Sans" font-size="10.00">active</text>
</g>
<!-- prompt -->
<g id="node4" class="node">
<title>prompt</title>
<polygon fill="#f1f5f9" stroke="#333333" points="773.5,-585 500.5,-585 500.5,-508 773.5,-508 773.5,-585"/>
<text text-anchor="middle" x="637" y="-569.4" font-family="DejaVu Sans" font-size="12.00">Construction du system prompt</text>
<text text-anchor="middle" x="637" y="-556.4" font-family="DejaVu Sans" font-size="12.00">mode directory / documents / general</text>
<text text-anchor="middle" x="637" y="-543.4" font-family="DejaVu Sans" font-size="12.00">contexte vault pré&#45;chargé (BooksLM :</text>
<text text-anchor="middle" x="637" y="-530.4" font-family="DejaVu Sans" font-size="12.00">≤200 fichiers, ≤200k chars, cache 5 min)</text>
<text text-anchor="middle" x="637" y="-517.4" font-family="DejaVu Sans" font-size="12.00">protocole agent = function calling natif</text>
</g>
<!-- api&#45;&gt;prompt -->
<g id="edge3" class="edge">
<title>api&#45;&gt;prompt</title>
<path fill="none" stroke="#555555" d="M637,-617.54C637,-610.12 637,-601.74 637,-593.43"/>
<polygon fill="#555555" stroke="#555555" points="639.8,-593.17 637,-585.17 634.2,-593.17 639.8,-593.17"/>
</g>
<!-- llm -->
<g id="node5" class="node">
<title>llm</title>
<polygon fill="#fee2e2" stroke="#333333" points="784.5,-453 489.5,-453 489.5,-376 784.5,-376 784.5,-453"/>
<text text-anchor="middle" x="637" y="-437.4" font-family="DejaVu Sans" font-size="12.00">Appel LLM — chat_completion()</text>
<text text-anchor="middle" x="637" y="-424.4" font-family="DejaVu Sans" font-size="12.00">fournisseur OpenAI&#45;compatible / Gemini</text>
<text text-anchor="middle" x="637" y="-411.4" font-family="DejaVu Sans" font-size="12.00">temp=0.3 · max_tokens 8192</text>
<text text-anchor="middle" x="637" y="-398.4" font-family="DejaVu Sans" font-size="12.00">schemas des 50 outils exposés</text>
<text text-anchor="middle" x="637" y="-385.4" font-family="DejaVu Sans" font-size="12.00">(si modèle sans tools → retombe chat simple)</text>
</g>
<!-- prompt&#45;&gt;llm -->
<g id="edge8" class="edge">
<title>prompt&#45;&gt;llm</title>
<path fill="none" stroke="#555555" d="M637,-507.9C637,-493.35 637,-476.55 637,-461.3"/>
<polygon fill="#555555" stroke="#555555" points="639.8,-461.02 637,-453.02 634.2,-461.02 639.8,-461.02"/>
</g>
<!-- decide -->
<g id="node6" class="node">
<title>decide</title>
<polygon fill="#fff7ed" stroke="#333333" points="641,-325 489,-287 641,-249 793,-287 641,-325"/>
<text text-anchor="middle" x="641" y="-290.4" font-family="DejaVu Sans" font-size="12.00">Le LLM demande des</text>
<text text-anchor="middle" x="641" y="-277.4" font-family="DejaVu Sans" font-size="12.00">outils (tool_calls) ?</text>
</g>
<!-- llm&#45;&gt;decide -->
<g id="edge4" class="edge">
<title>llm&#45;&gt;decide</title>
<path fill="none" stroke="#555555" d="M638.2,-375.85C638.63,-362.33 639.12,-346.95 639.57,-332.87"/>
<polygon fill="#555555" stroke="#555555" points="642.37,-332.85 639.83,-324.76 636.77,-332.67 642.37,-332.85"/>
</g>
<!-- thought -->
<g id="node7" class="node">
<title>thought</title>
<polygon fill="#fef9c3" stroke="#333333" points="786.5,-154.5 529.5,-154.5 529.5,-116.5 786.5,-116.5 786.5,-154.5"/>
<text text-anchor="middle" x="658" y="-138.9" font-family="DejaVu Sans" font-size="12.00">note de raisonnement</text>
<text text-anchor="middle" x="658" y="-125.9" font-family="DejaVu Sans" font-size="12.00">intermédiaire → step visible « pensée »</text>
</g>
<!-- decide&#45;&gt;thought -->
<g id="edge5" class="edge">
<title>decide&#45;&gt;thought</title>
<path fill="none" stroke="#e11d48" d="M645.12,-249.81C648.17,-222.91 652.28,-186.83 655.02,-162.68"/>
<polygon fill="#e11d48" stroke="#e11d48" points="657.81,-162.94 655.93,-154.67 652.25,-162.31 657.81,-162.94"/>
<text text-anchor="middle" x="659.5" y="-205.2" font-family="DejaVu Sans" font-size="11.00">oui</text>
</g>
<!-- done -->
<g id="node16" class="node">
<title>done</title>
<polygon fill="#bbf7d0" stroke="#333333" points="473,-167.5 275,-167.5 275,-103.5 473,-103.5 473,-167.5"/>
<text text-anchor="middle" x="374" y="-151.9" font-family="DejaVu Sans" font-size="12.00">Réponse finale</text>
<text text-anchor="middle" x="374" y="-138.9" font-family="DejaVu Sans" font-size="12.00">réponse sans tool_calls</text>
<text text-anchor="middle" x="374" y="-125.9" font-family="DejaVu Sans" font-size="12.00">→ SSE « message »</text>
<text text-anchor="middle" x="374" y="-112.9" font-family="DejaVu Sans" font-size="12.00">(Markdown + sources citées)</text>
</g>
<!-- decide&#45;&gt;done -->
<g id="edge19" class="edge">
<title>decide&#45;&gt;done</title>
<path fill="none" stroke="#059669" stroke-width="2" d="M595.1,-260.3C551.33,-235.79 485.07,-198.69 436.49,-171.49"/>
<polygon fill="#059669" stroke="#059669" stroke-width="2" points="437.81,-169.02 429.46,-167.55 435.07,-173.9 437.81,-169.02"/>
<text text-anchor="middle" x="541.5" y="-205.2" font-family="DejaVu Sans" font-size="11.00">non</text>
</g>
<!-- final -->
<g id="node17" class="node">
<title>final</title>
<polygon fill="#fed7aa" stroke="#333333" points="250,-174 0,-174 0,-97 250,-97 250,-174"/>
<text text-anchor="middle" x="125" y="-158.4" font-family="DejaVu Sans" font-size="12.00">Budget épuisé (10 itérations</text>
<text text-anchor="middle" x="125" y="-145.4" font-family="DejaVu Sans" font-size="12.00">ou 25 appels) → _finalize_answer :</text>
<text text-anchor="middle" x="125" y="-132.4" font-family="DejaVu Sans" font-size="12.00">dernier appel SANS outils pour forcer</text>
<text text-anchor="middle" x="125" y="-119.4" font-family="DejaVu Sans" font-size="12.00">une synthèse, sinon liste des sources</text>
<text text-anchor="middle" x="125" y="-106.4" font-family="DejaVu Sans" font-size="12.00">déterministe (jamais de réponse vide)</text>
</g>
<!-- decide&#45;&gt;final -->
<g id="edge20" class="edge">
<title>decide&#45;&gt;final</title>
<path fill="none" stroke="#ea580c" d="M565.01,-267.84C491.13,-250.18 390,-226 390,-226 390,-226 314.36,-200.46 243.91,-176.66"/>
<polygon fill="#ea580c" stroke="#ea580c" points="244.71,-173.97 236.23,-174.07 242.92,-179.28 244.71,-173.97"/>
<text text-anchor="middle" x="429.5" y="-205.2" font-family="DejaVu Sans" font-size="11.00">budget atteint</text>
</g>
<!-- tools -->
<g id="node8" class="node">
<title>tools</title>
<polygon fill="#dcfce7" stroke="#333333" points="987,-57.5 787,-57.5 787,-19.5 987,-19.5 987,-57.5"/>
<text text-anchor="middle" x="887" y="-41.9" font-family="DejaVu Sans" font-size="12.00">Exécution de chaque tool call</text>
<text text-anchor="middle" x="887" y="-28.9" font-family="DejaVu Sans" font-size="12.00">via call_tool() (voir pipeline)</text>
</g>
<!-- thought&#45;&gt;tools -->
<g id="edge6" class="edge">
<title>thought&#45;&gt;tools</title>
<path fill="none" stroke="#555555" d="M701.61,-116.41C739.89,-100.53 795.6,-77.42 835.95,-60.68"/>
<polygon fill="#555555" stroke="#555555" points="837.23,-63.18 843.55,-57.53 835.09,-58 837.23,-63.18"/>
</g>
<!-- tools&#45;&gt;llm -->
<g id="edge7" class="edge">
<title>tools&#45;&gt;llm</title>
<path fill="none" stroke="#e11d48" d="M881.93,-57.8C864.2,-121.83 806,-332 806,-332 806,-332 764.31,-352.1 722.35,-372.34"/>
<polygon fill="#e11d48" stroke="#e11d48" points="721.06,-369.85 715.07,-375.85 723.49,-374.9 721.06,-369.85"/>
<text text-anchor="middle" x="930" y="-217.2" font-family="DejaVu Sans" font-size="11.00">itération ≤ 10</text>
<text text-anchor="middle" x="930" y="-205.2" font-family="DejaVu Sans" font-size="11.00">quota tools ≤ 25</text>
<text text-anchor="middle" x="930" y="-193.2" font-family="DejaVu Sans" font-size="11.00">(BOOKSLM_MAX_TOOL_CALLS)</text>
</g>
<!-- p1 -->
<g id="node10" class="node">
<title>p1</title>
<polygon fill="#ede9fe" stroke="#333333" points="1244,-743.5 1080,-743.5 1080,-705.5 1244,-705.5 1244,-743.5"/>
<text text-anchor="middle" x="1162" y="-727.9" font-family="DejaVu Sans" font-size="12.00">1 · Schéma Pydantic</text>
<text text-anchor="middle" x="1162" y="-714.9" font-family="DejaVu Sans" font-size="12.00">+ validation arguments</text>
</g>
<!-- tools&#45;&gt;p1 -->
<g id="edge15" class="edge">
<title>tools&#45;&gt;p1</title>
<path fill="none" stroke="#555555" d="M927.18,-57.59C988.4,-85.16 1098,-134.5 1098,-134.5 1098,-644.5 1098,-644.5 1098,-644.5 1098,-644.5 1123.27,-675.7 1142.06,-698.88"/>
<polygon fill="#555555" stroke="#555555" points="1139.94,-700.72 1147.16,-705.18 1144.3,-697.2 1139.94,-700.72"/>
</g>
<!-- back -->
<g id="node9" class="node">
<title>back</title>
<polygon fill="#333333" stroke="transparent" points="1056,-440 810,-440 810,-389 1056,-389 1056,-440"/>
<text text-anchor="middle" x="933" y="-424.4" font-family="DejaVu Sans" font-size="12.00">résultat (JSON, tronqué ≤100k chars)</text>
<text text-anchor="middle" x="933" y="-411.4" font-family="DejaVu Sans" font-size="12.00">ajouté à la conversation →</text>
<text text-anchor="middle" x="933" y="-398.4" font-family="DejaVu Sans" font-size="12.00">itération suivante</text>
</g>
<!-- back&#45;&gt;p1 -->
<!-- p2 -->
<g id="node11" class="node">
<title>p2</title>
<polygon fill="#ede9fe" stroke="#333333" points="1282,-662.5 1146,-662.5 1146,-624.5 1282,-624.5 1282,-662.5"/>
<text text-anchor="middle" x="1214" y="-646.9" font-family="DejaVu Sans" font-size="12.00">2 · Rate limit</text>
<text text-anchor="middle" x="1214" y="-633.9" font-family="DejaVu Sans" font-size="12.00">par identité + outil</text>
</g>
<!-- p1&#45;&gt;p2 -->
<g id="edge9" class="edge">
<title>p1&#45;&gt;p2</title>
<path fill="none" stroke="#555555" d="M1174.06,-705.18C1181.02,-694.61 1189.86,-681.17 1197.46,-669.62"/>
<polygon fill="#555555" stroke="#555555" points="1199.95,-670.95 1202,-662.72 1195.27,-667.87 1199.95,-670.95"/>
</g>
<!-- p3 -->
<g id="node12" class="node">
<title>p3</title>
<polygon fill="#ede9fe" stroke="#333333" points="1321.5,-572 1168.5,-572 1168.5,-521 1321.5,-521 1321.5,-572"/>
<text text-anchor="middle" x="1245" y="-556.4" font-family="DejaVu Sans" font-size="12.00">3 · Permission vault</text>
<text text-anchor="middle" x="1245" y="-543.4" font-family="DejaVu Sans" font-size="12.00">(+ déstructif si risque</text>
<text text-anchor="middle" x="1245" y="-530.4" font-family="DejaVu Sans" font-size="12.00">DANGEROUS)</text>
</g>
<!-- p2&#45;&gt;p3 -->
<g id="edge10" class="edge">
<title>p2&#45;&gt;p3</title>
<path fill="none" stroke="#555555" d="M1219.98,-624.18C1224.04,-611.72 1229.54,-594.88 1234.35,-580.15"/>
<polygon fill="#555555" stroke="#555555" points="1237.11,-580.7 1236.93,-572.23 1231.79,-578.96 1237.11,-580.7"/>
</g>
<!-- p4 -->
<g id="node13" class="node">
<title>p4</title>
<polygon fill="#f5f3ff" stroke="#333333" points="1278,-452.5 1136,-414.5 1278,-376.5 1420,-414.5 1278,-452.5"/>
<text text-anchor="middle" x="1278" y="-417.9" font-family="DejaVu Sans" font-size="12.00">4 · Risque READ ?</text>
<text text-anchor="middle" x="1278" y="-404.9" font-family="DejaVu Sans" font-size="12.00">→ exécution directe</text>
</g>
<!-- p3&#45;&gt;p4 -->
<g id="edge11" class="edge">
<title>p3&#45;&gt;p4</title>
<path fill="none" stroke="#555555" d="M1251.28,-520.74C1255.74,-503.18 1261.84,-479.16 1267.08,-458.5"/>
<polygon fill="#555555" stroke="#555555" points="1269.87,-458.92 1269.12,-450.48 1264.44,-457.54 1269.87,-458.92"/>
</g>
<!-- p5 -->
<g id="node14" class="node">
<title>p5</title>
<polygon fill="#fde68a" stroke="#333333" points="1413.5,-332 1136.5,-332 1136.5,-242 1413.5,-242 1413.5,-332"/>
<text text-anchor="middle" x="1275" y="-316.4" font-family="DejaVu Sans" font-size="12.00">PAUSE CONFIRMATION</text>
<text text-anchor="middle" x="1275" y="-303.4" font-family="DejaVu Sans" font-size="12.00">outil WRITE/DANGEROUS → event SSE</text>
<text text-anchor="middle" x="1275" y="-290.4" font-family="DejaVu Sans" font-size="12.00">« confirmation » avec le plan complet</text>
<text text-anchor="middle" x="1275" y="-277.4" font-family="DejaVu Sans" font-size="12.00">batché (BUG&#45;075) : une seule</text>
<text text-anchor="middle" x="1275" y="-264.4" font-family="DejaVu Sans" font-size="12.00">approbation applique toutes les mutations</text>
<text text-anchor="middle" x="1275" y="-251.4" font-family="DejaVu Sans" font-size="12.00">(resume via confirm / confirm_all)</text>
</g>
<!-- p4&#45;&gt;p5 -->
<g id="edge13" class="edge">
<title>p4&#45;&gt;p5</title>
<path fill="none" stroke="#d97706" d="M1277.12,-376.52C1276.85,-365.25 1276.54,-352.62 1276.26,-340.52"/>
<polygon fill="#d97706" stroke="#d97706" points="1279.05,-340.18 1276.06,-332.25 1273.45,-340.31 1279.05,-340.18"/>
<text text-anchor="middle" x="1301" y="-351.2" font-family="DejaVu Sans" font-size="11.00">mutation</text>
</g>
<!-- p6 -->
<g id="node15" class="node">
<title>p6</title>
<polygon fill="#dcfce7" stroke="#333333" points="1395.5,-161 1142.5,-161 1142.5,-110 1395.5,-110 1395.5,-161"/>
<text text-anchor="middle" x="1269" y="-145.4" font-family="DejaVu Sans" font-size="12.00">5 · Handler exécuté</text>
<text text-anchor="middle" x="1269" y="-132.4" font-family="DejaVu Sans" font-size="12.00">6 · Audit JSON&#45;lines</text>
<text text-anchor="middle" x="1269" y="-119.4" font-family="DejaVu Sans" font-size="12.00">7 · Redaction secrets (redact_payload)</text>
</g>
<!-- p4&#45;&gt;p6 -->
<g id="edge12" class="edge">
<title>p4&#45;&gt;p6</title>
<path fill="none" stroke="#555555" d="M1323.61,-388.38C1367.1,-364.44 1426,-332 1426,-332 1426,-332 1426,-242 1426,-242 1426,-242 1358.97,-196.96 1312.51,-165.73"/>
<polygon fill="#555555" stroke="#555555" points="1314.01,-163.37 1305.81,-161.24 1310.89,-168.02 1314.01,-163.37"/>
<text text-anchor="middle" x="1438.5" y="-284.2" font-family="DejaVu Sans" font-size="11.00">read</text>
</g>
<!-- p5&#45;&gt;p6 -->
<g id="edge14" class="edge">
<title>p5&#45;&gt;p6</title>
<path fill="none" stroke="#555555" stroke-dasharray="5,2" d="M1273.23,-241.95C1272.3,-218.69 1271.17,-190.62 1270.32,-169.29"/>
<polygon fill="#555555" stroke="#555555" points="1273.11,-169.06 1269.99,-161.18 1267.51,-169.28 1273.11,-169.06"/>
<text text-anchor="middle" x="1298" y="-205.2" font-family="DejaVu Sans" font-size="11.00">approuvé</text>
</g>
<!-- p6&#45;&gt;tools -->
<!-- p6&#45;&gt;back -->
<!-- cluster_tools -->
<g id="node27" class="node">
<title>cluster_tools</title>
<polygon fill="#333333" stroke="#333333" points="1319,-56.5 1219,-56.5 1219,-20.5 1319,-20.5 1319,-56.5"/>
<text text-anchor="middle" x="1269" y="-35.4" font-family="DejaVu Sans" font-size="12.00">cluster_tools</text>
</g>
<!-- p6&#45;&gt;cluster_tools -->
<!-- answer -->
<g id="node18" class="node">
<title>answer</title>
<polygon fill="#dbeafe" stroke="#333333" points="347,-64 151,-64 151,-13 347,-13 347,-64"/>
<text text-anchor="middle" x="249" y="-48.4" font-family="DejaVu Sans" font-size="12.00">Affichage UI : bloc</text>
<text text-anchor="middle" x="249" y="-35.4" font-family="DejaVu Sans" font-size="12.00">« N étapes » Notion&#45;style</text>
<text text-anchor="middle" x="249" y="-22.4" font-family="DejaVu Sans" font-size="12.00">(pensées + outils dépliables)</text>
</g>
<!-- done&#45;&gt;answer -->
<g id="edge21" class="edge">
<title>done&#45;&gt;answer</title>
<path fill="none" stroke="#555555" d="M333.07,-103.39C318.5,-92.32 302.14,-79.89 287.76,-68.96"/>
<polygon fill="#555555" stroke="#555555" points="289.37,-66.66 281.3,-64.05 285.98,-71.12 289.37,-66.66"/>
</g>
<!-- final&#45;&gt;answer -->
<g id="edge22" class="edge">
<title>final&#45;&gt;answer</title>
<path fill="none" stroke="#555555" d="M174.01,-96.95C186.01,-87.76 198.68,-78.05 210.13,-69.28"/>
<polygon fill="#555555" stroke="#555555" points="212.07,-71.32 216.71,-64.24 208.66,-66.88 212.07,-71.32"/>
</g>
<!-- fam_vault -->
<g id="node19" class="node">
<title>fam_vault</title>
<polygon fill="#f0f9ff" stroke="#333333" points="1958,-952.5 1720,-952.5 1720,-808.5 1958,-808.5 1958,-952.5"/>
<text text-anchor="middle" x="1839" y="-938.5" font-family="DejaVu Sans" font-size="10.00">VAULT · service.py (22)</text>
<text text-anchor="middle" x="1839" y="-927.5" font-family="DejaVu Sans" font-size="10.00">lecture : list_vaults, list_directory,</text>
<text text-anchor="middle" x="1839" y="-916.5" font-family="DejaVu Sans" font-size="10.00">list_all_files, read_file, read_file_raw,</text>
<text text-anchor="middle" x="1839" y="-905.5" font-family="DejaVu Sans" font-size="10.00">search_fulltext, search_advanced,</text>
<text text-anchor="middle" x="1839" y="-894.5" font-family="DejaVu Sans" font-size="10.00">search_paths, get_graph, get_backlinks,</text>
<text text-anchor="middle" x="1839" y="-883.5" font-family="DejaVu Sans" font-size="10.00">list_tags, suggest_tags, list_recent,</text>
<text text-anchor="middle" x="1839" y="-872.5" font-family="DejaVu Sans" font-size="10.00">list_backups, diff_backup</text>
<text text-anchor="middle" x="1839" y="-861.5" font-family="DejaVu Sans" font-size="10.00">écriture : create_file, create_directory,</text>
<text text-anchor="middle" x="1839" y="-850.5" font-family="DejaVu Sans" font-size="10.00">append_to_file, edit_file, restore_backup</text>
<text text-anchor="middle" x="1839" y="-839.5" font-family="DejaVu Sans" font-size="10.00">DANGEROUS : delete_file, delete_directory,</text>
<text text-anchor="middle" x="1839" y="-828.5" font-family="DejaVu Sans" font-size="10.00">move_path, rename_file, rename_directory,</text>
<text text-anchor="middle" x="1839" y="-817.5" font-family="DejaVu Sans" font-size="10.00">replace_in_files</text>
</g>
<!-- fam_web -->
<g id="node20" class="node">
<title>fam_web</title>
<polygon fill="#f0f9ff" stroke="#333333" points="1938.5,-747 1739.5,-747 1739.5,-702 1938.5,-702 1938.5,-747"/>
<text text-anchor="middle" x="1839" y="-733" font-family="DejaVu Sans" font-size="10.00">WEB · web.py + crawler.py</text>
<text text-anchor="middle" x="1839" y="-722" font-family="DejaVu Sans" font-size="10.00">web_search · fetch_url</text>
<text text-anchor="middle" x="1839" y="-711" font-family="DejaVu Sans" font-size="10.00">crawl_site (≤20 pages, même hôte)</text>
</g>
<!-- fam_vault&#45;&gt;fam_web -->
<!-- fam_doc -->
<g id="node21" class="node">
<title>fam_doc</title>
<polygon fill="#f0f9ff" stroke="#333333" points="1922,-666 1756,-666 1756,-621 1922,-621 1922,-666"/>
<text text-anchor="middle" x="1839" y="-652" font-family="DejaVu Sans" font-size="10.00">DOCUMENTS · documents.py</text>
<text text-anchor="middle" x="1839" y="-641" font-family="DejaVu Sans" font-size="10.00">create_pdf · create_docx</text>
<text text-anchor="middle" x="1839" y="-630" font-family="DejaVu Sans" font-size="10.00">create_xlsx · create_csv</text>
</g>
<!-- fam_web&#45;&gt;fam_doc -->
<!-- fam_xlsx -->
<g id="node22" class="node">
<title>fam_xlsx</title>
<polygon fill="#f0f9ff" stroke="#333333" points="1944.5,-580 1733.5,-580 1733.5,-513 1944.5,-513 1944.5,-580"/>
<text text-anchor="middle" x="1839" y="-566" font-family="DejaVu Sans" font-size="10.00">TABLEURS · spreadsheets.py</text>
<text text-anchor="middle" x="1839" y="-555" font-family="DejaVu Sans" font-size="10.00">list_xlsx_sheets · xlsx_to_markdown</text>
<text text-anchor="middle" x="1839" y="-544" font-family="DejaVu Sans" font-size="10.00">search_workbook · analyze_range</text>
<text text-anchor="middle" x="1839" y="-533" font-family="DejaVu Sans" font-size="10.00">update_xlsx_cells · append_xlsx_rows</text>
<text text-anchor="middle" x="1839" y="-522" font-family="DejaVu Sans" font-size="10.00">edit_xlsx_structure</text>
</g>
<!-- fam_doc&#45;&gt;fam_xlsx -->
<!-- fam_dup -->
<g id="node23" class="node">
<title>fam_dup</title>
<polygon fill="#f0f9ff" stroke="#333333" points="1929.5,-437 1748.5,-437 1748.5,-392 1929.5,-392 1929.5,-437"/>
<text text-anchor="middle" x="1839" y="-423" font-family="DejaVu Sans" font-size="10.00">DOUBLONS · duplicates.py</text>
<text text-anchor="middle" x="1839" y="-412" font-family="DejaVu Sans" font-size="10.00">find_duplicates</text>
<text text-anchor="middle" x="1839" y="-401" font-family="DejaVu Sans" font-size="10.00">merge_duplicate_notes (DANG.)</text>
</g>
<!-- fam_xlsx&#45;&gt;fam_dup -->
<!-- fam_git -->
<g id="node24" class="node">
<title>fam_git</title>
<polygon fill="#f0f9ff" stroke="#333333" points="1931,-309.5 1747,-309.5 1747,-264.5 1931,-264.5 1931,-309.5"/>
<text text-anchor="middle" x="1839" y="-295.5" font-family="DejaVu Sans" font-size="10.00">GIT CONNECTÉ · connected.py</text>
<text text-anchor="middle" x="1839" y="-284.5" font-family="DejaVu Sans" font-size="10.00">git_list_repos · git_get_file</text>
<text text-anchor="middle" x="1839" y="-273.5" font-family="DejaVu Sans" font-size="10.00">git_search_issues (Gitea/GitHub)</text>
</g>
<!-- fam_dup&#45;&gt;fam_git -->
<!-- fam_sched -->
<g id="node25" class="node">
<title>fam_sched</title>
<polygon fill="#f0f9ff" stroke="#333333" points="1942.5,-153.5 1735.5,-153.5 1735.5,-117.5 1942.5,-117.5 1942.5,-153.5"/>
<text text-anchor="middle" x="1839" y="-138.5" font-family="DejaVu Sans" font-size="10.00">SCHEDULER · scheduled.py</text>
<text text-anchor="middle" x="1839" y="-127.5" font-family="DejaVu Sans" font-size="10.00">list/create/delete/run_scheduled_task</text>
</g>
<!-- fam_git&#45;&gt;fam_sched -->
<!-- fam_notify -->
<g id="node26" class="node">
<title>fam_notify</title>
<polygon fill="#f0f9ff" stroke="#333333" points="1917.5,-61 1760.5,-61 1760.5,-16 1917.5,-16 1917.5,-61"/>
<text text-anchor="middle" x="1839" y="-47" font-family="DejaVu Sans" font-size="10.00">NOTIFICATIONS · notify.py</text>
<text text-anchor="middle" x="1839" y="-36" font-family="DejaVu Sans" font-size="10.00">notify_external (Discord,</text>
<text text-anchor="middle" x="1839" y="-25" font-family="DejaVu Sans" font-size="10.00">Telegram, SMTP, webhook)</text>
</g>
<!-- fam_sched&#45;&gt;fam_notify -->
</g>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB