jdmagent / subgraph_visualization_howto.md
expAge
feat(viz): outil de visualisation de sous-graphe JDM (méthode + MCP)
23cd061
# 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.