- 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>
353 lines
13 KiB
Python
353 lines
13 KiB
Python
"""
|
|
Visual Workflow Builder - Backend Flask Application
|
|
|
|
This is the main entry point for the Visual Workflow Builder backend API.
|
|
It provides REST endpoints for workflow management and WebSocket support
|
|
for real-time execution updates.
|
|
"""
|
|
|
|
from flask import Flask
|
|
from flask_cors import CORS
|
|
from flask_socketio import SocketIO
|
|
from flask_caching import Cache
|
|
from flask_migrate import Migrate
|
|
import os
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Initialize Flask app
|
|
app = Flask(__name__)
|
|
|
|
# Configuration
|
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
|
|
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///vwb_v3.db')
|
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10MB max upload
|
|
app.config['CACHE_TYPE'] = 'redis' if os.getenv('REDIS_URL') else 'simple'
|
|
app.config['CACHE_REDIS_URL'] = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
|
|
|
|
# Initialize extensions - Use db from v3 models (source of truth)
|
|
from db.models import db
|
|
db.init_app(app)
|
|
|
|
# Initialize Flask-Migrate for database migrations
|
|
migrate = Migrate(app, db)
|
|
|
|
cache = Cache(app)
|
|
socketio = SocketIO(
|
|
app,
|
|
cors_allowed_origins="*",
|
|
async_mode='threading',
|
|
logger=True,
|
|
engineio_logger=True
|
|
)
|
|
|
|
# Enable CORS - autoriser tous les ports locaux en développement
|
|
CORS(app, resources={
|
|
r"/api/*": {
|
|
"origins": os.getenv('CORS_ORIGINS', 'http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:5173').split(','),
|
|
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
"allow_headers": ["Content-Type", "Authorization"]
|
|
}
|
|
})
|
|
|
|
# Import and register blueprints (minimal set)
|
|
from api.workflows import workflows_bp
|
|
from api.screen_capture import screen_capture_bp
|
|
from api.real_demo import real_demo_bp
|
|
from api.errors import error_response
|
|
|
|
app.register_blueprint(workflows_bp, url_prefix='/api/workflows')
|
|
app.register_blueprint(screen_capture_bp, url_prefix='/api/screen-capture')
|
|
app.register_blueprint(real_demo_bp)
|
|
|
|
# Optional / Phase 2+ blueprints (loaded only if modules are available)
|
|
try:
|
|
from api.self_healing import self_healing_bp
|
|
app.register_blueprint(self_healing_bp)
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint self_healing désactivé: {e}")
|
|
|
|
try:
|
|
from api.visual_targets import visual_targets_bp, init_visual_target_manager
|
|
app.register_blueprint(visual_targets_bp)
|
|
VISUAL_TARGETS_BP_AVAILABLE = True
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint visual_targets désactivé: {e}")
|
|
VISUAL_TARGETS_BP_AVAILABLE = False
|
|
init_visual_target_manager = None
|
|
|
|
try:
|
|
from api.element_detection import element_detection_bp, init_element_detection
|
|
app.register_blueprint(element_detection_bp)
|
|
ELEMENT_DETECTION_BP_AVAILABLE = True
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint element_detection désactivé: {e}")
|
|
ELEMENT_DETECTION_BP_AVAILABLE = False
|
|
init_element_detection = None
|
|
|
|
try:
|
|
from api.analytics import analytics_bp
|
|
app.register_blueprint(analytics_bp, url_prefix='/api/analytics')
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
# Register other blueprints (optional - depends on Phase 2+ services)
|
|
try:
|
|
from api.templates import templates_bp
|
|
app.register_blueprint(templates_bp, url_prefix='/api/templates')
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint templates désactivé: {e}")
|
|
|
|
from api.node_types import node_types_bp
|
|
app.register_blueprint(node_types_bp, url_prefix='/api/node-types')
|
|
|
|
try:
|
|
from api.executions import executions_bp
|
|
app.register_blueprint(executions_bp, url_prefix='/api/executions')
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint executions désactivé: {e}")
|
|
|
|
try:
|
|
from api.import_export import import_export_bp
|
|
app.register_blueprint(import_export_bp, url_prefix='/api')
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint import_export désactivé: {e}")
|
|
|
|
try:
|
|
from api.correction_packs import correction_packs_bp
|
|
app.register_blueprint(correction_packs_bp, url_prefix='/api')
|
|
print("✅ Blueprint correction_packs enregistré")
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint correction_packs désactivé: {e}")
|
|
|
|
try:
|
|
from api.coaching_sessions import coaching_sessions_bp
|
|
app.register_blueprint(coaching_sessions_bp, url_prefix='/api/coaching-sessions')
|
|
print("✅ Blueprint coaching_sessions enregistré")
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint coaching_sessions désactivé: {e}")
|
|
|
|
# Catalogue VWB - actions VisionOnly
|
|
# V2 avec VLM (Vision Language Model) pour détection intelligente
|
|
try:
|
|
from catalog_routes_v2_vlm import catalog_bp
|
|
app.register_blueprint(catalog_bp)
|
|
print("✅ Blueprint catalog V2 VLM (Ollama qwen2.5vl) enregistré")
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint catalog V2 VLM désactivé: {e}")
|
|
# Fallback sur la version pyautogui
|
|
try:
|
|
from catalog_routes import catalog_bp
|
|
app.register_blueprint(catalog_bp)
|
|
print("✅ Blueprint catalog (fallback pyautogui) enregistré")
|
|
except ImportError as e2:
|
|
print(f"⚠️ Blueprint catalog désactivé: {e2}")
|
|
|
|
# API Images Ancres Visuelles - stockage serveur
|
|
try:
|
|
from api.anchor_images import anchor_images_bp
|
|
app.register_blueprint(anchor_images_bp)
|
|
print("✅ Blueprint anchor_images enregistré")
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint anchor_images désactivé: {e}")
|
|
|
|
# API UI Detection - UI-DETR-1
|
|
try:
|
|
from api.ui_detection import ui_detection_bp
|
|
app.register_blueprint(ui_detection_bp)
|
|
print("✅ Blueprint ui_detection (UI-DETR-1) enregistré - /api/ui-detection/*")
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint ui_detection désactivé: {e}")
|
|
|
|
# ============================================================
|
|
# API V3 - Thin Client Architecture (Source de Vérité Unique)
|
|
# ============================================================
|
|
try:
|
|
from api_v3 import api_v3_bp
|
|
app.register_blueprint(api_v3_bp)
|
|
print("✅ Blueprint API v3 (Thin Client) enregistré - /api/v3/*")
|
|
except ImportError as e:
|
|
print(f"⚠️ Blueprint API v3 désactivé: {e}")
|
|
|
|
|
|
# Import WebSocket handlers (optional)
|
|
try:
|
|
from api import websocket_handlers # noqa: F401
|
|
except Exception as e:
|
|
print(f"⚠️ WebSocket handlers désactivés: {e}")
|
|
|
|
# Global error handlers
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
"""Handle 404 errors"""
|
|
return error_response(404, "Resource not found")
|
|
|
|
@app.errorhandler(405)
|
|
def method_not_allowed(error):
|
|
"""Handle 405 errors"""
|
|
return error_response(405, "Method not allowed")
|
|
|
|
@app.errorhandler(500)
|
|
def internal_error(error):
|
|
"""Handle 500 errors"""
|
|
return error_response(500, "Internal server error")
|
|
|
|
@app.errorhandler(Exception)
|
|
def handle_exception(error):
|
|
"""Handle all unhandled exceptions"""
|
|
import traceback
|
|
traceback.print_exc()
|
|
return error_response(500, f"Unexpected error: {str(error)}")
|
|
|
|
# Health check endpoint
|
|
@app.route('/health')
|
|
@app.route('/api/health')
|
|
def health_check():
|
|
"""Health check endpoint for monitoring"""
|
|
from flask import jsonify
|
|
return jsonify({'status': 'healthy', 'version': '1.0.0'})
|
|
|
|
# Workflow execution endpoint (proxy to catalog execute)
|
|
@app.route('/api/workflow/execute-step', methods=['POST'])
|
|
def execute_workflow_step():
|
|
"""Execute a workflow step via the catalog execute endpoint"""
|
|
from flask import jsonify, request
|
|
import requests
|
|
|
|
try:
|
|
data = request.get_json() or {}
|
|
step_id = data.get('stepId', f'step_{int(__import__("time").time() * 1000)}')
|
|
step_type = data.get('stepType', 'click_anchor')
|
|
parameters = data.get('parameters', {})
|
|
|
|
# DEBUG: Écrire les données reçues dans un fichier
|
|
import json as json_module
|
|
with open('/tmp/vwb_debug.log', 'a') as debug_file:
|
|
debug_file.write(f"\n{'='*60}\n")
|
|
debug_file.write(f"[execute-step] stepType={step_type}, stepId={step_id}\n")
|
|
debug_file.write(f"[execute-step] parameters keys: {list(parameters.keys())}\n")
|
|
if 'visual_anchor' in parameters:
|
|
va = parameters['visual_anchor']
|
|
debug_file.write(f"[execute-step] visual_anchor keys: {list(va.keys()) if va else 'None'}\n")
|
|
debug_file.write(f"[execute-step] visual_anchor.id: {va.get('id')}\n")
|
|
debug_file.write(f"[execute-step] visual_anchor.thumbnail_url: {va.get('thumbnail_url') or (va.get('metadata', {}) or {}).get('thumbnail_url')}\n")
|
|
debug_file.write(f"[execute-step] FULL visual_anchor: {json_module.dumps(va, default=str)[:500]}\n")
|
|
debug_file.flush()
|
|
|
|
# Convert to catalog execute format
|
|
catalog_request = {
|
|
'type': step_type,
|
|
'step_id': step_id,
|
|
'parameters': parameters
|
|
}
|
|
|
|
# Call the internal catalog execute endpoint
|
|
from catalog_routes import catalog_bp
|
|
|
|
# Direct execution via catalog
|
|
try:
|
|
# Import the execute function directly
|
|
from catalog_routes import execute_action as catalog_execute
|
|
# We need to simulate Flask request context - use internal call
|
|
from flask import current_app
|
|
with current_app.test_request_context(
|
|
'/api/vwb/catalog/execute',
|
|
method='POST',
|
|
data=__import__('json').dumps(catalog_request),
|
|
content_type='application/json'
|
|
):
|
|
response = catalog_execute()
|
|
if hasattr(response, 'get_json'):
|
|
result = response.get_json()
|
|
else:
|
|
result = __import__('json').loads(response[0].get_data(as_text=True))
|
|
|
|
# Convert to expected format
|
|
if result.get('success') and result.get('result'):
|
|
return jsonify({
|
|
'success': result['result'].get('status') == 'success',
|
|
'output': result['result'].get('output_data', {}),
|
|
'error': result['result'].get('error', {}).get('message') if result['result'].get('error') else None
|
|
})
|
|
else:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': result.get('error', 'Échec de l\'exécution')
|
|
})
|
|
except Exception as inner_e:
|
|
print(f"❌ Erreur exécution interne: {inner_e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(inner_e)
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f"❌ Erreur execute-step: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
# Create database tables - only if migrations not available
|
|
# In production, use: flask db upgrade
|
|
import os
|
|
migrations_dir = os.path.join(os.path.dirname(__file__), 'migrations')
|
|
with app.app_context():
|
|
if not os.path.exists(migrations_dir):
|
|
# No migrations folder - use create_all for development
|
|
db.create_all()
|
|
print("✅ [DB] Tables créées avec db.create_all()")
|
|
else:
|
|
# Migrations available - check if alembic_version exists
|
|
from sqlalchemy import inspect
|
|
inspector = inspect(db.engine)
|
|
if 'alembic_version' not in inspector.get_table_names():
|
|
# First run with migrations - create tables and stamp
|
|
db.create_all()
|
|
print("✅ [DB] Tables créées, utiliser 'flask db stamp head' pour initialiser les migrations")
|
|
|
|
# Initialize VisualTargetManager with RPA Vision V3 components (optional)
|
|
try:
|
|
from core.capture.screen_capturer import ScreenCapturer
|
|
from core.detection.ui_detector import UIDetector
|
|
from core.embedding.fusion_engine import FusionEngine
|
|
|
|
# Only initialize if the related blueprints were actually loaded
|
|
if VISUAL_TARGETS_BP_AVAILABLE and init_visual_target_manager:
|
|
screen_capturer = ScreenCapturer()
|
|
ui_detector = UIDetector()
|
|
fusion_engine = FusionEngine()
|
|
init_visual_target_manager(screen_capturer, ui_detector, fusion_engine)
|
|
|
|
if ELEMENT_DETECTION_BP_AVAILABLE and init_element_detection:
|
|
# Reuse the same instances when possible
|
|
if 'ui_detector' not in locals():
|
|
ui_detector = UIDetector()
|
|
if 'screen_capturer' not in locals():
|
|
screen_capturer = ScreenCapturer()
|
|
init_element_detection(ui_detector, screen_capturer)
|
|
|
|
if (VISUAL_TARGETS_BP_AVAILABLE and init_visual_target_manager) or (ELEMENT_DETECTION_BP_AVAILABLE and init_element_detection):
|
|
print("✅ Services visuels initialisés (VisualTargets / ElementDetection)")
|
|
except ImportError as e:
|
|
print(f"⚠️ Core RPA non disponible pour l'initialisation visuelle: {e}")
|
|
except Exception as e:
|
|
print(f"❌ Erreur lors de l'initialisation des services visuels: {e}")
|
|
|
|
if __name__ == '__main__':
|
|
port = int(os.getenv('PORT', 5000))
|
|
debug = os.getenv('FLASK_ENV') == 'development'
|
|
|
|
socketio.run(
|
|
app,
|
|
host='0.0.0.0',
|
|
port=port,
|
|
debug=debug,
|
|
use_reloader=debug,
|
|
allow_unsafe_werkzeug=True # For development only
|
|
)
|