jdmagent / subgraph_visualization_howto.md
expAge
feat(viz): outil de visualisation de sous-graphe JDM (méthode + MCP)
23cd061

A newer version of the Gradio SDK is available: 6.15.2

Upgrade

Construire une visualisation de sous-graphe JDM

Spécification pour transformer la démarche en outil MCP. Le fichier de référence est plat_asiatique_subgraph.html.

1. Source des données — outils MCP jdm

Tout est construit en interrogeant les outils du serveur MCP jdm (basés sur le dump JeuxDeMots du LIRMM/CNRS). Pour un terme racine T :

Profondeur 1 — voisins directs de T, un appel par relation

  • disambiguate(T) → si polysémique, on choisit un sense_id à explorer
  • get_hypernyms(T)r_isa (catégories)
  • get_hyponyms(T)r_hypo (exemples)
  • get_synonyms(T)r_syn
  • get_antonyms(T)r_anto
  • get_characteristics(T)r_carac
  • get_parts(T)r_has_part
  • get_locations(T)r_lieu
  • get_actions_on(T)r_patient-1 (verbes dont T est COD)
  • get_relations_of_type(T, "r_domain") et get_relations_of_type(T, "r_associated")

Chaque appel renvoie une liste {source, relation, target, w, polarity}.

À conserver tel quel :

  • target : déjà décodé en français lisible — ne pas re-transformer.
  • w : poids consensuel ; sert à filtrer (w ≥ 25) et à épaissir les arêtes.
  • polarity : "affirmation" ou "négation" — à ne pas mélanger.

Profondeur 2 — voisins de voisins

Pour chaque voisin sélectionné N (typiquement top-K par poids), relancer un sous-ensemble des mêmes outils — généralement get_parts, get_locations, get_characteristics — selon le type de N (un nom de plat → ingrédients + lieux ; un ingrédient → caractéristiques ; etc.).

2. Structure du graphe en mémoire

Nœuds

Un par terme unique rencontré.

{
  "id": "Y1",
  "label": "curry",
  "depth": 1,
  "kind": "hypo"
}
  • kind est dérivé de la relation entrante : center | isa | hypo | syn | anto | carac | part | lieu | verb | domain | assoc.

Arêtes

{
  "from": "P",
  "to": "Y1",
  "relation": "r_hypo",
  "weight": 70,
  "polarity": "affirmation",
  "depth": 1
}
  • Le label affiché concatène relation et poids, ex. "r_hypo 70".
  • depth === 2 → tracer en pointillés gris.
  • polarity === "négation" → préfixer le label « NON » et/ou utiliser une couleur dédiée (rouge). Ne jamais traiter une négation comme une affirmation.

Filtrage

  • Top-K par relation (ici K = 4 à 9 selon la relation).
  • w ≥ 25 par défaut (le seuil JDM standard pour exclure le bruit).
  • Évite l'explosion combinatoire à profondeur 2.

3. Rendu (vis-network)

Lib

vis-network via CDN (https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js). Moteur force-directed prêt à l'emploi, alternative possible : Cytoscape.js (même format JSON).

Centre épinglé

Le nœud racine est fixé au centre :

{ id:'P', x:0, y:0, fixed:{x:true,y:true}, mass:5 }

Les autres nœuds gravitent autour via le solver forceAtlas2Based.

Paramètres physiques clés (à ajuster selon densité)

forceAtlas2Based: {
  gravitationalConstant: -90,  // répulsion entre nœuds
  centralGravity: 0.05,        // attire l'ensemble vers le centre
  springLength: 110,           // longueur d'arête au repos — petit = compact
  springConstant: 0.12,
  avoidOverlap: 1
}

Physique désactivée après stabilisation

network.once('stabilizationIterationsDone', () => {
  network.setOptions({ physics:{ enabled:false } });
  network.fit({ animation:{ duration:400 } });
});

Le layout fige une fois calculé, ce qui rend les nœuds draggables sans rebondissement.

Palette par type de relation

