Refonte majeure du système Agent Chat et ajout de nombreux modules : - Chat unifié : suppression du dual Workflows/Agent Libre, tout passe par /api/chat avec résolution en 3 niveaux (workflow → geste → "montre-moi") - GestureCatalog : 38 raccourcis clavier universels Windows avec matching sémantique, substitution automatique dans les replays, et endpoint /api/gestures - Mode Copilot : exécution pas-à-pas des workflows avec validation humaine via WebSocket (approve/skip/abort) avant chaque action - Léa UI (agent_v0/lea_ui/) : interface PyQt5 pour Windows avec overlay transparent pour feedback visuel pendant le replay - Data Extraction (core/extraction/) : moteur d'extraction visuelle de données (OCR + VLM → SQLite), avec schémas YAML et export CSV/Excel - ReplayVerifier (agent_v0/server_v1/) : vérification post-action par comparaison de screenshots, avec logique de retry (max 3) - IntentParser durci : meilleur fallback regex, type GREETING, patterns améliorés - Dashboard : nouvelles pages gestures, streaming, extractions - Tests : 63 tests GestureCatalog, 47 tests extraction, corrections tests existants - Dépréciation : /api/agent/plan et /api/agent/execute retournent HTTP 410, suppression du code hardcodé _plan_to_replay_actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
316 lines
10 KiB
Python
Executable File
316 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
record_and_build.py — Script démo Phase 1
|
||
|
||
Enregistre une session RPA (screenshots + événements clavier/souris)
|
||
puis construit un Workflow automatiquement via le GraphBuilder.
|
||
|
||
Usage:
|
||
# Enregistrer une session (Ctrl+C pour arrêter)
|
||
python scripts/record_and_build.py record --name "login_workflow"
|
||
|
||
# Construire un workflow depuis une session existante
|
||
python scripts/record_and_build.py build --session data/training/sessions/session_xxx
|
||
|
||
# Enregistrer ET construire
|
||
python scripts/record_and_build.py full --name "login_workflow"
|
||
|
||
# Lister les sessions enregistrées
|
||
python scripts/record_and_build.py list
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import logging
|
||
import signal
|
||
import sys
|
||
import time
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
# Ajouter la racine du projet au path
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
# Vérifier que le venv est activé (éviter les erreurs silencieuses)
|
||
_venv_path = ROOT / ".venv" / "bin" / "python"
|
||
if _venv_path.exists() and _venv_path.resolve() != Path(sys.executable).resolve():
|
||
print(f"ATTENTION : le venv n'est pas activé.")
|
||
print(f" Lancer avec : source .venv/bin/activate && python {' '.join(sys.argv)}")
|
||
print(f" Ou directement : .venv/bin/python {' '.join(sys.argv)}")
|
||
sys.exit(1)
|
||
|
||
from core.models.raw_session import RawSession
|
||
from core.capture.session_recorder import SessionRecorder
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||
datefmt="%H:%M:%S",
|
||
)
|
||
logger = logging.getLogger("record_and_build")
|
||
|
||
|
||
# =========================================================================
|
||
# Commandes
|
||
# =========================================================================
|
||
|
||
|
||
def cmd_record(args) -> str:
|
||
"""Enregistrer une session."""
|
||
recorder = SessionRecorder(
|
||
output_dir=args.output_dir,
|
||
screenshot_on_click=True,
|
||
screenshot_interval_ms=args.interval,
|
||
)
|
||
|
||
session_id = recorder.start(workflow_name=args.name)
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f" Enregistrement en cours : {session_id}")
|
||
print(f" Workflow : {args.name or '(non nommé)'}")
|
||
print(f" Screenshots sur clic : oui")
|
||
if args.interval > 0:
|
||
print(f" Screenshots périodiques : {args.interval}ms")
|
||
print(f"{'='*60}")
|
||
print(f" Effectuez vos actions... Appuyez sur Ctrl+C pour arrêter.")
|
||
print(f"{'='*60}\n")
|
||
|
||
# Afficher les events en temps réel
|
||
def on_event(raw_event):
|
||
etype = raw_event.get("type", "?")
|
||
t = raw_event.get("t", 0)
|
||
if etype == "mouse_click":
|
||
pos = raw_event.get("pos", [0, 0])
|
||
btn = raw_event.get("button", "?")
|
||
print(f" [{t:7.2f}s] CLIC {btn} @ ({pos[0]}, {pos[1]})")
|
||
elif etype in ("key_press",):
|
||
keys = raw_event.get("keys", [])
|
||
print(f" [{t:7.2f}s] TOUCHE {' '.join(keys)}")
|
||
|
||
def on_screenshot(path):
|
||
count = recorder.screenshot_count
|
||
print(f" -> Screenshot #{count} sauvegardé")
|
||
|
||
recorder._on_event = on_event
|
||
recorder._on_screenshot = on_screenshot
|
||
|
||
# Attendre Ctrl+C
|
||
stop_event = False
|
||
|
||
def signal_handler(sig, frame):
|
||
nonlocal stop_event
|
||
stop_event = True
|
||
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
|
||
while not stop_event:
|
||
time.sleep(0.2)
|
||
|
||
session = recorder.stop()
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f" Session terminée : {session.session_id}")
|
||
print(f" Events : {len(session.events)}")
|
||
print(f" Screenshots: {len(session.screenshots)}")
|
||
print(f" Durée : {(session.ended_at - session.started_at).total_seconds():.1f}s")
|
||
session_dir = Path(args.output_dir) / session.session_id
|
||
print(f" Dossier : {session_dir}")
|
||
print(f"{'='*60}\n")
|
||
|
||
return str(session_dir)
|
||
|
||
|
||
def cmd_build(args) -> None:
|
||
"""Construire un workflow depuis une session existante."""
|
||
session_dir = Path(args.session)
|
||
|
||
# Trouver le fichier JSON de session
|
||
json_files = list(session_dir.glob("*.json"))
|
||
if not json_files:
|
||
print(f"Erreur : aucun fichier JSON trouvé dans {session_dir}")
|
||
sys.exit(1)
|
||
|
||
session_path = json_files[0]
|
||
print(f"Chargement de la session : {session_path}")
|
||
|
||
session = RawSession.load_from_file(session_path)
|
||
print(
|
||
f"Session {session.session_id} : "
|
||
f"{len(session.events)} events, {len(session.screenshots)} screenshots"
|
||
)
|
||
|
||
if not session.screenshots:
|
||
print("Erreur : la session n'a aucun screenshot. Impossible de construire un workflow.")
|
||
sys.exit(1)
|
||
|
||
# Construire le workflow
|
||
print("\nConstruction du workflow...")
|
||
print(" Initialisation des composants (CLIP, FAISS, DBSCAN)...")
|
||
|
||
from core.graph.graph_builder import GraphBuilder
|
||
|
||
# min_pattern_repetitions adaptatif :
|
||
# < 10 screenshots → 2 (exploration)
|
||
# 10-30 screenshots → 3
|
||
# > 30 screenshots → min(5, n//10)
|
||
n = len(session.screenshots)
|
||
if n < 10:
|
||
min_reps = 2
|
||
elif n <= 30:
|
||
min_reps = 3
|
||
else:
|
||
min_reps = min(5, n // 10)
|
||
|
||
builder = GraphBuilder(
|
||
min_pattern_repetitions=min_reps,
|
||
clustering_eps=0.15,
|
||
clustering_min_samples=2,
|
||
enable_quality_validation=True,
|
||
)
|
||
print(f" min_pattern_repetitions={min_reps} (pour {n} screenshots)")
|
||
|
||
workflow_name = args.name or f"workflow_{session.session_id}"
|
||
workflow = builder.build_from_session(session, workflow_name)
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f" Workflow construit : {workflow.name}")
|
||
print(f" Nodes : {len(workflow.nodes)}")
|
||
print(f" Edges : {len(workflow.edges)}")
|
||
print(f" État : {workflow.learning_state}")
|
||
|
||
if workflow.metadata and "quality_report" in workflow.metadata:
|
||
qr = workflow.metadata["quality_report"]
|
||
print(f" Qualité: {qr.get('overall_score', 0):.2%}")
|
||
print(f" Prod OK: {qr.get('is_production_ready', False)}")
|
||
|
||
print(f"\n Nodes :")
|
||
for node in workflow.nodes:
|
||
title = ""
|
||
if node.template and node.template.window:
|
||
title = node.template.window.title_pattern or ""
|
||
obs = node.metadata.get("observation_count", "?")
|
||
print(f" - {node.node_id}: {node.name} [{title}] ({obs} obs)")
|
||
|
||
print(f"\n Edges :")
|
||
for edge in workflow.edges:
|
||
action_type = edge.action.type if edge.action else "?"
|
||
count = edge.stats.execution_count if edge.stats else 0
|
||
print(f" - {edge.from_node} → {edge.to_node} [{action_type}] (×{count})")
|
||
|
||
# Sauvegarder le workflow
|
||
workflow_path = session_dir / f"{workflow.workflow_id}.json"
|
||
try:
|
||
with open(workflow_path, "w", encoding="utf-8") as f:
|
||
f.write(workflow.to_json())
|
||
print(f"\n Workflow sauvegardé : {workflow_path}")
|
||
except Exception as e:
|
||
print(f"\n Erreur sauvegarde : {e}")
|
||
|
||
print(f"{'='*60}\n")
|
||
|
||
|
||
def cmd_full(args) -> None:
|
||
"""Enregistrer puis construire."""
|
||
session_dir = cmd_record(args)
|
||
args.session = session_dir
|
||
cmd_build(args)
|
||
|
||
|
||
def cmd_list(args) -> None:
|
||
"""Lister les sessions enregistrées."""
|
||
sessions_dir = Path(args.output_dir)
|
||
if not sessions_dir.exists():
|
||
print(f"Aucune session trouvée dans {sessions_dir}")
|
||
return
|
||
|
||
sessions = []
|
||
for d in sorted(sessions_dir.iterdir()):
|
||
if not d.is_dir():
|
||
continue
|
||
json_files = list(d.glob("*.json"))
|
||
if json_files:
|
||
try:
|
||
session = RawSession.load_from_file(json_files[0])
|
||
sessions.append(session)
|
||
except Exception:
|
||
pass
|
||
|
||
if not sessions:
|
||
print("Aucune session trouvée.")
|
||
return
|
||
|
||
print(f"\n{'='*70}")
|
||
print(f" Sessions enregistrées ({len(sessions)})")
|
||
print(f"{'='*70}")
|
||
for s in sessions:
|
||
duration = ""
|
||
if s.ended_at and s.started_at:
|
||
duration = f"{(s.ended_at - s.started_at).total_seconds():.0f}s"
|
||
wf = s.context.get("workflow", "")
|
||
print(
|
||
f" {s.session_id} | {len(s.events):3d} events "
|
||
f"| {len(s.screenshots):3d} screenshots | {duration:>5s} | {wf}"
|
||
)
|
||
print(f"{'='*70}\n")
|
||
|
||
|
||
# =========================================================================
|
||
# Main
|
||
# =========================================================================
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="Enregistre des sessions RPA et construit des workflows",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
)
|
||
parser.add_argument(
|
||
"--output-dir",
|
||
default="data/training/sessions",
|
||
help="Répertoire de sortie (défaut: data/training/sessions)",
|
||
)
|
||
|
||
sub = parser.add_subparsers(dest="command", help="Commande")
|
||
|
||
# record
|
||
p_record = sub.add_parser("record", help="Enregistrer une session")
|
||
p_record.add_argument("--name", default="", help="Nom du workflow")
|
||
p_record.add_argument(
|
||
"--interval", type=int, default=0,
|
||
help="Intervalle de capture périodique en ms (0=désactivé)",
|
||
)
|
||
|
||
# build
|
||
p_build = sub.add_parser("build", help="Construire un workflow depuis une session")
|
||
p_build.add_argument("--session", required=True, help="Chemin vers le dossier de session")
|
||
p_build.add_argument("--name", default="", help="Nom du workflow")
|
||
|
||
# full
|
||
p_full = sub.add_parser("full", help="Enregistrer + construire")
|
||
p_full.add_argument("--name", default="", help="Nom du workflow")
|
||
p_full.add_argument(
|
||
"--interval", type=int, default=0,
|
||
help="Intervalle de capture périodique en ms (0=désactivé)",
|
||
)
|
||
|
||
# list
|
||
sub.add_parser("list", help="Lister les sessions enregistrées")
|
||
|
||
args = parser.parse_args()
|
||
|
||
if args.command == "record":
|
||
cmd_record(args)
|
||
elif args.command == "build":
|
||
cmd_build(args)
|
||
elif args.command == "full":
|
||
cmd_full(args)
|
||
elif args.command == "list":
|
||
cmd_list(args)
|
||
else:
|
||
parser.print_help()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|