File size: 6,241 Bytes
13c0411
 
 
 
 
 
 
 
 
 
 
 
 
c19d9d0
 
13c0411
 
 
 
 
 
c19d9d0
13c0411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c19d9d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13c0411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c19d9d0
 
 
13c0411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c19d9d0
 
 
 
 
 
13c0411
 
 
 
 
 
 
 
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""REPL minimal de Q&A NL → JeuxDeMots.

Usage :
    python -m jdm_agent.apps.qa_cli
    python -m jdm_agent.apps.qa_cli --question "synonymes de voiture"
    python -m jdm_agent.apps.qa_cli --provider ollama --model llama3.1:8b

Variables d'environnement (alternatives aux flags) :
    LLM_PROVIDER, LLM_MODEL, LLM_TEMPERATURE
    ANTHROPIC_API_KEY, OPENAI_API_KEY (selon provider)
"""
from __future__ import annotations

from jdm_agent.apps import _console  # noqa: F401 — force stdout UTF-8 (Windows)

import argparse
import os
import sys
from typing import Optional

from jdm_agent.client import JDMClient
from jdm_agent.tools.jdm_agent import ask, build_jdm_agent, stream
from jdm_agent.tools.llm_factory import get_llm


BANNER = """
╔══════════════════════════════════════════════════════════════╗
║  JDM Q&A — réponses ancrées dans le graphe JeuxDeMots        ║
║  Tape une question (en français) ou 'exit' pour quitter.     ║
║  '/help' pour l'aide, '/tools' pour lister les outils.       ║
╚══════════════════════════════════════════════════════════════╝
"""

HELP = """
Commandes :
  /help            cette aide
  /tools           liste les outils JDM disponibles
  /verbose on|off  active/désactive la trace des appels d'outils
  /clear           efface l'écran
  exit | quit      quitter

Exemples de questions :
  - Qu'est-ce qu'un chat ?
  - Donne-moi des synonymes de voiture.
  - Quel est le rapport entre chat et internet ?
  - Le mot "avocat" a-t-il plusieurs sens ?
  - Quelle est la couleur du sang d'après JDM ?
"""


def _print_tool_calls(tool_calls: list[dict]) -> None:
    if not tool_calls:
        return
    print("\n[outils appelés]")
    for tc in tool_calls:
        args = tc.get("args") or {}
        args_str = ", ".join(f"{k}={v!r}" for k, v in args.items())
        print(f"  • {tc['name']}({args_str})")


def _stream_printer(verbose: bool):
    """Imprime un événement par étape de l'agent — montre que ça avance."""
    import time
    t0 = [time.time()]

    def on_event(ev: dict) -> None:
        dt = time.time() - t0[0]
        kind = ev["kind"]
        if kind == "AIMessage":
            tcs = ev.get("tool_calls") or []
            if tcs:
                for tc in tcs:
                    args = ", ".join(f"{k}={v!r}" for k, v in (tc.get("args") or {}).items())
                    print(f"  ⏱ {dt:5.1f}s  →  appel {tc['name']}({args})", flush=True)
            else:
                # Réponse finale du modèle.
                preview = (ev.get("content") or "").strip().replace("\n", " ")[:80]
                if preview:
                    print(f"  ⏱ {dt:5.1f}s  ←  réponse du modèle ({len(ev['content'])} chars)", flush=True)
        elif kind == "ToolMessage":
            content = ev.get("content") or ""
            preview = content[:100].replace("\n", " ")
            print(f"  ⏱ {dt:5.1f}s  ←  outil {ev.get('name')} renvoie {len(content)} chars : {preview}…", flush=True)
        t0[0] = time.time()

    return on_event if verbose else None


def run_repl(provider: Optional[str], model: Optional[str], verbose: bool) -> int:
    print(BANNER)
    print(f"Provider : {provider or os.environ.get('LLM_PROVIDER', 'anthropic')}")
    print(f"Modèle   : {model or os.environ.get('LLM_MODEL', 'claude-sonnet-4-5')}")
    print("Chargement du client JDM et du modèle…")

    client = JDMClient()
    try:
        llm = get_llm(provider=provider, model=model)
        agent = build_jdm_agent(client=client, llm=llm)
    except Exception as e:
        print(f"\n[erreur] impossible d'initialiser le modèle: {e}", file=sys.stderr)
        return 2

    print("Prêt.\n")
    show_tools = verbose
    while True:
        try:
            q = input("> ").strip()
        except (EOFError, KeyboardInterrupt):
            print()
            break
        if not q:
            continue
        if q.lower() in {"exit", "quit"}:
            break
        if q == "/help":
            print(HELP)
            continue
        if q == "/clear":
            os.system("cls" if os.name == "nt" else "clear")
            continue
        if q.startswith("/verbose"):
            show_tools = "on" in q
            print(f"verbose = {show_tools}")
            continue
        if q == "/tools":
            from jdm_agent.tools.jdm_tools import ALL_TOOLS
            for t in ALL_TOOLS:
                print(f"  - {t.name}: {t.description.splitlines()[0]}")
            continue

        try:
            print("(réflexion en cours…)", flush=True)
            on_event = _stream_printer(show_tools)
            out = stream(agent, q, on_event=on_event)
        except Exception as e:
            print(f"[erreur] {e}", file=sys.stderr)
            continue

        print()
        print(out["answer"])
        print()

    client.close()
    return 0


def main() -> int:
    p = argparse.ArgumentParser(description="REPL Q&A JeuxDeMots (LangChain agent).")
    p.add_argument("--provider", default=None, help="LLM provider (anthropic, openai, ollama, ...)")
    p.add_argument("--model", default=None, help="Nom du modèle (claude-sonnet-4-5, gpt-4o, llama3.1:8b, ...)")
    p.add_argument("--question", "-q", default=None, help="Pose une seule question puis quitte.")
    p.add_argument("--verbose", action="store_true", help="Affiche les appels d'outils.")
    args = p.parse_args()

    if args.question:
        client = JDMClient()
        llm = get_llm(provider=args.provider, model=args.model)
        agent = build_jdm_agent(client=client, llm=llm)
        if args.verbose:
            print("(réflexion en cours…)", flush=True)
            out = stream(agent, args.question, on_event=_stream_printer(True))
        else:
            out = ask(agent, args.question)
        print()
        print(out["answer"])
        client.close()
        return 0

    return run_repl(args.provider, args.model, args.verbose)


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