Relation Couleur (fond / bordure)
centre #212121 / #000 (texte blanc)
r_isa #e3f2fd / #1976d2 (bleu)
r_hypo #e8f5e9 / #388e3c (vert)
r_carac #f3e5f5 / #7b1fa2 (violet)
r_has_part #fff3e0 / #ef6c00 (orange)
r_lieu #e0f7fa / #00838f (cyan)
r_patient-1 #fce4ec / #ad1457 (rose)
profondeur 2 #fafafa / #bdbdbd (gris, arêtes pointillées)

4. Interactions

Zoom

  • Molette : zoom natif vis-network (zoomSpeed: 0.6, minZoom: 0.1, maxZoom: 4).
  • Boutons Zoom +/− : network.moveTo({ scale: network.getScale() * factor }).
  • Bouton Recentrer : network.fit({ animation:true }).

Hover — mise en évidence des voisins

network.on('hoverNode', p => highlight(p.node));
network.on('blurNode', resetHighlight);

Dans highlight(focusId) :

  1. const connected = new Set(network.getConnectedNodes(focusId)) + ajouter focusId.
  2. network.getConnectedEdges(focusId) → set des arêtes voisines.
  3. nodes.update(...) : opacité 0.2 et texte gris pour tout ce qui n'est pas connecté.
  4. edges.update(...) : couleur gris très clair pour les arêtes hors voisinage.

resetHighlight restaure les couleurs d'origine en relisant l'attribut dashes (profondeur 2) de chaque arête.

5. Recette pour en faire un outil MCP

API proposée

build_subgraph(
    term: str,
    depth: int = 2,
    top_k_per_relation: int = 6,
    min_weight: float = 25,
    relations: list[str] | None = None,  # défaut : jeu standard ci-dessus
    output: Literal["json", "html"] = "html"
) -> str

Étapes

  1. Désambiguïsation : disambiguate(term). Si plusieurs sens forts, prendre le top par poids ou exposer un paramètre sense_id.
  2. Profondeur 1 : appels parallèles des ~10 outils listés en §1. Agréger en {nodes, edges}.
  3. Profondeur 2 : pour chaque voisin retenu (top-K par poids), choisir le sous-ensemble d'outils pertinent selon kind, relancer. Marquer les nœuds/arêtes depth: 2.
  4. Sérialisation :
    • output="json" → retourner {nodes, edges, root, stats}.
    • output="html" → injecter les DataSet dans le template de plat_asiatique_subgraph.html et retourner un fichier autonome.

Format vis-network (cible)

const nodes = new vis.DataSet([
  { id:'P', label:'plat asiatique', x:0, y:0, fixed:{x:true,y:true}, mass:5,
    color:{ background:'#212121', border:'#000' }, font:{ color:'#fff', size:28 } },
  { id:'Y1', label:'curry', color:{ background:'#e8f5e9', border:'#388e3c' },
    font:{ size:22 } },
  // ...
]);
const edges = new vis.DataSet([
  { from:'P', to:'Y1', label:'r_hypo 70', arrows:'to',
    color:{ color:'#9e9e9e' }, font:{ size:14 } },
  // depth 2 : dashes: true
]);

6. Pièges à connaître

  • Polysémie : toujours disambiguate en premier. Pour les termes très ambigus (avocat, souris, police), interroger sur le sense_id plutôt que sur le terme nu — sinon on mélange des branches sans rapport. Cf. mémoire feedback_jdm_disambiguate_first.
  • Polarité négative : polarity === "négation" est fréquente et porteuse de sens. Exemple réel : plat asiatique | r_isa | cuisine asiatique avec w = -30, polarity="négation" — JDM marque ici que le plat n'EST PAS la cuisine. La traiter comme une affirmation = erreur factuelle.
  • Artefacts d'héritage : canard laqué | r_has_part | plumage (w=146) est hérité du canard vivant — incohérent avec le plat. Un filtre heuristique par cohérence sémantique aide, mais reste un travail manuel ou nécessite un LLM en post-traitement.
  • Lourdeur de profondeur 3 : limiter depth à 2 par défaut. Profondeur 3 → graphes illisibles et latence multipliée.
  • Doublons en français/anglais : JDM mélange parfois en:music, en:chorus dans les résultats — filtrer le préfixe en: si on veut rester monolingue.