File size: 5,121 Bytes
7e3f910
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad1f162
7e3f910
 
 
 
 
 
 
 
 
 
 
ad1f162
7e3f910
 
 
 
 
 
 
 
 
 
 
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""CLI du fact-checker JDM.

Trois modes :
  1. Texte → LLM extrait les claims, puis vérification
       python -m jdm_agent.apps.factcheck --text "La baleine est un poisson."

  2. Claim unique directe (sans LLM, instantanée)
       python -m jdm_agent.apps.factcheck --claim "baleine r_isa poisson"
       python -m jdm_agent.apps.factcheck --claim "sang r_has_color rouge"

  3. Stdin : un claim "subject r_xxx object" par ligne
       echo "baleine r_isa poisson" | python -m jdm_agent.apps.factcheck --stdin
"""
from __future__ import annotations

from jdm_agent.apps import _console  # noqa: F401 — UTF-8 console

import argparse
import json
import sys
from typing import Iterable

from jdm_agent.client import JDMClient
from jdm_agent.factcheck import Claim, Report, factcheck, factcheck_claims
from jdm_agent.factcheck.models import Status


GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
GRAY = "\033[90m"
RESET = "\033[0m"

STATUS_COLOR = {
    Status.SUPPORTED: GREEN,
    Status.CONTRADICTED: RED,
    Status.UNKNOWN: YELLOW,
}
STATUS_ICON = {
    Status.SUPPORTED: "✓",
    Status.CONTRADICTED: "✗",
    Status.UNKNOWN: "?",
}


def parse_claim_line(line: str) -> Claim:
    """Parse 'subject r_xxx object' (séparateurs espace OU pipe)."""
    line = line.strip().replace("|", " ")
    parts = line.split()
    if len(parts) < 3:
        raise ValueError(f"Format invalide : {line!r}. Attendu : 'subject r_xxx object'")
    # subject = parts[0], relation = parts[1] (commence par r_), object = reste
    subject = parts[0]
    relation = parts[1]
    obj = " ".join(parts[2:])
    if not relation.startswith("r_"):
        raise ValueError(f"Le 2e champ doit être une relation JDM (`r_xxx`) : {relation!r}")
    return Claim(text=line, subject=subject, relation=relation, object=obj)


def print_report(report: Report, as_json: bool = False) -> None:
    if as_json:
        print(report.model_dump_json(indent=2))
        return

    summary = report.summary()
    print(f"\n=== Rapport ({summary['total']} claim(s)) ===")
    print(f"  {GREEN}✓ supported{RESET}    : {summary['supported']}")
    print(f"  {RED}✗ contradicted{RESET} : {summary['contradicted']}")
    print(f"  {YELLOW}? unknown{RESET}      : {summary['unknown']}")
    print()

    for v in report.verdicts:
        color = STATUS_COLOR[v.status]
        icon = STATUS_ICON[v.status]
        print(f"{color}{icon} [{v.status.value.upper()}]{RESET} "
              f"`{v.claim.subject} | {v.claim.relation} | {v.claim.object}`"
              f"  {GRAY}(conf={v.confidence:.2f}){RESET}")
        if v.claim.text and v.claim.text != f"{v.claim.subject} {v.claim.relation} {v.claim.object}":
            print(f"   {GRAY}texte : {v.claim.text!r}{RESET}")
        if v.explanation:
            print(f"   → {v.explanation}")
        for e in v.evidence_for:
            print(f"     {GREEN}+ {e.source} | {e.relation} | {e.target} (w={e.w:.0f}){RESET}")
        for e in v.evidence_against:
            print(f"     {RED}- {e.source} | {e.relation} | {e.target} (w={e.w:.0f}){RESET}")
        print()


def main() -> int:
    p = argparse.ArgumentParser(description="Fact-checker JeuxDeMots.")
    p.add_argument("--text", help="Texte libre à analyser (LLM extrait les claims).")
    p.add_argument("--claim", action="append",
                   help="Claim direct au format 'subject r_xxx object' (répétable).")
    p.add_argument("--stdin", action="store_true",
                   help="Lit un claim par ligne sur stdin.")
    p.add_argument("--provider", default=None, help="LLM provider (pour --text)")
    p.add_argument("--model", default=None, help="LLM model (pour --text)")
    p.add_argument("--json", action="store_true", help="Sortie en JSON brut.")
    args = p.parse_args()

    if not (args.text or args.claim or args.stdin):
        p.print_help()
        return 1

    client = JDMClient()

    if args.text:
        try:
            from jdm_agent.tools.llm_factory import get_llm
            llm = get_llm(provider=args.provider, model=args.model)
        except Exception as e:
            print(f"[erreur] init LLM : {e}", file=sys.stderr)
            return 2
        print(f"[extraction] {args.text!r} → claims via LLM…", file=sys.stderr)
        report = factcheck(args.text, client=client, llm=llm)
    else:
        claims_lines: list[str] = []
        if args.claim:
            claims_lines.extend(args.claim)
        if args.stdin:
            claims_lines.extend(l for l in sys.stdin if l.strip() and not l.startswith("#"))
        try:
            claims = [parse_claim_line(l) for l in claims_lines]
        except ValueError as e:
            print(f"[erreur] {e}", file=sys.stderr)
            return 1
        report = factcheck_claims(claims, client=client)

    print_report(report, as_json=args.json)
    client.close()

    # Exit code basé sur les contradictions trouvées (utile en CI).
    n_contradicted = sum(1 for v in report.verdicts if v.status == Status.CONTRADICTED)
    return 0 if n_contradicted == 0 else 3


if __name__ == "__main__":
    sys.exit(main())