#!/usr/bin/env python3 # Author: Simon-Pierre Boucher # Contact: contact@spboucher.ai """CLI de ka-bot — cartographe du web québécois (Claude Haiku 4.5 + Firecrawl). Exemples : python main.py agent --goal "Cartographier les PME technos de Québec" \\ --seed "https://www.quebecinternational.ca" --max-steps 20 python main.py pipeline --query "agences marketing Montréal" --limit 10 python main.py drain --max-pages 30 python main.py stats python main.py export --out exports/entites.json """ from __future__ import annotations import argparse import json import os import sys from src.config import config from src.orchestrator import Orchestrator from src.scheduler import LongRunner def _print(obj) -> None: print(json.dumps(obj, ensure_ascii=False, indent=2)) def cmd_agent(args) -> None: orch = Orchestrator(config) try: summary = orch.run_agent(args.goal, args.seed, args.max_steps) print("\n=== Résumé de mission ===") print(summary) print("\n=== Statistiques ===") _print(orch.store.stats()) finally: orch.close() def cmd_pipeline(args) -> None: orch = Orchestrator(config) try: _print(orch.run_pipeline(args.query, args.limit, args.per_site_pages)) finally: orch.close() def cmd_drain(args) -> None: orch = Orchestrator(config) try: _print(orch.drain_queue(args.max_pages)) finally: orch.close() def cmd_run(args) -> None: runner = LongRunner( config, cycle_delay=args.cycle_delay, queue_batch=args.queue_batch, daily_page_budget=args.daily_budget, revisit_hours=args.revisit_hours, session_minutes=args.session_minutes, ) print("ka4 démarré (Ctrl+C pour arrêter proprement).") runner.run(max_cycles=args.max_cycles) def cmd_missions(args) -> None: orch = Orchestrator(config) try: if args.add: mid = orch.store.add_mission(goal=args.add, seed=args.seed or "", priority=args.priority) print(f"Mission #{mid} ajoutée." if mid else "Mission déjà existante.") _print(orch.store.list_missions()) finally: orch.close() def cmd_stats(_args) -> None: orch = Orchestrator(config) try: _print(orch.store.stats()) finally: orch.close() def cmd_export(args) -> None: orch = Orchestrator(config) try: data = orch.store.export() if args.out: os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True) with open(args.out, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) print(f"{len(data)} entités exportées vers {args.out}") else: _print(data) finally: orch.close() def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(prog="ka-bot", description="Cartographe du web québécois.") sub = p.add_subparsers(dest="cmd", required=True) a = sub.add_parser("agent", help="Mode agentique piloté par Claude Haiku") a.add_argument("--goal", required=True, help="Objectif de mission") a.add_argument("--seed", help="URL de départ optionnelle") a.add_argument("--max-steps", type=int, default=20) a.set_defaults(func=cmd_agent) pl = sub.add_parser("pipeline", help="Pipeline déterministe search->scrape->extract") pl.add_argument("--query", required=True) pl.add_argument("--limit", type=int, default=10) pl.add_argument("--per-site-pages", type=int, default=3) pl.set_defaults(func=cmd_pipeline) d = sub.add_parser("drain", help="Traiter la file d'URLs en attente") d.add_argument("--max-pages", type=int, default=None) d.set_defaults(func=cmd_drain) r = sub.add_parser("run", help="Bot LONG TERME : boucle perpétuelle de cartographie") r.add_argument("--cycle-delay", type=float, default=20.0) r.add_argument("--queue-batch", type=int, default=8) r.add_argument("--daily-budget", type=int, default=800, help="Pages max par jour") r.add_argument("--revisit-hours", type=float, default=72.0) r.add_argument("--session-minutes", type=float, default=15.0, help="Durée de session avant d'attendre « Continuer »") r.add_argument("--max-cycles", type=int, default=None, help="Limite de cycles (test)") r.set_defaults(func=cmd_run) m = sub.add_parser("missions", help="Lister / ajouter des missions") m.add_argument("--add", help="Ajouter une mission (objectif)") m.add_argument("--seed", help="URL de départ pour la mission") m.add_argument("--priority", type=int, default=5) m.set_defaults(func=cmd_missions) s = sub.add_parser("stats", help="Statistiques de la base") s.set_defaults(func=cmd_stats) e = sub.add_parser("export", help="Exporter les entités en JSON") e.add_argument("--out", help="Fichier de sortie (sinon stdout)") e.set_defaults(func=cmd_export) return p def main() -> int: args = build_parser().parse_args() try: args.func(args) return 0 except RuntimeError as e: print(f"Erreur : {e}", file=sys.stderr) return 1 except KeyboardInterrupt: print("\nInterrompu.", file=sys.stderr) return 130 if __name__ == "__main__": raise SystemExit(main())