| # 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](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é. |
| ```json |
| { |
| "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 |
| ```json |
| { |
| "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 : |
| ```js |
| { 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é) |
| ```js |
| 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 |
| ```js |
| 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 |
| ```js |
| 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 |
| ```python |
| 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](plat_asiatique_subgraph.html) et retourner un fichier autonome. |
|
|
| ### Format vis-network (cible) |
| ```js |
| 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. |
|
|