chore: versioning initial du systeme Context Continuity

Code du systeme de memoire multi-LLM sur Trilium :
- trilium_api.py : wrapper trilium-py (notes, labels, relations)
- mcp_server.py : serveur MCP Starlette (19 tools, OAuth + Bearer)
- api_context.py : API REST FastAPI
- trilium_context.py : workflow CLI
- watchdog.sh, start_*.sh : supervision et demarrage
- skills, docs et ontologie associes
Secrets (.env, oauth_state.json) exclus via .gitignore.
This commit is contained in:
2026-06-29 11:02:22 +02:00
commit 01629780f4
26 changed files with 5259 additions and 0 deletions
+365
View File
@@ -0,0 +1,365 @@
"""
trilium_context.py — Gestion opérationnelle du contexte LLM via Trilium
Usage :
python trilium_context.py new-conversation --projet SlidingAutomation --llm "Claude Sonnet" --titre "Session render engine"
python trilium_context.py close-session --note-id <ID> --summary "..."
python trilium_context.py generate-context --projet SlidingAutomation --llm-cible "Le Chat Large" [--note-id <ID>]
python trilium_context.py list-backlog --projet SlidingAutomation
python trilium_context.py add-decision --projet SlidingAutomation --enonce "..." --justification "..."
python trilium_context.py add-history --projet SlidingAutomation --type "Test effectué" --enonce "..." --detail "..."
python trilium_context.py list-projects
python trilium_context.py add-skill --titre "Mon skill" --fichier skill.md --portee universel
python trilium_context.py add-skill --titre "Mon skill" --fichier skill.md --portee projet --projet SlidingAutomation
python trilium_context.py list-skills [--projet SlidingAutomation] [--portee universel]
"""
import argparse
import json
import os
import sys
from datetime import datetime
from trilium_api import (check_api, create_note, get_note_id, get_note, get_note_content,
update_note_content, search_by_label, set_label,
get_label_value, find_note_by_title)
IDS_FILE = os.path.join(os.path.dirname(__file__), "trilium_ids.json")
GREEN, YELLOW, RED, BOLD, RESET = "\033[32m", "\033[33m", "\033[31m", "\033[1m", "\033[0m"
def ok(m): print(f" {GREEN}{RESET} {m}")
def warn(m): print(f" {YELLOW}{RESET} {m}")
def err(m): print(f" {RED}{RESET} {m}")
def head(m): print(f"\n{BOLD}{m}{RESET}")
def load_ids() -> dict:
if not os.path.exists(IDS_FILE):
raise SystemExit("❌ trilium_ids.json introuvable. Lance d'abord : python trilium_init.py")
with open(IDS_FILE) as f:
return json.load(f)
def estimate_tokens(text: str) -> int:
return max(1, int(len(text) / 4))
# ---------------------------------------------------------------------------
# Commandes
# ---------------------------------------------------------------------------
def cmd_new_conversation(args):
ids = load_ids()
date = datetime.now().strftime("%Y-%m-%d %H:%M")
titre = f"[{args.llm}] {date}{args.titre}"
content = f"""<h2>{titre}</h2>
<table>
<tr><td><b>Projet</b></td><td>{args.projet}</td></tr>
<tr><td><b>LLM</b></td><td>{args.llm}</td></tr>
<tr><td><b>Date</b></td><td>{date}</td></tr>
<tr><td><b>Synthèse de clôture</b></td><td><i>À remplir en fin de session</i></td></tr>
</table>"""
result = create_note(ids["Conversations"], titre, content)
note_id = result["note"]["noteId"]
set_label(note_id, "type", "conversation")
set_label(note_id, "projet", args.projet)
set_label(note_id, "llm", args.llm)
set_label(note_id, "statut", "en-cours")
set_label(note_id, "date", date)
ok(f"Conversation créée : {titre}")
print(f"\n Note l'ID pour close-session : {BOLD}{note_id}{RESET}\n")
return note_id
def cmd_close_session(args):
date = datetime.now().strftime("%Y-%m-%d %H:%M")
content = get_note_content(args.note_id)
# Remplace le placeholder de synthèse
if "À remplir en fin de session" in content:
content = content.replace(
"<i>À remplir en fin de session</i>",
args.summary.replace("<", "&lt;").replace(">", "&gt;")
)
else:
content += f"\n<h3>Synthèse de clôture ({date})</h3><p>{args.summary}</p>"
update_note_content(args.note_id, content)
set_label(args.note_id, "statut", "clos")
set_label(args.note_id, "syntheseCloture", args.summary[:200])
ok(f"Session clôturée (ID: {args.note_id})")
def cmd_generate_context(args):
ids = load_ids()
date = datetime.now().strftime("%d/%m/%Y")
# Récupérer les données du projet
projets = search_by_label("projet", args.projet)
decisions = search_by_label("type", "decision")
decisions = [d for d in decisions if get_label_value(d["noteId"], "projet") == args.projet
and get_label_value(d["noteId"], "statut") == "active"]
historique = search_by_label("type", "historiqueItem")
historique = [h for h in historique if get_label_value(h["noteId"], "projet") == args.projet
and get_label_value(h["noteId"], "encoreValide") == "true"]
backlog = search_by_label("type", "backlogItem")
backlog = [b for b in backlog if get_label_value(b["noteId"], "projet") == args.projet
and get_label_value(b["noteId"], "statut") not in ("fait", "abandonné")]
glossaire = search_by_label("type", "termeGlossaire")
glossaire = [g for g in glossaire if get_label_value(g["noteId"], "projet") == args.projet]
# Synthèse de la session précédente
last_summary = ""
if args.note_id:
last_summary = get_label_value(args.note_id, "syntheseCloture") or ""
# Construire le briefing
lines = [
f"# REPRISE DE CONTEXTE — {args.projet} — v{args.version}{date}",
f"**LLM cible : {args.llm_cible}**", "",
]
if projets:
p = projets[0]
content = get_note_content(p["noteId"])
# Extraction texte brut simplifié
import re
texte = re.sub(r"<[^>]+>", " ", content).strip()
lines += ["## 1. Projet", texte[:400], ""]
if last_summary:
lines += ["## 2. Où on en était", last_summary, ""]
if decisions:
lines += ["## 3. Décisions actives (ne pas remettre en question)"]
for d in decisions:
lines.append(f"- {d.get('title', '?')}")
lines.append("")
if historique:
lines += ["## 4. Historique — déjà testé / établi (ne pas refaire)"]
for h in historique:
type_h = get_label_value(h["noteId"], "typeHistorique") or ""
lines.append(f"- [{type_h}] {h.get('title', '?')}")
lines.append("")
if glossaire:
lines += ["## 5. Glossaire projet"]
for g in glossaire:
lines.append(f"- **{g.get('title','?')}** : {get_label_value(g['noteId'], 'definition') or '(voir note)'}")
lines.append("")
if backlog:
lines += ["## 6. Backlog actif"]
prio_ordre = {"haute": 0, "moyenne": 1, "basse": 2}
backlog_sorted = sorted(backlog,
key=lambda b: prio_ordre.get(get_label_value(b["noteId"], "priorite") or "basse", 99))
for b in backlog_sorted:
prio = get_label_value(b["noteId"], "priorite") or "?"
statut = get_label_value(b["noteId"], "statut") or "?"
lines.append(f"- [{statut}] [{prio}] {b.get('title','?')}")
lines.append("")
lines += [
"---",
"**Confirme la prise en compte du contexte en 3 lignes :**",
"(a) L'objectif du projet selon ta compréhension",
"(b) La prochaine action concrète",
"(c) Une incertitude ou question que tu identifies",
]
briefing = "\n".join(lines)
tokens = estimate_tokens(briefing)
titre = f"Reprise {args.projet} v{args.version}{date}"
# Créer la note Contexte Reprise dans Trilium
ids = load_ids()
result = create_note(ids["ContextesReprise"], titre, briefing.replace("\n", "<br>"))
ctx_id = result["note"]["noteId"]
set_label(ctx_id, "type", "contexteReprise")
set_label(ctx_id, "projet", args.projet)
set_label(ctx_id, "llmCible", args.llm_cible)
set_label(ctx_id, "version", str(args.version))
set_label(ctx_id, "tokens", str(tokens))
if args.note_id:
set_label(ctx_id, "conversationSource", args.note_id)
head("CONTEXTE DE REPRISE GÉNÉRÉ")
ok(f"Note Trilium : {titre} (ID: {ctx_id})")
ok(f"Tokens estimés : ~{tokens:,}")
print("\n" + ""*60)
print("\n BRIEFING À COPIER-COLLER :\n")
print(briefing)
print("\n" + ""*60)
def cmd_list_backlog(args):
backlog = search_by_label("type", "backlogItem")
items = [b for b in backlog
if get_label_value(b["noteId"], "projet") == args.projet
and get_label_value(b["noteId"], "statut") not in ("fait", "abandonné")]
if not items:
ok("Backlog vide (ou tout est fait).")
return
head(f"BACKLOG — {args.projet} ({len(items)} items actifs)")
emoji = {"haute": "", "moyenne": "", "basse": ""}
prio_ordre = {"haute": 0, "moyenne": 1, "basse": 2}
for b in sorted(items, key=lambda x: prio_ordre.get(
get_label_value(x["noteId"], "priorite") or "basse", 99)):
prio = get_label_value(b["noteId"], "priorite") or "?"
statut = get_label_value(b["noteId"], "statut") or "?"
e = emoji.get(prio, "")
print(f" {e} [{statut}] {b.get('title','?')} [id: {b['noteId']}]")
def cmd_add_decision(args):
ids = load_ids()
date = datetime.now().strftime("%Y-%m-%d")
result = create_note(
ids["Decisions"], args.enonce,
f"<p><b>Justification</b> : {args.justification}</p><p><b>Date</b> : {date}</p>"
)
nid = result["note"]["noteId"]
set_label(nid, "type", "decision")
set_label(nid, "projet", args.projet)
set_label(nid, "statut", "active")
set_label(nid, "llm", args.llm or "")
ok(f"Décision créée : {args.enonce[:60]} (ID: {nid})")
def cmd_add_history(args):
ids = load_ids()
date = datetime.now().strftime("%Y-%m-%d")
result = create_note(
ids["Historique"], args.enonce,
f"<p><b>Type</b> : {args.type_h}</p>"
f"<p><b>Détail</b> : {args.detail or ''}</p>"
f"<p><b>Date</b> : {date}</p>"
)
nid = result["note"]["noteId"]
set_label(nid, "type", "historiqueItem")
set_label(nid, "projet", args.projet)
set_label(nid, "typeHistorique", args.type_h)
set_label(nid, "encoreValide", "true")
ok(f"Historique créé : {args.enonce[:60]} (ID: {nid})")
def cmd_add_backlog(args):
ids = load_ids()
result = create_note(ids["Backlog"], args.titre)
nid = get_note_id(result)
set_label(nid, "type", "backlogItem")
set_label(nid, "projet", args.projet)
set_label(nid, "priorite", args.priorite)
set_label(nid, "statut", "a faire")
ok(f"Backlog item cree : {args.titre[:60]} (ID: {nid})")
def cmd_list_projects(args):
projets = search_by_label("type", "projet")
head(f"PROJETS ({len(projets)} trouvés)")
for p in projets:
statut = get_label_value(p["noteId"], "statut") or "?"
print(f" [{statut}] {p.get('title','?')} [id: {p['noteId']}]")
def cmd_add_skill(args):
if args.portee != "universel" and not args.projet:
err("--projet requis sauf si --portee universel")
sys.exit(1)
ids = load_ids()
existant = find_note_by_title(args.titre, ids["Skills"])
if existant:
err(f"Un skill '{args.titre}' existe déjà (ID: {existant})")
sys.exit(1)
with open(args.fichier, encoding="utf-8") as f:
contenu = f.read()
safe = contenu.replace("<", "&lt;").replace(">", "&gt;")
result = create_note(ids["Skills"], args.titre, f"<pre>{safe}</pre>")
nid = get_note_id(result)
set_label(nid, "type", "skill")
set_label(nid, "portee", args.portee)
if args.projet:
set_label(nid, "projet", args.projet)
ok(f"Skill créé : {args.titre} (ID: {nid})")
def cmd_list_skills(args):
skills = search_by_label("type", "skill")
if args.projet:
skills = [s for s in skills
if get_label_value(s["noteId"], "projet") == args.projet
or get_label_value(s["noteId"], "portee") == "universel"]
if args.portee:
skills = [s for s in skills if get_label_value(s["noteId"], "portee") == args.portee]
if not skills:
ok("Aucun skill trouvé pour ces filtres.")
return
head(f"SKILLS ({len(skills)} trouvés)")
for s in skills:
portee = get_label_value(s["noteId"], "portee") or "?"
projet = get_label_value(s["noteId"], "projet") or ""
print(f" [{portee}] [{projet}] {s.get('title','?')} [id: {s['noteId']}]")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Gestion du contexte LLM multi-modèles via Trilium",
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
p1 = sub.add_parser("new-conversation")
p1.add_argument("--projet", required=True)
p1.add_argument("--llm", required=True)
p1.add_argument("--titre", required=True)
p2 = sub.add_parser("close-session")
p2.add_argument("--note-id", required=True)
p2.add_argument("--summary", required=True)
p3 = sub.add_parser("generate-context")
p3.add_argument("--projet", required=True)
p3.add_argument("--llm-cible", required=True)
p3.add_argument("--note-id", default="")
p3.add_argument("--version", type=int, default=1)
p4 = sub.add_parser("list-backlog")
p4.add_argument("--projet", required=True)
p5 = sub.add_parser("add-decision")
p5.add_argument("--projet", required=True)
p5.add_argument("--enonce", required=True)
p5.add_argument("--justification", default="")
p5.add_argument("--llm", default="")
p6 = sub.add_parser("add-history")
p6.add_argument("--projet", required=True)
p6.add_argument("--type", required=True, dest="type_h",
choices=["Fait etabli", "Test effectue", "Hypothese invalidee", "Contrainte decouverte"])
p6.add_argument("--enonce", required=True)
p6.add_argument("--detail", default="")
p_ab = sub.add_parser("add-backlog", help="Ajoute un item au backlog")
p_ab.add_argument("--projet", required=True)
p_ab.add_argument("--titre", required=True)
p_ab.add_argument("--priorite", default="moyenne",
choices=["haute", "moyenne", "basse"])
sub.add_parser("list-projects")
p_as = sub.add_parser("add-skill", help="Enregistre un skill depuis un fichier")
p_as.add_argument("--titre", required=True)
p_as.add_argument("--fichier", required=True, help="Chemin du fichier .md contenant le skill")
p_as.add_argument("--portee", default="projet",
help="universel | reference-technique | projet | ... (defaut: projet)")
p_as.add_argument("--projet", default="", help="Requis sauf si --portee universel")
p_ls = sub.add_parser("list-skills", help="Liste les skills enregistres")
p_ls.add_argument("--projet", default="")
p_ls.add_argument("--portee", default="")
args = parser.parse_args()
check_api()
{
"new-conversation": cmd_new_conversation,
"close-session": cmd_close_session,
"generate-context": cmd_generate_context,
"list-backlog": cmd_list_backlog,
"add-decision": cmd_add_decision,
"add-history": cmd_add_history,
"add-backlog": cmd_add_backlog,
"list-projects": cmd_list_projects,
"add-skill": cmd_add_skill,
"list-skills": cmd_list_skills,
}[args.command](args)
if __name__ == "__main__":
main()