spb/ka2 Public
ka2 — explorateur structuré du web québécois (édition légère, Groupe KA). Bot scraper+IA → graphe de connaissances. Claude Haiku + Firecrawl/Scrapfly.
Python 98.1%
Shell 1.9%
1#!/usr/bin/env python32# Author: Simon-Pierre Boucher3# Contact: contact@spboucher.ai4"""CLI de ka-bot — cartographe du web québécois (Claude Haiku 4.5 + Firecrawl).56Exemples :7 python main.py agent --goal "Cartographier les PME technos de Québec" \\8 --seed "https://www.quebecinternational.ca" --max-steps 209 python main.py pipeline --query "agences marketing Montréal" --limit 1010 python main.py drain --max-pages 3011 python main.py stats12 python main.py export --out exports/entites.json13"""1415from __future__ import annotations1617import argparse18import json19import os20import sys2122from src.config import config23from src.orchestrator import Orchestrator24from src.scheduler import LongRunner252627def _print(obj) -> None:28 print(json.dumps(obj, ensure_ascii=False, indent=2))293031def cmd_agent(args) -> None:32 orch = Orchestrator(config)33 try:34 summary = orch.run_agent(args.goal, args.seed, args.max_steps)35 print("\n=== Résumé de mission ===")36 print(summary)37 print("\n=== Statistiques ===")38 _print(orch.store.stats())39 finally:40 orch.close()414243def cmd_pipeline(args) -> None:44 orch = Orchestrator(config)45 try:46 _print(orch.run_pipeline(args.query, args.limit, args.per_site_pages))47 finally:48 orch.close()495051def cmd_drain(args) -> None:52 orch = Orchestrator(config)53 try:54 _print(orch.drain_queue(args.max_pages))55 finally:56 orch.close()575859def cmd_run(args) -> None:60 runner = LongRunner(61 config,62 cycle_delay=args.cycle_delay,63 queue_batch=args.queue_batch,64 daily_page_budget=args.daily_budget,65 revisit_hours=args.revisit_hours,66 session_minutes=args.session_minutes,67 )68 print("ka2 démarré (Ctrl+C pour arrêter proprement).")69 runner.run(max_cycles=args.max_cycles)707172def cmd_missions(args) -> None:73 orch = Orchestrator(config)74 try:75 if args.add:76 mid = orch.store.add_mission(goal=args.add, seed=args.seed or "", priority=args.priority)77 print(f"Mission #{mid} ajoutée." if mid else "Mission déjà existante.")78 _print(orch.store.list_missions())79 finally:80 orch.close()818283def cmd_stats(_args) -> None:84 orch = Orchestrator(config)85 try:86 _print(orch.store.stats())87 finally:88 orch.close()899091def cmd_export(args) -> None:92 orch = Orchestrator(config)93 try:94 data = orch.store.export()95 if args.out:96 os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)97 with open(args.out, "w", encoding="utf-8") as f:98 json.dump(data, f, ensure_ascii=False, indent=2)99 print(f"{len(data)} entités exportées vers {args.out}")100 else:101 _print(data)102 finally:103 orch.close()104105106def build_parser() -> argparse.ArgumentParser:107 p = argparse.ArgumentParser(prog="ka-bot", description="Cartographe du web québécois.")108 sub = p.add_subparsers(dest="cmd", required=True)109110 a = sub.add_parser("agent", help="Mode agentique piloté par Claude Haiku")111 a.add_argument("--goal", required=True, help="Objectif de mission")112 a.add_argument("--seed", help="URL de départ optionnelle")113 a.add_argument("--max-steps", type=int, default=20)114 a.set_defaults(func=cmd_agent)115116 pl = sub.add_parser("pipeline", help="Pipeline déterministe search->scrape->extract")117 pl.add_argument("--query", required=True)118 pl.add_argument("--limit", type=int, default=10)119 pl.add_argument("--per-site-pages", type=int, default=3)120 pl.set_defaults(func=cmd_pipeline)121122 d = sub.add_parser("drain", help="Traiter la file d'URLs en attente")123 d.add_argument("--max-pages", type=int, default=None)124 d.set_defaults(func=cmd_drain)125126 r = sub.add_parser("run", help="Bot LONG TERME : boucle perpétuelle de cartographie")127 r.add_argument("--cycle-delay", type=float, default=20.0)128 r.add_argument("--queue-batch", type=int, default=8)129 r.add_argument("--daily-budget", type=int, default=800, help="Pages max par jour")130 r.add_argument("--revisit-hours", type=float, default=72.0)131 r.add_argument("--session-minutes", type=float, default=15.0,132 help="Durée de session avant d'attendre « Continuer »")133 r.add_argument("--max-cycles", type=int, default=None, help="Limite de cycles (test)")134 r.set_defaults(func=cmd_run)135136 m = sub.add_parser("missions", help="Lister / ajouter des missions")137 m.add_argument("--add", help="Ajouter une mission (objectif)")138 m.add_argument("--seed", help="URL de départ pour la mission")139 m.add_argument("--priority", type=int, default=5)140 m.set_defaults(func=cmd_missions)141142 s = sub.add_parser("stats", help="Statistiques de la base")143 s.set_defaults(func=cmd_stats)144145 e = sub.add_parser("export", help="Exporter les entités en JSON")146 e.add_argument("--out", help="Fichier de sortie (sinon stdout)")147 e.set_defaults(func=cmd_export)148149 return p150151152def main() -> int:153 args = build_parser().parse_args()154 try:155 args.func(args)156 return 0157 except RuntimeError as e:158 print(f"Erreur : {e}", file=sys.stderr)159 return 1160 except KeyboardInterrupt:161 print("\nInterrompu.", file=sys.stderr)162 return 130163164165if __name__ == "__main__":166 raise SystemExit(main())167