File size: 4,726 Bytes
e13590b
 
 
 
 
12d6af7
 
e13590b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a26b99c
 
ad1f162
a26b99c
2a7a79d
 
 
 
 
 
a26b99c
 
 
 
 
2a7a79d
a26b99c
 
 
 
 
 
 
 
2a7a79d
 
 
a26b99c
 
 
e13590b
 
ad1f162
a26b99c
ad1f162
 
a26b99c
ad1f162
a26b99c
e13590b
 
a26b99c
ad1f162
a26b99c
 
 
 
 
 
 
 
 
 
 
 
 
e13590b
 
 
ad1f162
e13590b
a26b99c
e13590b
 
 
ad1f162
e13590b
 
 
 
 
 
 
 
 
 
12d6af7
e13590b
 
 
 
 
12d6af7
e13590b
 
 
 
 
 
12d6af7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""Détecte les trous de couverture dans JDM pour un terme donné.

Trois familles de gaps :
  - MISSING : aucun triplet (term, relation, ?) pour une relation pourtant
              utile pour ce type de terme.
  - NEGATIVE_FILLED : que des triplets négatifs (JDM a regardé et dit non).
  - LOW_COVERAGE : très peu de triplets positifs (< seuil).

Pas d'appel LLM, déterministe, basé sur les requêtes JDM.
"""
from __future__ import annotations

from typing import Iterable, Optional

from jdm_agent.client import JDMClient
from jdm_agent.enrich.models import Gap, GapType


# Relations qu'on considère utile d'avoir pour la plupart des noms communs.
DEFAULT_TARGET_RELATIONS: tuple[str, ...] = (
    "r_has_part", "r_carac", "r_has_color", "r_telic_role",
    "r_lieu", "r_make", "r_object>mater",
)

# Relations qu'on considère utile pour les verbes.
VERB_TARGET_RELATIONS: tuple[str, ...] = (
    "r_agent", "r_patient", "r_instr", "r_lieu", "r_manner",
    "r_has_conseq", "r_has_causatif", "r_but",
)


def _get_relations_signed(client: JDMClient, term: str, relation: str,
                          limit: int = 50) -> list[tuple[str, float]]:
    """Récupère les triplets avec leur poids (signé), Phase 9b sans seuil.

    Renvoie [(target_name, w), ...] trié par |w| décroissant. Inclut tout
    ce que JDM renvoie par défaut (négatifs et faibles inclus).

    Note : on NE passe PAS `limit` à JDM (l'API tronque AVANT le tri par
    poids — on rate alors les triplets les plus pertinents). On récupère
    tout, on trie ici, puis on tronque.
    """
    rid = client.relation_type_id(relation)
    if rid is None:
        return []
    try:
        res = client.relations_from(term, types_ids=[rid])
    except Exception:
        return []
    idx = res.node_index()
    out = []
    for r in res.relations:
        n = idx.get(r.node2)
        if n is not None:
            out.append((n.name, r.w))
    out.sort(key=lambda x: -abs(x[1]))
    if limit and limit > 0:
        out = out[:limit]
    return out


def _detect_missing(client: JDMClient, term: str,
                    relations: Iterable[str], min_to_consider: int = 1) -> list[Gap]:
    """Pour chaque relation cible, classifie en MISSING / NEGATIVE_FILLED / LOW_COVERAGE.

    Phase 9b : aucun seuil de poids hardcodé.
      - MISSING strict : aucun triplet (ni positif NI négatif)
      - NEGATIVE_FILLED : que des triplets négatifs (JDM a regardé et dit non)
      - LOW_COVERAGE : < min_to_consider triplets positifs
    """
    gaps: list[Gap] = []
    for rel in relations:
        signed = _get_relations_signed(client, term, rel)
        positives = [w for _, w in signed if w > 0]
        negatives = [(n, w) for n, w in signed if w < 0]
        n_pos = len(positives)
        if n_pos == 0 and len(negatives) > 0:
            top_neg = sorted(negatives, key=lambda x: x[1])[:3]
            detail_extras = "; ".join(f"{n} (w={w:.0f})" for n, w in top_neg)
            gaps.append(Gap(
                term=term, relation=rel, gap_type=GapType.NEGATIVE_FILLED,
                severity=0.3,
                detail=(f"JDM contient {len(negatives)} triplet(s) NÉGATIFS pour "
                        f"`{term} | {rel} | ?` : {detail_extras}. Pas un gap au "
                        f"sens strict — déjà rempli avec des assertions négatives."),
            ))
        elif n_pos == 0:
            gaps.append(Gap(
                term=term, relation=rel, gap_type=GapType.MISSING,
                severity=1.0,
                detail=f"Aucun triplet positif `{term} | {rel} | ?` dans JDM.",
            ))
        elif n_pos < min_to_consider:
            gaps.append(Gap(
                term=term, relation=rel, gap_type=GapType.LOW_COVERAGE,
                severity=0.6,
                detail=f"Seulement {n_pos} triplet(s) positif(s) `{term} | {rel} | ?`.",
            ))
    return gaps


def detect_gaps(
    client: JDMClient,
    term: str,
    target_relations: Optional[Iterable[str]] = None,
    min_to_consider: int = 3,
) -> list[Gap]:
    """Point d'entrée principal : trouve les gaps de couverture d'un terme.

    Args:
        client: JDMClient.
        term: le terme à analyser.
        target_relations: relations à examiner (défaut: noun-typiques + verb-typiques).
        min_to_consider: seuil pour LOW_COVERAGE (< N triplets positifs).
    """
    if target_relations is None:
        # On essaie les deux jeux ; les non-pertinents se traduiront par MISSING
        # qu'on pourra filtrer côté pipeline si besoin.
        target_relations = tuple(set(DEFAULT_TARGET_RELATIONS) | set(VERB_TARGET_RELATIONS))

    return _detect_missing(client, term, target_relations, min_to_consider)