jdmagent / src /jdm_agent /apps /factcheck.py
expAge
refactor(phase-9b): retirer TOUS les seuils de poids hardcodés
ad1f162
"""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())