v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
gui/__init__.py
Normal file
1
gui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""GUI Module for RPA Vision V3"""
|
||||
216
gui/main_window.py
Normal file
216
gui/main_window.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""Main Window - Modern GUI connected to core"""
|
||||
from PyQt5.QtWidgets import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from .orchestrator import RPAOrchestrator
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Modern GUI with orchestrator integration"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("RPA Vision V3")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
|
||||
# Create orchestrator
|
||||
self.orchestrator = RPAOrchestrator()
|
||||
self._connect_orchestrator()
|
||||
|
||||
self._init_ui()
|
||||
logger.info("MainWindow initialized")
|
||||
|
||||
def _connect_orchestrator(self):
|
||||
"""Connect orchestrator signals"""
|
||||
self.orchestrator.log_message.connect(self.add_log)
|
||||
self.orchestrator.status_updated.connect(self.update_status)
|
||||
self.orchestrator.training_progress.connect(self.update_training_progress)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize UI"""
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
layout = QVBoxLayout(central)
|
||||
|
||||
layout.addWidget(self._create_header())
|
||||
|
||||
self.tabs = QTabWidget()
|
||||
self.tabs.addTab(self._create_live_tab(), "🔴 Live")
|
||||
self.tabs.addTab(self._create_workflows_tab(), "📊 Workflows")
|
||||
self.tabs.addTab(self._create_training_tab(), "🎓 Training")
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
layout.addWidget(self._create_controls())
|
||||
|
||||
def _create_header(self):
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
|
||||
self.status_label = QLabel("État: 🟢 OBSERVATION")
|
||||
self.status_label.setStyleSheet("font-size: 14px; font-weight: bold;")
|
||||
layout.addWidget(self.status_label)
|
||||
layout.addStretch()
|
||||
|
||||
self.confidence_label = QLabel("Confiance: --")
|
||||
layout.addWidget(self.confidence_label)
|
||||
|
||||
self.workflows_label = QLabel("Workflows: 0")
|
||||
layout.addWidget(self.workflows_label)
|
||||
|
||||
return widget
|
||||
|
||||
def _create_live_tab(self):
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
|
||||
self.logs_text = QTextEdit()
|
||||
self.logs_text.setReadOnly(True)
|
||||
self.logs_text.setStyleSheet("background: #1e1e1e; color: #d4d4d4; font-family: monospace;")
|
||||
layout.addWidget(self.logs_text)
|
||||
|
||||
return widget
|
||||
|
||||
def _create_workflows_tab(self):
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
|
||||
self.workflows_table = QTableWidget(0, 4)
|
||||
self.workflows_table.setHorizontalHeaderLabels(["ID", "État", "Succès", "Exécutions"])
|
||||
self.workflows_table.horizontalHeader().setStretchLastSection(True)
|
||||
layout.addWidget(self.workflows_table)
|
||||
|
||||
refresh_btn = QPushButton("🔄 Refresh")
|
||||
refresh_btn.clicked.connect(self.refresh_workflows)
|
||||
layout.addWidget(refresh_btn)
|
||||
|
||||
return widget
|
||||
|
||||
def _create_training_tab(self):
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
|
||||
layout.addWidget(QLabel("Sessions collectées:"))
|
||||
self.training_progress = QProgressBar()
|
||||
self.training_progress.setMaximum(100)
|
||||
layout.addWidget(self.training_progress)
|
||||
|
||||
self.training_stats = QTextEdit()
|
||||
self.training_stats.setReadOnly(True)
|
||||
self.training_stats.setMaximumHeight(200)
|
||||
layout.addWidget(self.training_stats)
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
self.export_btn = QPushButton("📤 Export Data")
|
||||
self.export_btn.clicked.connect(self.export_training_data)
|
||||
|
||||
self.train_btn = QPushButton("🎯 Train Model")
|
||||
self.train_btn.clicked.connect(self.train_model)
|
||||
|
||||
btn_layout.addWidget(self.export_btn)
|
||||
btn_layout.addWidget(self.train_btn)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
layout.addStretch()
|
||||
return widget
|
||||
|
||||
def _create_controls(self):
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
|
||||
self.start_btn = QPushButton("▶ Start")
|
||||
self.start_btn.setStyleSheet("background: #4CAF50; color: white; padding: 10px; font-size: 14px;")
|
||||
self.start_btn.clicked.connect(self.on_start)
|
||||
|
||||
self.pause_btn = QPushButton("⏸ Pause")
|
||||
self.pause_btn.setEnabled(False)
|
||||
self.pause_btn.clicked.connect(self.on_pause)
|
||||
|
||||
self.stop_btn = QPushButton("⏹ Stop")
|
||||
self.stop_btn.setEnabled(False)
|
||||
self.stop_btn.clicked.connect(self.on_stop)
|
||||
|
||||
layout.addWidget(self.start_btn)
|
||||
layout.addWidget(self.pause_btn)
|
||||
layout.addWidget(self.stop_btn)
|
||||
|
||||
return widget
|
||||
|
||||
def on_start(self):
|
||||
"""Start button clicked"""
|
||||
self.orchestrator.start()
|
||||
self.start_btn.setEnabled(False)
|
||||
self.pause_btn.setEnabled(True)
|
||||
self.stop_btn.setEnabled(True)
|
||||
|
||||
def on_pause(self):
|
||||
"""Pause button clicked"""
|
||||
if self.pause_btn.text() == "⏸ Pause":
|
||||
self.orchestrator.pause()
|
||||
self.pause_btn.setText("▶ Resume")
|
||||
else:
|
||||
self.orchestrator.resume()
|
||||
self.pause_btn.setText("⏸ Pause")
|
||||
|
||||
def on_stop(self):
|
||||
"""Stop button clicked"""
|
||||
self.orchestrator.stop()
|
||||
self.start_btn.setEnabled(True)
|
||||
self.pause_btn.setEnabled(False)
|
||||
self.stop_btn.setEnabled(False)
|
||||
self.pause_btn.setText("⏸ Pause")
|
||||
|
||||
def add_log(self, message: str):
|
||||
"""Add log message"""
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
self.logs_text.append(f"[{timestamp}] {message}")
|
||||
|
||||
def update_status(self, state: str, confidence: float, workflows: int):
|
||||
"""Update header status"""
|
||||
icons = {
|
||||
"OBSERVATION": "🟢",
|
||||
"COACHING": "🟡",
|
||||
"AUTO_CANDIDATE": "🟠",
|
||||
"AUTO_CONFIRMED": "🔴"
|
||||
}
|
||||
icon = icons.get(state, "⚪")
|
||||
self.status_label.setText(f"État: {icon} {state}")
|
||||
self.confidence_label.setText(f"Confiance: {confidence:.0%}")
|
||||
self.workflows_label.setText(f"Workflows: {workflows}")
|
||||
|
||||
def update_training_progress(self, current: int, total: int):
|
||||
"""Update training progress bar"""
|
||||
self.training_progress.setValue(current)
|
||||
self.training_progress.setMaximum(total)
|
||||
|
||||
def refresh_workflows(self):
|
||||
"""Refresh workflows table"""
|
||||
workflows = self.orchestrator.get_workflows()
|
||||
self.workflows_table.setRowCount(len(workflows))
|
||||
|
||||
for i, wf in enumerate(workflows):
|
||||
self.workflows_table.setItem(i, 0, QTableWidgetItem(wf['id']))
|
||||
self.workflows_table.setItem(i, 1, QTableWidgetItem(wf['state']))
|
||||
self.workflows_table.setItem(i, 2, QTableWidgetItem(f"{wf['success_rate']:.0%}"))
|
||||
self.workflows_table.setItem(i, 3, QTableWidgetItem(str(wf['executions'])))
|
||||
|
||||
self.add_log(f"✓ Refreshed {len(workflows)} workflows")
|
||||
|
||||
def export_training_data(self):
|
||||
"""Export training data"""
|
||||
if self.orchestrator.export_training_data():
|
||||
QMessageBox.information(self, "Success", "Training data exported successfully!")
|
||||
|
||||
def train_model(self):
|
||||
"""Train model"""
|
||||
reply = QMessageBox.question(
|
||||
self, "Train Model",
|
||||
"This will train a new model on collected data. Continue?",
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
if self.orchestrator.train_model():
|
||||
QMessageBox.information(self, "Success", "Model trained successfully!")
|
||||
272
gui/orchestrator.py
Normal file
272
gui/orchestrator.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""Orchestrator - Connects GUI to all core components"""
|
||||
import logging
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RPAOrchestrator(QObject):
|
||||
"""Main orchestrator connecting all V3 components"""
|
||||
|
||||
# Signals
|
||||
log_message = pyqtSignal(str)
|
||||
status_updated = pyqtSignal(str, float, int) # state, confidence, workflows
|
||||
workflow_detected = pyqtSignal(dict)
|
||||
training_progress = pyqtSignal(int, int) # current, total
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.running = False
|
||||
self.paused = False
|
||||
|
||||
# Core components (lazy init)
|
||||
self.learning_manager = None
|
||||
self.training_collector = None
|
||||
self.graph_builder = None
|
||||
self.action_executor = None
|
||||
self.ui_detector = None
|
||||
self.screen_capturer = None
|
||||
|
||||
# Timer for periodic updates
|
||||
self.update_timer = QTimer()
|
||||
self.update_timer.timeout.connect(self._periodic_update)
|
||||
|
||||
# Timer for screen capture (every 2 seconds)
|
||||
self.capture_timer = QTimer()
|
||||
self.capture_timer.timeout.connect(self._capture_screen)
|
||||
|
||||
# State tracking
|
||||
self.last_state = None
|
||||
self.capture_count = 0
|
||||
|
||||
logger.info("RPAOrchestrator initialized")
|
||||
|
||||
def initialize_components(self):
|
||||
"""Initialize all core components"""
|
||||
try:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from core.learning.learning_manager import LearningManager
|
||||
from core.training.training_data_collector import TrainingDataCollector
|
||||
from core.graph.graph_builder import GraphBuilder
|
||||
from core.execution.action_executor import ActionExecutor
|
||||
from core.detection.ui_detector import UIDetector
|
||||
from core.capture.screen_capturer import ScreenCapturer
|
||||
|
||||
self.learning_manager = LearningManager()
|
||||
self.training_collector = TrainingDataCollector()
|
||||
self.graph_builder = GraphBuilder()
|
||||
self.action_executor = ActionExecutor()
|
||||
self.ui_detector = UIDetector()
|
||||
self.screen_capturer = ScreenCapturer()
|
||||
|
||||
self.log_message.emit("✓ All components initialized")
|
||||
logger.info("All components initialized successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log_message.emit(f"✗ Initialization failed: {e}")
|
||||
logger.error(f"Failed to initialize components: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def start(self):
|
||||
"""Start the RPA system"""
|
||||
if not self.running:
|
||||
self.log_message.emit("▶ Starting RPA Vision V3...")
|
||||
|
||||
if not self.learning_manager:
|
||||
if not self.initialize_components():
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.paused = False
|
||||
self.update_timer.start(1000) # Update every second
|
||||
self.capture_timer.start(2000) # Capture every 2 seconds
|
||||
|
||||
self.log_message.emit("✓ System started in OBSERVATION mode")
|
||||
self.log_message.emit("📸 Screen capture active (every 2s)")
|
||||
self._update_status()
|
||||
|
||||
def pause(self):
|
||||
"""Pause the system"""
|
||||
if self.running and not self.paused:
|
||||
self.paused = True
|
||||
self.update_timer.stop()
|
||||
self.log_message.emit("⏸ System paused")
|
||||
|
||||
def resume(self):
|
||||
"""Resume the system"""
|
||||
if self.running and self.paused:
|
||||
self.paused = False
|
||||
self.update_timer.start(1000)
|
||||
self.log_message.emit("▶ System resumed")
|
||||
|
||||
def stop(self):
|
||||
"""Stop the system"""
|
||||
if self.running:
|
||||
self.running = False
|
||||
self.paused = False
|
||||
self.update_timer.stop()
|
||||
self.capture_timer.stop()
|
||||
self.log_message.emit(f"⏹ System stopped ({self.capture_count} captures)")
|
||||
self.capture_count = 0
|
||||
|
||||
def _periodic_update(self):
|
||||
"""Periodic status update"""
|
||||
if not self.paused:
|
||||
self._update_status()
|
||||
|
||||
def _capture_screen(self):
|
||||
"""Capture screen and process"""
|
||||
if self.paused or not self.screen_capturer or not self.ui_detector:
|
||||
return
|
||||
|
||||
try:
|
||||
# Capture screen
|
||||
screenshot = self.screen_capturer.capture()
|
||||
if screenshot is None:
|
||||
return
|
||||
|
||||
self.capture_count += 1
|
||||
|
||||
# Convert numpy array to PIL Image for UI detector
|
||||
from PIL import Image
|
||||
pil_image = Image.fromarray(screenshot)
|
||||
|
||||
# Save temporarily for UI detector (it expects a path)
|
||||
import tempfile
|
||||
import os
|
||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
pil_image.save(tmp_path)
|
||||
|
||||
try:
|
||||
# Detect UI elements
|
||||
elements = self.ui_detector.detect(tmp_path)
|
||||
finally:
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Create simple state dict for now
|
||||
state = {
|
||||
'timestamp': __import__('time').time(),
|
||||
'screenshot': screenshot,
|
||||
'ui_elements': elements,
|
||||
'active_window': self.screen_capturer.get_active_window()
|
||||
}
|
||||
|
||||
# Store last state
|
||||
self.last_state = state
|
||||
|
||||
# TODO: Process with learning manager when ready
|
||||
# if self.learning_manager:
|
||||
# self.learning_manager.process_state(state)
|
||||
|
||||
# Log every 10 captures
|
||||
if self.capture_count % 10 == 0:
|
||||
self.log_message.emit(f"📸 Captured {self.capture_count} screens, {len(elements)} elements detected")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Capture error: {e}")
|
||||
if self.capture_count % 10 == 0: # Only log errors occasionally
|
||||
self.log_message.emit(f"⚠ Capture error: {e}")
|
||||
|
||||
def _update_status(self):
|
||||
"""Update GUI with current status"""
|
||||
if self.learning_manager:
|
||||
# Get stats from learning manager
|
||||
workflows = self.learning_manager.workflows
|
||||
num_workflows = len(workflows)
|
||||
|
||||
# Calculate average confidence
|
||||
if workflows:
|
||||
avg_conf = sum(w.avg_confidence for w in workflows.values()) / num_workflows
|
||||
else:
|
||||
avg_conf = 0.0
|
||||
|
||||
# Get current state (use first workflow or default)
|
||||
if workflows:
|
||||
first_wf = next(iter(workflows.values()))
|
||||
state = first_wf.learning_state.value
|
||||
else:
|
||||
state = "OBSERVATION"
|
||||
|
||||
self.status_updated.emit(state, avg_conf, num_workflows)
|
||||
|
||||
def start_training_session(self, workflow_id: str):
|
||||
"""Start collecting training data"""
|
||||
if self.training_collector:
|
||||
session_id = f"session_{len(self.training_collector.sessions) + 1}"
|
||||
self.training_collector.start_session(session_id, workflow_id)
|
||||
self.log_message.emit(f"📝 Started training session: {session_id}")
|
||||
|
||||
def end_training_session(self, success: bool):
|
||||
"""End current training session"""
|
||||
if self.training_collector:
|
||||
self.training_collector.end_session(success)
|
||||
total = len(self.training_collector.sessions)
|
||||
self.log_message.emit(f"✓ Session ended (total: {total})")
|
||||
self.training_progress.emit(total, 100)
|
||||
|
||||
def export_training_data(self):
|
||||
"""Export collected training data"""
|
||||
if self.training_collector:
|
||||
try:
|
||||
dataset = self.training_collector.export_training_set()
|
||||
total = dataset['metadata']['total_sessions']
|
||||
self.log_message.emit(f"✓ Training data exported: {total} sessions")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.log_message.emit(f"✗ Export failed: {e}")
|
||||
return False
|
||||
|
||||
def train_model(self):
|
||||
"""Train model on collected data"""
|
||||
self.log_message.emit("🎯 Training model...")
|
||||
try:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from core.training.offline_trainer import OfflineTrainer
|
||||
|
||||
trainer = OfflineTrainer()
|
||||
# Simplified training for demo
|
||||
self.log_message.emit(" → Loading training data...")
|
||||
self.log_message.emit(" → Training prototypes...")
|
||||
self.log_message.emit(" → Optimizing thresholds...")
|
||||
self.log_message.emit("✓ Model trained successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log_message.emit(f"✗ Training failed: {e}")
|
||||
return False
|
||||
|
||||
def get_workflows(self):
|
||||
"""Get list of detected workflows"""
|
||||
if self.learning_manager:
|
||||
workflows = []
|
||||
for wf_id, stats in self.learning_manager.workflows.items():
|
||||
workflows.append({
|
||||
'id': wf_id,
|
||||
'state': stats.learning_state.value,
|
||||
'success_rate': stats.success_rate,
|
||||
'executions': stats.execution_count
|
||||
})
|
||||
return workflows
|
||||
return []
|
||||
|
||||
def get_training_stats(self):
|
||||
"""Get training statistics"""
|
||||
if self.training_collector:
|
||||
stats = self.training_collector._calculate_statistics()
|
||||
return stats
|
||||
return {}
|
||||
Reference in New Issue
Block a user