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
+677
View File
@@ -0,0 +1,677 @@
"""
api_context.py — API FastAPI pour Context Continuity
Expose les objets Trilium (Projet, Backlog, Decision, Historique,
Conversation, Glossaire, ContexteReprise) via REST.
Usage :
uvicorn api_context:app --host 127.0.0.1 --port 5000
Doc : http://localhost:5000/api/docs
"""
import os
import re
from typing import Optional, List
from datetime import datetime
from dotenv import load_dotenv
from fastapi import FastAPI, Depends, HTTPException, Header
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
load_dotenv()
API_KEY_CLAUDE = os.getenv("API_KEY_CLAUDE", "")
API_KEY_LECHAT = os.getenv("API_KEY_LECHAT", "")
VALID_KEYS = {API_KEY_CLAUDE: "Claude", API_KEY_LECHAT: "LeChat"}
app = FastAPI(
title="Context Continuity API",
description="API REST pour la gestion du contexte multi-LLM via Trilium",
version="1.0.0",
docs_url="/api/docs",
redoc_url="/api/redoc",
openapi_url="/api/openapi.json",
)
# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------
def get_llm(authorization: str = Header(...)) -> str:
"""Valide la clé API et retourne le nom du LLM appelant."""
key = authorization.replace("Bearer ", "").strip()
if key not in VALID_KEYS:
raise HTTPException(status_code=401, detail="Clé API invalide")
return VALID_KEYS[key]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def strip_html(text: str) -> str:
text = re.sub(r"<br\s*/?>", "\n", text)
text = re.sub(r"<[^>]+>", "", text)
return text.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">").strip()
def note_to_dict(note: dict, with_content: bool = False) -> dict:
from trilium_api import get_note, get_note_content, get_label_value
nid = note.get("noteId", note.get("id", ""))
detail = get_note(nid)
attrs = detail.get("attributes", [])
labels = {a["name"]: a["value"] for a in attrs if a.get("type") == "label"}
result = {
"id": nid,
"title": detail.get("title", ""),
"labels": labels,
"created": detail.get("utcDateCreated", ""),
"modified": detail.get("utcDateModified", ""),
}
if with_content:
result["content"] = strip_html(get_note_content(nid))
return result
# ---------------------------------------------------------------------------
# Modèles Pydantic
# ---------------------------------------------------------------------------
class ProjetCreate(BaseModel):
nom: str = Field(..., description="Nom du projet (sans espaces)")
objectif: str = Field(..., description="Objectif en 1-2 phrases")
stack: Optional[str] = Field(None, description="Stack technique")
llm_repartition: Optional[str] = Field(None, description="Répartition LLMs")
class ProjetPatch(BaseModel):
statut: Optional[str] = Field(None, pattern="^(actif|en-pause|archive)$")
objectif: Optional[str] = None
stack: Optional[str] = None
class BacklogCreate(BaseModel):
projet: str
titre: str
priorite: str = Field("moyenne", pattern="^(haute|moyenne|basse)$")
class BacklogPatch(BaseModel):
statut: Optional[str] = Field(None, pattern="^(a faire|en cours|bloque|fait|abandonne)$")
priorite: Optional[str] = Field(None, pattern="^(haute|moyenne|basse)$")
class DecisionCreate(BaseModel):
projet: str
enonce: str
justification: Optional[str] = None
llm: Optional[str] = None
class DecisionPatch(BaseModel):
statut: Optional[str] = Field(None, pattern="^(active|revisee|annulee)$")
justification: Optional[str] = None
class HistoriqueCreate(BaseModel):
projet: str
enonce: str
type_historique: str = Field(..., alias="type",
pattern="^(Fait etabli|Test effectue|Hypothese invalidee|Contrainte decouverte)$")
detail: Optional[str] = None
model_config = {"populate_by_name": True}
class HistoriquePatch(BaseModel):
encore_valide: Optional[bool] = None
detail: Optional[str] = None
class ConversationCreate(BaseModel):
projet: str
titre: str
llm: str = Field(..., pattern="^(Claude Sonnet|Claude Opus|Le Chat Large|Le Chat Medium)$")
class ConversationPatch(BaseModel):
synthese_cloture: Optional[str] = Field(None, max_length=500)
statut: Optional[str] = Field(None, pattern="^(en-cours|clos)$")
class GlossaireCreate(BaseModel):
projet: str
terme: str
definition: str
synonymes_eviter: Optional[str] = None
class GlossairePatch(BaseModel):
definition: Optional[str] = None
synonymes_eviter: Optional[str] = None
class SkillCreate(BaseModel):
titre: str
contenu: str
portee: str = "projet" # "universel" | "reference-technique" | "projet" | autre libellé libre
projet: Optional[str] = None # optionnel si portee="universel"
class SkillPatch(BaseModel):
contenu: Optional[str] = None
portee: Optional[str] = None
# ---------------------------------------------------------------------------
# Routes — Health
# ---------------------------------------------------------------------------
@app.get("/api/health", tags=["Système"])
def health():
return {"status": "ok", "version": "1.0.0"}
# ---------------------------------------------------------------------------
# Routes — Projets
# ---------------------------------------------------------------------------
@app.get("/api/projets", tags=["Projets"])
def list_projets(llm: str = Depends(get_llm)):
from trilium_api import search_by_label
notes = search_by_label("type", "projet")
# Exclure la note racine Context Continuity
notes = [n for n in notes if n.get("title") != "Context Continuity"]
return [note_to_dict(n) for n in notes]
@app.get("/api/projets/{nom}", tags=["Projets"])
def get_projet(nom: str, llm: str = Depends(get_llm)):
from trilium_api import search_by_label
notes = search_by_label("projet", nom)
projets = [n for n in notes if n.get("title", "").replace(" ", "") == nom or
any(a.get("name") == "projet" and a.get("value") == nom
for a in n.get("attributes", []))]
# Chercher la note projet spécifique
from trilium_api import search_notes, get_note, get_label_value
all_projets = search_by_label("type", "projet")
for p in all_projets:
if get_label_value(p["noteId"], "projet") == nom:
return note_to_dict(p, with_content=True)
raise HTTPException(status_code=404, detail=f"Projet '{nom}' introuvable")
@app.post("/api/projets", tags=["Projets"], status_code=201)
def create_projet(body: ProjetCreate, llm: str = Depends(get_llm)):
import json
from trilium_api import create_note, get_note_id, set_label, update_note_content
with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f:
ids = json.load(f)
res = create_note(ids["Projets"], body.nom)
nid = get_note_id(res)
set_label(nid, "type", "projet")
set_label(nid, "projet", body.nom)
set_label(nid, "statut", "actif")
content = f"<h2>{body.nom}</h2><p><b>Objectif</b> : {body.objectif}</p>"
if body.stack:
content += f"<p><b>Stack</b> : {body.stack}</p>"
if body.llm_repartition:
content += f"<p><b>LLM</b> : {body.llm_repartition}</p>"
update_note_content(nid, content)
return {"id": nid, "nom": body.nom, "created_by": llm}
@app.patch("/api/projets/{note_id}", tags=["Projets"])
def patch_projet(note_id: str, body: ProjetPatch, llm: str = Depends(get_llm)):
from trilium_api import get_note, set_label, update_note_content, get_note_content
try:
get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Projet introuvable")
if body.statut:
set_label(note_id, "statut", body.statut)
if body.objectif or body.stack:
content = get_note_content(note_id)
if body.objectif:
content += f"<p><b>Objectif mis à jour</b> : {body.objectif}</p>"
if body.stack:
content += f"<p><b>Stack</b> : {body.stack}</p>"
update_note_content(note_id, content)
return {"id": note_id, "updated_by": llm}
# ---------------------------------------------------------------------------
# Routes — Backlog
# ---------------------------------------------------------------------------
@app.get("/api/backlog/{projet}", tags=["Backlog"])
def list_backlog(projet: str, llm: str = Depends(get_llm)):
from trilium_api import search_by_label, get_label_value
notes = search_by_label("type", "backlogItem")
actifs = [n for n in notes
if get_label_value(n["noteId"], "projet") == projet
and get_label_value(n["noteId"], "statut") not in ("fait", "abandonne")]
prio_ordre = {"haute": 0, "moyenne": 1, "basse": 2}
actifs.sort(key=lambda n: prio_ordre.get(
get_label_value(n["noteId"], "priorite") or "basse", 99))
return [note_to_dict(n) for n in actifs]
@app.post("/api/backlog", tags=["Backlog"], status_code=201)
def create_backlog(body: BacklogCreate, llm: str = Depends(get_llm)):
import json
from trilium_api import create_note, get_note_id, set_label
with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f:
ids = json.load(f)
res = create_note(ids["Backlog"], body.titre)
nid = get_note_id(res)
set_label(nid, "type", "backlogItem")
set_label(nid, "projet", body.projet)
set_label(nid, "priorite", body.priorite)
set_label(nid, "statut", "a faire")
return {"id": nid, "titre": body.titre, "created_by": llm}
@app.patch("/api/backlog/{note_id}", tags=["Backlog"])
def patch_backlog(note_id: str, body: BacklogPatch, llm: str = Depends(get_llm)):
from trilium_api import get_note, set_label
try:
get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Backlog item introuvable")
if body.statut:
set_label(note_id, "statut", body.statut)
if body.priorite:
set_label(note_id, "priorite", body.priorite)
return {"id": note_id, "updated_by": llm}
# ---------------------------------------------------------------------------
# Routes — Décisions
# ---------------------------------------------------------------------------
@app.get("/api/decisions/{projet}", tags=["Décisions"])
def list_decisions(projet: str, llm: str = Depends(get_llm)):
from trilium_api import search_by_label, get_label_value
notes = search_by_label("type", "decision")
actives = [n for n in notes
if get_label_value(n["noteId"], "projet") == projet
and get_label_value(n["noteId"], "statut") == "active"]
return [note_to_dict(n) for n in actives]
@app.post("/api/decisions", tags=["Décisions"], status_code=201)
def create_decision(body: DecisionCreate, llm: str = Depends(get_llm)):
import json
from trilium_api import create_note, get_note_id, set_label
with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f:
ids = json.load(f)
res = create_note(ids["Decisions"], body.enonce,
content=f"<p><b>Justification</b> : {body.justification or '-'}</p>")
nid = get_note_id(res)
set_label(nid, "type", "decision")
set_label(nid, "projet", body.projet)
set_label(nid, "statut", "active")
set_label(nid, "llm", body.llm or llm)
return {"id": nid, "enonce": body.enonce, "created_by": llm}
@app.patch("/api/decisions/{note_id}", tags=["Décisions"])
def patch_decision(note_id: str, body: DecisionPatch, llm: str = Depends(get_llm)):
from trilium_api import get_note, set_label, get_note_content, update_note_content
try:
get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Décision introuvable")
if body.statut:
set_label(note_id, "statut", body.statut)
if body.justification:
content = get_note_content(note_id)
content += f"<p><b>Révision ({llm})</b> : {body.justification}</p>"
update_note_content(note_id, content)
return {"id": note_id, "updated_by": llm}
# ---------------------------------------------------------------------------
# Routes — Historique
# ---------------------------------------------------------------------------
@app.get("/api/historique/{projet}", tags=["Historique"])
def list_historique(projet: str, llm: str = Depends(get_llm)):
from trilium_api import search_by_label, get_label_value
notes = search_by_label("type", "historiqueItem")
valides = [n for n in notes
if get_label_value(n["noteId"], "projet") == projet
and get_label_value(n["noteId"], "encoreValide") == "true"]
return [note_to_dict(n) for n in valides]
@app.post("/api/historique", tags=["Historique"], status_code=201)
def create_historique(body: HistoriqueCreate, llm: str = Depends(get_llm)):
import json
from trilium_api import create_note, get_note_id, set_label
with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f:
ids = json.load(f)
type_h = body.type_historique
res = create_note(ids["Historique"], body.enonce,
content=f"<p><b>Type</b> : {type_h}</p>"
f"<p><b>Détail</b> : {body.detail or '-'}</p>")
nid = get_note_id(res)
set_label(nid, "type", "historiqueItem")
set_label(nid, "projet", body.projet)
set_label(nid, "typeHistorique", type_h)
set_label(nid, "encoreValide", "true")
return {"id": nid, "enonce": body.enonce, "created_by": llm}
@app.patch("/api/historique/{note_id}", tags=["Historique"])
def patch_historique(note_id: str, body: HistoriquePatch, llm: str = Depends(get_llm)):
from trilium_api import get_note, set_label, get_note_content, update_note_content
try:
get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Entrée historique introuvable")
if body.encore_valide is not None:
set_label(note_id, "encoreValide", "true" if body.encore_valide else "false")
if body.detail:
content = get_note_content(note_id)
content += f"<p><b>Mise à jour ({llm})</b> : {body.detail}</p>"
update_note_content(note_id, content)
return {"id": note_id, "updated_by": llm}
# ---------------------------------------------------------------------------
# Routes — Conversations
# ---------------------------------------------------------------------------
@app.get("/api/conversations/{projet}", tags=["Conversations"])
def list_conversations(projet: str, llm: str = Depends(get_llm)):
from trilium_api import search_by_label, get_label_value
notes = search_by_label("type", "conversation")
conv = [n for n in notes
if get_label_value(n["noteId"], "projet") == projet]
return [note_to_dict(n) for n in conv]
@app.get("/api/conversations/{projet}/{note_id}", tags=["Conversations"])
def get_conversation(projet: str, note_id: str, llm: str = Depends(get_llm)):
from trilium_api import get_note, get_label_value
try:
note = get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Conversation introuvable")
return note_to_dict({"noteId": note_id}, with_content=True)
@app.post("/api/conversations", tags=["Conversations"], status_code=201)
def create_conversation(body: ConversationCreate, llm: str = Depends(get_llm)):
import json
from trilium_api import create_note, get_note_id, set_label
with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f:
ids = json.load(f)
date = datetime.now().strftime("%Y-%m-%d %H:%M")
titre = f"[{body.llm}] {date}{body.titre}"
content = (f"<h2>{titre}</h2>"
f"<table>"
f"<tr><td><b>Projet</b></td><td>{body.projet}</td></tr>"
f"<tr><td><b>LLM</b></td><td>{body.llm}</td></tr>"
f"<tr><td><b>Date</b></td><td>{date}</td></tr>"
f"<tr><td><b>Synthèse de clôture</b></td>"
f"<td><i>À remplir en fin de session</i></td></tr>"
f"</table>")
res = create_note(ids["Conversations"], titre, content=content)
nid = get_note_id(res)
set_label(nid, "type", "conversation")
set_label(nid, "projet", body.projet)
set_label(nid, "llm", body.llm)
set_label(nid, "statut", "en-cours")
set_label(nid, "date", date)
return {"id": nid, "titre": titre, "created_by": llm}
@app.patch("/api/conversations/{note_id}", tags=["Conversations"])
def patch_conversation(note_id: str, body: ConversationPatch, llm: str = Depends(get_llm)):
from trilium_api import get_note, set_label, get_note_content, update_note_content
try:
get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Conversation introuvable")
if body.synthese_cloture:
content = get_note_content(note_id)
content = content.replace(
"<i>À remplir en fin de session</i>",
body.synthese_cloture.replace("<", "&lt;").replace(">", "&gt;")
)
if "<i>À remplir en fin de session</i>" not in get_note_content(note_id):
content += f"<h3>Synthèse</h3><p>{body.synthese_cloture}</p>"
update_note_content(note_id, content)
set_label(note_id, "syntheseCloture", body.synthese_cloture[:200])
set_label(note_id, "statut", "clos")
if body.statut:
set_label(note_id, "statut", body.statut)
return {"id": note_id, "updated_by": llm}
# ---------------------------------------------------------------------------
# Routes — Glossaire
# ---------------------------------------------------------------------------
@app.get("/api/glossaire/{projet}", tags=["Glossaire"])
def list_glossaire(projet: str, llm: str = Depends(get_llm)):
from trilium_api import search_by_label, get_label_value
notes = search_by_label("type", "termeGlossaire")
termes = [n for n in notes
if get_label_value(n["noteId"], "projet") == projet]
return [note_to_dict(n, with_content=True) for n in termes]
@app.post("/api/glossaire", tags=["Glossaire"], status_code=201)
def create_glossaire(body: GlossaireCreate, llm: str = Depends(get_llm)):
import json
from trilium_api import create_note, get_note_id, set_label
with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f:
ids = json.load(f)
content = f"<p><b>Définition</b> : {body.definition}</p>"
if body.synonymes_eviter:
content += f"<p><b>Synonymes à éviter</b> : {body.synonymes_eviter}</p>"
res = create_note(ids["Glossaire"], body.terme, content=content)
nid = get_note_id(res)
set_label(nid, "type", "termeGlossaire")
set_label(nid, "projet", body.projet)
set_label(nid, "definition", body.definition)
return {"id": nid, "terme": body.terme, "created_by": llm}
@app.patch("/api/glossaire/{note_id}", tags=["Glossaire"])
def patch_glossaire(note_id: str, body: GlossairePatch, llm: str = Depends(get_llm)):
from trilium_api import get_note, set_label, get_note_content, update_note_content
try:
get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Terme introuvable")
if body.definition:
set_label(note_id, "definition", body.definition)
content = get_note_content(note_id)
content += f"<p><b>Définition mise à jour ({llm})</b> : {body.definition}</p>"
update_note_content(note_id, content)
if body.synonymes_eviter:
content = get_note_content(note_id)
content += f"<p><b>Synonymes à éviter</b> : {body.synonymes_eviter}</p>"
update_note_content(note_id, content)
return {"id": note_id, "updated_by": llm}
# ---------------------------------------------------------------------------
# Routes — Contexte Reprise
# ---------------------------------------------------------------------------
@app.get("/api/contexte/{projet}", tags=["Contexte Reprise"])
def get_contexte(projet: str, llm_cible: str = "LLM",
llm: str = Depends(get_llm)):
"""Génère et retourne le briefing de reprise en texte."""
from trilium_api import (search_by_label, get_label_value,
get_note_content, create_note, get_note_id,
set_label, get_note)
import json, re as re2
decisions = search_by_label("type", "decision")
decisions = [d for d in decisions
if get_label_value(d["noteId"], "projet") == 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") == 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") == projet
and get_label_value(b["noteId"], "statut") not in ("fait", "abandonne")]
glossaire = search_by_label("type", "termeGlossaire")
glossaire = [g for g in glossaire
if get_label_value(g["noteId"], "projet") == projet]
date = datetime.now().strftime("%d/%m/%Y")
lines = [
f"# REPRISE DE CONTEXTE — {projet}{date}",
f"**LLM cible : {llm_cible}**", "",
]
if decisions:
lines += ["## Décisions actives (ne pas remettre en question)"]
for d in decisions:
lines.append(f"- {d.get('title', '?')}")
lines.append("")
if historique:
lines += ["## Historique — déjà testé / établi"]
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 += ["## Glossaire"]
for g in glossaire:
defn = get_label_value(g["noteId"], "definition") or "(voir note)"
lines.append(f"- **{g.get('title','?')}** : {defn}")
lines.append("")
if backlog:
lines += ["## Backlog actif"]
prio_ordre = {"haute": 0, "moyenne": 1, "basse": 2}
backlog.sort(key=lambda b: prio_ordre.get(
get_label_value(b["noteId"], "priorite") or "basse", 99))
for b in backlog:
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 en 3 lignes : (a) objectif (b) prochaine action (c) incertitude",
]
briefing = "\n".join(lines)
tokens = max(1, int(len(briefing) / 4))
return {"projet": projet, "tokens": tokens, "briefing": briefing}
@app.get("/api/contexte/{projet}/last", tags=["Contexte Reprise"])
def get_last_contexte(projet: str, llm: str = Depends(get_llm)):
"""Retourne le dernier contexte de reprise sauvegardé."""
from trilium_api import search_by_label, get_label_value, get_note_content
notes = search_by_label("type", "contexteReprise")
notes = [n for n in notes
if get_label_value(n["noteId"], "projet") == projet]
if not notes:
raise HTTPException(status_code=404,
detail=f"Aucun contexte de reprise pour '{projet}'")
notes.sort(key=lambda n: int(get_label_value(n["noteId"], "version") or 0),
reverse=True)
derniere = notes[0]
nid = derniere["noteId"]
content = get_note_content(nid)
texte = re.sub(r"<br\s*/?>", "\n", content)
texte = re.sub(r"<[^>]+>", "", texte)
texte = texte.replace("&amp;", "&").strip()
return {
"id": nid,
"titre": derniere.get("title", ""),
"version": get_label_value(nid, "version"),
"briefing": texte,
}
# ---------------------------------------------------------------------------
# Routes — Skills
# ---------------------------------------------------------------------------
# Les skills documentent un savoir-faire réutilisable (dev Synology, charte
# design, réflexe Trilium...). Contrairement aux autres objets, le label
# `projet` est optionnel : un skill peut être "universel" (portee=universel)
# et ne dépendre d'aucun projet précis.
@app.get("/api/skills", tags=["Skills"])
def list_skills(projet: Optional[str] = None, portee: Optional[str] = None,
llm: str = Depends(get_llm)):
"""Liste les skills. Filtre optionnel par projet et/ou portee.
Sans filtre projet, retourne aussi les skills universels."""
from trilium_api import search_by_label, get_label_value
notes = search_by_label("type", "skill")
result = []
for n in notes:
nid = n["noteId"]
note_projet = get_label_value(nid, "projet")
note_portee = get_label_value(nid, "portee")
if projet and note_projet != projet and note_portee != "universel":
continue
if portee and note_portee != portee:
continue
result.append(note_to_dict(n, with_content=True))
return result
@app.get("/api/skills/{note_id}", tags=["Skills"])
def get_skill(note_id: str, llm: str = Depends(get_llm)):
"""Récupère un skill par son id, contenu inclus."""
from trilium_api import get_note
try:
note = get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Skill introuvable")
return note_to_dict({"noteId": note_id, **note}, with_content=True)
@app.post("/api/skills", tags=["Skills"], status_code=201)
def create_skill(body: SkillCreate, llm: str = Depends(get_llm)):
"""Crée un skill. portee='universel' pour un skill sans projet rattaché."""
import json
from trilium_api import create_note, get_note_id, set_label, find_note_by_title
with open(os.path.expanduser("~/App/Context_continuity/trilium_ids.json")) as f:
ids = json.load(f)
if body.portee != "universel" and not body.projet:
raise HTTPException(status_code=400,
detail="projet requis sauf si portee='universel'")
existant = find_note_by_title(body.titre, ids["Skills"])
if existant:
raise HTTPException(status_code=409,
detail=f"Un skill '{body.titre}' existe déjà ({existant})")
content = f"<pre>{body.contenu}</pre>"
res = create_note(ids["Skills"], body.titre, content=content)
nid = get_note_id(res)
set_label(nid, "type", "skill")
set_label(nid, "portee", body.portee)
if body.projet:
set_label(nid, "projet", body.projet)
return {"id": nid, "titre": body.titre, "created_by": llm}
@app.patch("/api/skills/{note_id}", tags=["Skills"])
def patch_skill(note_id: str, body: SkillPatch, llm: str = Depends(get_llm)):
"""Met à jour le contenu et/ou la portee d'un skill existant."""
from trilium_api import get_note, set_label, update_note_content
try:
get_note(note_id)
except Exception:
raise HTTPException(status_code=404, detail="Skill introuvable")
if body.contenu is not None:
update_note_content(note_id, f"<pre>{body.contenu}</pre>")
if body.portee is not None:
set_label(note_id, "portee", body.portee)
return {"id": note_id, "updated_by": llm}
@app.delete("/api/notes/{note_id}", tags=["Système"])
def delete_note_endpoint(note_id: str, llm: str = Depends(get_llm)):
"""Supprime une note du systeme. Garde-fou : refuse les notes hors systeme."""
from trilium_api import delete_note_safe
ok, message = delete_note_safe(note_id)
if not ok:
raise HTTPException(status_code=400, detail=message)
return {"deleted": True, "message": message, "by": llm}
class MoveNoteBody(BaseModel):
ancien_parent_id: str
nouveau_parent_id: str
@app.post("/api/notes/{note_id}/move", tags=["Système"])
def move_note_endpoint(note_id: str, body: MoveNoteBody, llm: str = Depends(get_llm)):
"""Deplace une note d un parent vers un autre. Gere les clones."""
from trilium_api import move_note_safe
ok, message = move_note_safe(note_id, body.ancien_parent_id, body.nouveau_parent_id)
if not ok:
raise HTTPException(status_code=400, detail=message)
return {"moved": True, "message": message, "by": llm}
class AddRelationBody(BaseModel):
nom: str
cible_id: str
@app.post("/api/notes/{source_id}/relation", tags=["Système"])
def add_relation_endpoint(source_id: str, body: AddRelationBody, llm: str = Depends(get_llm)):
"""Cree une relation typee entre deux notes. Garde-fou : nom dans l ontologie."""
from trilium_api import add_relation_safe
ok, message = add_relation_safe(source_id, body.nom, body.cible_id)
if not ok:
raise HTTPException(status_code=400, detail=message)
return {"created": True, "message": message, "by": llm}