"""CLI d'enrichissement JDM. Usages : # 1. Détection des gaps sans LLM (instantané, gratuit) python -m jdm_agent.apps.enrich --terms smartphone tablette --no-propose # 2. Proposition de candidats via LLM + validation, sortie CSV python -m jdm_agent.apps.enrich --terms smartphone --provider anthropic --model claude-sonnet-4-5 -o candidats.csv # 3. Liste de termes depuis un fichier python -m jdm_agent.apps.enrich --terms-file domaine.txt --provider ollama --model llama3.1:8b """ from __future__ import annotations from jdm_agent.apps import _console # noqa: F401 — UTF-8 console import argparse import sys from pathlib import Path from jdm_agent.client import JDMClient from jdm_agent.enrich import enrich from jdm_agent.enrich.pipeline import write_candidates_csv, write_submission GREEN = "\033[92m" RED = "\033[91m" YELLOW = "\033[93m" GRAY = "\033[90m" RESET = "\033[0m" def main() -> int: p = argparse.ArgumentParser(description="Enrichissement actif de JeuxDeMots.") p.add_argument("--terms", nargs="*", default=[], help="Liste de termes à analyser (séparés par espace).") p.add_argument("--terms-file", help="Fichier texte (1 terme par ligne) à analyser.") p.add_argument("--relations", default=None, help="Relations à inspecter, séparées par virgule. Défaut: jeu standard.") p.add_argument("--no-propose", action="store_true", help="Détecte les gaps sans demander de candidats au LLM.") p.add_argument("--no-validate", action="store_true", help="Désactive la validation des candidats (plus rapide, moins fiable).") p.add_argument("--consolidate", action="store_true", help="Consolide les candidats validés par INFÉRENCE dans le réseau JDM.") p.add_argument("--inference-effort", type=int, default=1, choices=(1, 2), help="Effort du moteur d'inférence pour la consolidation (1 noyau, 2 complet).") p.add_argument("--min-coverage", type=int, default=3, help="Une relation ayant < N triplets positifs est signalée comme " "gap. Monter pour enrichir aussi les relations bien fournies " "(défaut 3 → relations à 3+ entrées ignorées).") p.add_argument("--provider", default=None) p.add_argument("--model", default=None) p.add_argument("--max-per-gap", type=int, default=10) p.add_argument("-o", "--output", default=None, help="Fichier CSV de travail (tous les candidats annotés).") p.add_argument("--submission-output", default=None, help="Fichier de soumission : UNIQUEMENT les triplets consolidés " "par inférence (force --consolidate).") p.add_argument("--upload", action="store_true", help="Après écriture du fichier de soumission, le POST au " "endpoint LLMDrops de JDM. Nécessite JDM_DROPS_API_KEY " "dans l'env (ou --upload-api-key). Force aussi " "--submission-output si non fourni.") p.add_argument("--upload-model", default=None, help="Nom du LLM source utilisé dans le filename uploadé " "(ex. 'claude-sonnet-4-7'). Défaut: env LLM_MODEL ou " "'mcp_client'.") p.add_argument("--upload-endpoint", default=None, help="URL du endpoint LLMDrops (override env JDM_DROPS_URL).") p.add_argument("--upload-api-key", default=None, help="Clé API LLMDrops (override env JDM_DROPS_API_KEY).") args = p.parse_args() terms: list[str] = list(args.terms) if args.terms_file: terms.extend(l.strip() for l in Path(args.terms_file).read_text(encoding="utf-8").splitlines() if l.strip() and not l.startswith("#")) if not terms: p.print_help() return 1 target_relations = None if args.relations: target_relations = [r.strip() for r in args.relations.split(",") if r.strip()] client = JDMClient() llm = None if not args.no_propose: 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 # Une soumission n'a de sens que consolidée → --submission-output et # --upload forcent tous deux la consolidation systématique. do_consolidate = args.consolidate or bool(args.submission_output) or args.upload # --upload sans chemin explicite → on dérive un défaut sensé pour avoir # un fichier local en plus de l'envoi distant. if args.upload and not args.submission_output: args.submission_output = "soumission_jdm.enrich" print(f"[enrich] {len(terms)} terme(s), relations={target_relations or 'standard'}, " f"min_coverage={args.min_coverage}, consolidate={do_consolidate}", file=sys.stderr) gaps, candidates = enrich( terms, client=client, llm=llm, target_relations=target_relations, propose=not args.no_propose, validate=not args.no_validate, consolidate=do_consolidate, inference_effort=args.inference_effort, max_per_gap=args.max_per_gap, min_coverage=args.min_coverage, ) # Résumé gaps print(f"\n=== Gaps détectés : {len(gaps)} ===") by_type: dict[str, int] = {} for g in gaps: by_type[g.gap_type.value] = by_type.get(g.gap_type.value, 0) + 1 for k, v in sorted(by_type.items()): print(f" - {k}: {v}") for g in gaps[:20]: print(f" {YELLOW}? {g.term} | {g.relation}{RESET} [{g.gap_type.value}, sev={g.severity:.1f}]") print(f" {GRAY}{g.detail}{RESET}") if len(gaps) > 20: print(f" {GRAY}… {len(gaps) - 20} autre(s) gap(s) non affiché(s){RESET}") # Résumé candidats if candidates: ok = [c for c in candidates if c.is_valid()] dup = [c for c in candidates if c.validation_status == "duplicate"] unk = [c for c in candidates if c.validation_status == "unknown_term"] inc = [c for c in candidates if c.validation_status == "inconsistent"] print(f"\n=== Candidats proposés : {len(candidates)} ===") print(f" {GREEN}ok : {len(ok)}{RESET}") print(f" {GRAY}duplicate : {len(dup)}{RESET}") print(f" {YELLOW}unknown_term: {len(unk)}{RESET}") print(f" {RED}inconsistent: {len(inc)}{RESET}") for c in ok[:30]: print(f" {GREEN}+ {c.term} | {c.relation} | {c.target}{RESET} " f"{GRAY}(conf={c.confidence:.2f}){RESET}") if c.rationale: print(f" {GRAY}→ {c.rationale}{RESET}") if len(ok) > 30: print(f" {GRAY}… {len(ok) - 30} autre(s){RESET}") # Résumé consolidation (si demandée) if do_consolidate: cons = [c for c in candidates if c.is_consolidated()] rej = [c for c in candidates if c.consolidation_status == "rejected"] notc = [c for c in candidates if c.consolidation_status == "not_consolidated"] print(f"\n=== Consolidation par inférence ===") print(f" {GREEN}consolidés : {len(cons)}{RESET}") print(f" {YELLOW}non consolidés : {len(notc)}{RESET}") print(f" {RED}réfutés : {len(rej)}{RESET}") for c in cons[:20]: print(f" {GREEN}✓ {c.term} | {c.relation} | {c.target}{RESET}") print(f" {GRAY}{c.consolidation_explanation[:140]}{RESET}") if args.output: write_candidates_csv(args.output, candidates) print(f"\n[csv] {len(candidates)} candidat(s) écrit(s) dans {args.output}", file=sys.stderr) if args.submission_output: n = write_submission(args.submission_output, candidates) print(f"[soumission] {n} candidat(s) consolidé(s) — fichier {args.submission_output}", file=sys.stderr) if args.upload: from jdm_agent.enrich import submit_to_jdm # Détermine le nom de modèle uploadé : --upload-model > --model > # env LLM_MODEL > "mcp_client" (cf. submit_to_jdm). upload_model = args.upload_model or args.model result = submit_to_jdm( args.submission_output, api_key=args.upload_api_key, model_name=upload_model, endpoint_url=args.upload_endpoint, ) if result["ok"]: print(f"[upload] {GREEN}✓ uploadé{RESET} sous " f"{result['uploaded_as']} → {result['endpoint']} " f"(HTTP {result['status_code']})", file=sys.stderr) print(f"[upload] réponse : {result['response']}", file=sys.stderr) else: print(f"[upload] {RED}✗ échec{RESET} : {result['error']}", file=sys.stderr) print(f"[upload] le fichier local reste disponible : " f"{args.submission_output}", file=sys.stderr) client.close() return 0 if __name__ == "__main__": sys.exit(main())