jdmagent / src /jdm_agent /apps /qa_cli.py
expAge
feat: live streaming in qa_cli + layered diagnostic + Windows UTF-8 fix
c19d9d0
"""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())