feat: tools remove_relation + move_relation (gestion des aretes du graphe)

- trilium_api.py : remove_relation_safe (retrait via attributeId, idempotent),
  move_relation_safe (reconnexion = add nouvelle puis remove ancienne, ordre sur)
- mcp_server.py : 2 tools MCP exposes (22 tools au total)
Comble le manque revele par la fusion render_engine.py : le systeme savait
creer une relation mais pas la retirer ni la reconnecter. Rend triviales les
corrections de mesusage et les fusions de hub. Teste de bout en bout.
This commit is contained in:
2026-07-04 08:14:44 +02:00
parent da9c6319c7
commit 7c56f125ad
2 changed files with 71 additions and 1 deletions
+37 -1
View File
@@ -24,6 +24,7 @@ from trilium_api import (
create_note, get_note_id, get_note, get_note_content, get_children,
update_note_content, search_by_label, set_label, get_label_value,
find_note_by_title, delete_note_safe, move_note_safe, add_relation_safe,
remove_relation_safe, move_relation_safe,
)
load_dotenv()
@@ -248,6 +249,33 @@ TOOLS = [
"required": ["source_id", "nom", "cible_id"],
},
},
{
"name": "remove_relation",
"description": "Retire une relation existante : source ~nom cible. Idempotent (si absente, ne fait rien). Utile pour corriger un mesusage ou lors d une fusion de notes.",
"inputSchema": {
"type": "object",
"properties": {
"source_id": {"type": "string"},
"nom": {"type": "string"},
"cible_id": {"type": "string"},
},
"required": ["source_id", "nom", "cible_id"],
},
},
{
"name": "move_relation",
"description": "Reconnecte une relation d une cible vers une autre en une operation : cree source ~nom nouvelle_cible puis retire source ~nom ancienne_cible. Geste de fusion de notes (reconnexion sure : cree avant de retirer).",
"inputSchema": {
"type": "object",
"properties": {
"source_id": {"type": "string"},
"nom": {"type": "string"},
"ancienne_cible": {"type": "string"},
"nouvelle_cible": {"type": "string"},
},
"required": ["source_id", "nom", "ancienne_cible", "nouvelle_cible"],
},
},
{
"name": "create_entite",
"description": "Cree une entite de la couche connaissance/technique. type : concept, composantLogiciel ou service. Range automatiquement dans le bon dossier. definition (optionnelle) n est posee que pour les concepts (les fait remonter dans le briefing). Garde-fou anti-doublon sur le titre.",
@@ -548,6 +576,14 @@ def tool_add_relation(source_id, nom, cible_id):
ok, message = add_relation_safe(source_id, nom, cible_id)
return {"success": ok, "message": message}
def tool_remove_relation(source_id, nom, cible_id):
ok, message = remove_relation_safe(source_id, nom, cible_id)
return {"success": ok, "message": message}
def tool_move_relation(source_id, nom, ancienne_cible, nouvelle_cible):
ok, message = move_relation_safe(source_id, nom, ancienne_cible, nouvelle_cible)
return {"success": ok, "message": message}
def tool_move_note(note_id, ancien_parent_id, nouveau_parent_id):
ok, message = move_note_safe(note_id, ancien_parent_id, nouveau_parent_id)
return {"success": ok, "message": message}
@@ -582,7 +618,7 @@ DISPATCH = {
"close_session": tool_close_session, "get_skills": tool_get_skills,
"add_skill": tool_add_skill,
"move_note": tool_move_note,
"add_relation": tool_add_relation, "create_entite": tool_create_entite,
"add_relation": tool_add_relation, "remove_relation": tool_remove_relation, "move_relation": tool_move_relation, "create_entite": tool_create_entite,
"update_note": tool_update_note,
"get_note": tool_get_note, "get_children": tool_get_children,
}
+34
View File
@@ -227,6 +227,40 @@ def add_relation_safe(source_id, nom, cible_id):
return (True, "Relation creee: %s ~%s %s" % (source_id, nom, cible_id))
def remove_relation_safe(source_id, nom, cible_id):
"""Retire la relation source ~nom-> cible si elle existe (via son attributeId).
Idempotent : si absente, ne fait rien et le signale.
Retourne (True, message) ou (False, message)."""
src = get_note(source_id)
if not src:
return (False, "Note source introuvable: %s" % source_id)
aid = None
for a in src.get("attributes", []):
if a.get("type") == "relation" and a.get("name") == nom and a.get("value") == cible_id:
aid = a.get("attributeId")
break
if aid is None:
return (True, "Relation absente (rien a retirer): %s ~%s %s" % (source_id, nom, cible_id))
try:
_ea().delete_attribute(aid)
except Exception as e:
return (False, "Echec retrait relation: %s" % e)
return (True, "Relation retiree: %s ~%s %s" % (source_id, nom, cible_id))
def move_relation_safe(source_id, nom, ancienne_cible, nouvelle_cible):
"""Reconnecte une relation : cree source ~nom-> nouvelle_cible puis retire
source ~nom-> ancienne_cible. Geste de fusion en une operation.
Retourne (True, message) ou (False, message)."""
ok_add, msg_add = add_relation_safe(source_id, nom, nouvelle_cible)
if not ok_add:
return (False, "Echec (creation nouvelle cible) : %s" % msg_add)
ok_rm, msg_rm = remove_relation_safe(source_id, nom, ancienne_cible)
if not ok_rm:
return (False, "Nouvelle cible creee MAIS echec retrait ancienne : %s" % msg_rm)
return (True, "Relation deplacee: %s ~%s de %s vers %s" % (source_id, nom, ancienne_cible, nouvelle_cible))
def _est_editable(note):
"""Nouveau critere de garde-fou : une note est editable/supprimable si elle
porte un label projet (note de projet, propriete du projet), ou si son type