diff --git a/run.sh b/run.sh index effce06e1..124057153 100755 --- a/run.sh +++ b/run.sh @@ -36,7 +36,7 @@ show_help() { echo -e " ${BLUE}--server${NC} 🌐 API Server seul (port 8000)" echo -e " ${PURPLE}--dashboard${NC} 📊 Dashboard Web seul (port 5001)" echo -e " ${YELLOW}--monitoring${NC} 📈 Interface de monitoring (port 5003)" - echo -e " ${CYAN}--workflow${NC} 🔧 Visual Workflow Builder (port 3000)" + echo -e " ${CYAN}--workflow${NC} 🔧 Visual Workflow Builder (port 3002)" echo -e " ${GREEN}--agent${NC} 📹 Agent V0 (capture tool)" echo -e " ${BLUE}--chat${NC} 💬 Agent Chat (port 5002)" echo "" @@ -64,7 +64,7 @@ show_help() { echo -e " Dashboard: ${PURPLE}http://localhost:5001${NC}" echo -e " Agent Chat: ${BLUE}http://localhost:5002${NC}" echo -e " Monitoring: ${YELLOW}http://localhost:5003${NC}" - echo -e " Workflow Builder: ${CYAN}http://localhost:3000${NC}" + echo -e " Workflow Builder: ${CYAN}http://localhost:3002${NC}" echo "" } @@ -186,7 +186,7 @@ fi # Step 3: Check/Create Virtual Environment echo -e "${BLUE}[3/7]${NC} Setting up Python environment..." -VENV_DIR="venv_v3" +VENV_DIR=".venv" if [ ! -d "$VENV_DIR" ]; then echo " Creating virtual environment..." @@ -349,7 +349,8 @@ cleanup() { pkill -f "port 5001" 2>/dev/null || true pkill -f "port 5002" 2>/dev/null || true pkill -f "port 5003" 2>/dev/null || true - pkill -f "port 3000" 2>/dev/null || true + pkill -f "port 3002" 2>/dev/null || true + pkill -f "vite.*3002" 2>/dev/null || true deactivate 2>/dev/null || true echo -e "${GREEN}✓${NC} Cleanup complete" @@ -465,12 +466,12 @@ EOF workflow) echo "" - echo -e "${CYAN}🔧 Launching Visual Workflow Builder on port 3000...${NC}" + echo -e "${CYAN}🔧 Launching Visual Workflow Builder v4...${NC}" echo "" - echo "Access: http://localhost:3000" + echo "Access: http://localhost:3002 (frontend) / http://localhost:5001 (backend)" echo "" cd visual_workflow_builder - ./run.sh + ./run_v4.sh cd .. ;; @@ -581,10 +582,10 @@ if __name__ == '__main__': EOF MONITORING_PID=$(start_service "Monitoring" "$VENV_DIR/bin/python3 monitoring_server.py" "5003" "monitoring.log") - # Start Visual Workflow Builder (in background) - echo "Starting Visual Workflow Builder (port 3000)..." + # Start Visual Workflow Builder v4 (in background) + echo "Starting Visual Workflow Builder v4 (port 3002)..." cd visual_workflow_builder - ./run.sh > ../logs/workflow.log 2>&1 & + ./run_v4.sh > ../logs/workflow.log 2>&1 & WORKFLOW_PID=$! cd .. sleep 3 @@ -602,7 +603,7 @@ EOF echo -e " API Server: ${BLUE}http://localhost:8000${NC}" echo -e " Dashboard: ${PURPLE}http://localhost:5001${NC}" echo -e " Monitoring: ${YELLOW}http://localhost:5003${NC}" - echo -e " Workflow Builder: ${CYAN}http://localhost:3000${NC}" + echo -e " Workflow Builder: ${CYAN}http://localhost:3002${NC}" echo "" echo -e "${BOLD}📊 Logs:${NC}" echo " tail -f logs/api.log" @@ -697,7 +698,7 @@ EOF check_service_status "Dashboard" "5001" check_service_status "Agent Chat" "5002" check_service_status "Monitoring" "5003" - check_service_status "Workflow Builder" "3000" + check_service_status "Workflow Builder" "3002" echo "" ;; @@ -708,7 +709,8 @@ EOF pkill -f "port 5001" 2>/dev/null || true pkill -f "port 5002" 2>/dev/null || true pkill -f "port 5003" 2>/dev/null || true - pkill -f "port 3000" 2>/dev/null || true + pkill -f "port 3002" 2>/dev/null || true + pkill -f "vite.*3002" 2>/dev/null || true echo -e "${GREEN}✓${NC} All services stopped" ;; esac diff --git a/visual_workflow_builder/backend/actions/intelligence/analyser_avec_ia.py b/visual_workflow_builder/backend/actions/intelligence/analyser_avec_ia.py index 5e42974e6..e718046e5 100644 --- a/visual_workflow_builder/backend/actions/intelligence/analyser_avec_ia.py +++ b/visual_workflow_builder/backend/actions/intelligence/analyser_avec_ia.py @@ -22,11 +22,12 @@ import requests from ..base_action import BaseVWBAction, VWBActionResult, VWBActionStatus from ...contracts.error import VWBErrorType, create_vwb_error from ...contracts.visual_anchor import VWBVisualAnchor +import os -# Configuration Ollama par défaut -OLLAMA_DEFAULT_URL = "http://localhost:11434" -OLLAMA_DEFAULT_MODEL = "qwen2.5-vl:7b" +# Configuration Ollama par défaut (configurable via variables d'environnement) +OLLAMA_DEFAULT_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") +OLLAMA_DEFAULT_MODEL = os.environ.get("VLM_MODEL", "qwen3-vl:8b") class VWBAnalyserAvecIAAction(BaseVWBAction): diff --git a/visual_workflow_builder/backend/api_v3/workflow.py b/visual_workflow_builder/backend/api_v3/workflow.py index c23905db0..7736fe00e 100644 --- a/visual_workflow_builder/backend/api_v3/workflow.py +++ b/visual_workflow_builder/backend/api_v3/workflow.py @@ -7,14 +7,18 @@ GET /api/v3/workflow/{id} POST /api/v3/workflow/{id}/step PUT /api/v3/workflow/{id}/step/{step_id} DELETE /api/v3/workflow/{id}/step/{step_id} +POST /api/v3/workflow/{id}/validate +POST /api/v3/workflow/{id}/export-training """ from flask import jsonify, request from datetime import datetime import uuid +import json +import os from . import api_v3_bp from db.models import db, Workflow, Step, VisualAnchor, get_session_state -from contracts.action_contracts import enforce_action_contract, ContractValidationError, get_required_params +from contracts.action_contracts import enforce_action_contract, ContractValidationError, get_required_params, validate_action_contract def generate_id(prefix: str) -> str: @@ -461,3 +465,166 @@ def reorder_steps(workflow_id: str): 'success': False, 'error': str(e) }), 500 + + +def _validate_workflow_steps(workflow): + """ + Logique de validation partagée entre validate et export-training. + Retourne (errors, warnings, steps). + """ + errors = [] + warnings = [] + + steps = Step.query.filter_by(workflow_id=workflow.id).order_by(Step.order).all() + + if len(steps) == 0: + errors.append("Le workflow n'a aucune étape") + return errors, warnings, steps + + for i, step in enumerate(steps): + step_label = step.label or step.action_type + prefix = f"Étape {i+1} ({step_label})" + + # Valider le contrat d'action + params = step.parameters or {} + violations = validate_action_contract(step.action_type, params) + for v in violations: + errors.append(f"{prefix}: {v.message}") + + # Vérifier l'ancre visuelle si requise + required = get_required_params(step.action_type) + if 'visual_anchor' in required and not step.anchor_id: + errors.append(f"{prefix}: ancre visuelle manquante") + + # Warnings + if not step.label or step.label == step.action_type: + warnings.append(f"{prefix}: pas de label personnalisé") + + return errors, warnings, steps + + +@api_v3_bp.route('/workflow//validate', methods=['POST']) +def validate_workflow(workflow_id: str): + """ + Valide la structure d'un workflow. + + Response: + { + "success": true, + "is_valid": true/false, + "errors": [...], + "warnings": [...], + "step_count": 12 + } + """ + try: + workflow = Workflow.query.get(workflow_id) + if not workflow: + return jsonify({ + 'success': False, + 'error': f"Workflow '{workflow_id}' non trouvé" + }), 404 + + errors, warnings, steps = _validate_workflow_steps(workflow) + + is_valid = len(errors) == 0 + + print(f"{'✅' if is_valid else '❌'} [API v3] Validation workflow {workflow_id}: " + f"{len(errors)} erreur(s), {len(warnings)} warning(s)") + + return jsonify({ + 'success': True, + 'is_valid': is_valid, + 'errors': errors, + 'warnings': warnings, + 'step_count': len(steps) + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@api_v3_bp.route('/workflow//export-training', methods=['POST']) +def export_for_training(workflow_id: str): + """ + Exporte un workflow validé au format JSON d'entraînement. + + Response: + { + "success": true, + "export_path": "training_data/workflow_xxx_1234567890.json", + "training_entry": { ... } + } + """ + try: + workflow = Workflow.query.get(workflow_id) + if not workflow: + return jsonify({ + 'success': False, + 'error': f"Workflow '{workflow_id}' non trouvé" + }), 404 + + # Valider d'abord + errors, warnings, steps = _validate_workflow_steps(workflow) + if len(errors) > 0: + return jsonify({ + 'success': False, + 'error': 'Le workflow contient des erreurs de validation', + 'errors': errors + }), 400 + + # Construire le training entry + training_entry = { + 'workflow_id': workflow.id, + 'workflow_name': workflow.name, + 'description': workflow.description or '', + 'tags': workflow.tags if hasattr(workflow, 'tags') and workflow.tags else [], + 'steps': [], + 'exported_at': datetime.utcnow().isoformat(), + 'metadata': { + 'step_count': len(steps), + 'action_types': list(set(s.action_type for s in steps)), + 'has_anchors': any(s.anchor_id for s in steps), + 'warnings': warnings + } + } + + for step in steps: + step_data = { + 'order': step.order, + 'action_type': step.action_type, + 'label': step.label, + 'parameters': step.parameters or {}, + 'has_anchor': bool(step.anchor_id) + } + training_entry['steps'].append(step_data) + + # Sauvegarder dans training_data/ + training_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'training_data') + os.makedirs(training_dir, exist_ok=True) + + timestamp = int(datetime.now().timestamp()) + filename = f"workflow_{workflow_id}_{timestamp}.json" + filepath = os.path.join(training_dir, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(training_entry, f, ensure_ascii=False, indent=2) + + export_path = f"training_data/{filename}" + + print(f"📦 [API v3] Workflow exporté pour entraînement: {export_path}") + + return jsonify({ + 'success': True, + 'export_path': export_path, + 'training_entry': training_entry + }) + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 diff --git a/visual_workflow_builder/backend/contracts/action_contracts.py b/visual_workflow_builder/backend/contracts/action_contracts.py index 471d557c5..359e6ab25 100644 --- a/visual_workflow_builder/backend/contracts/action_contracts.py +++ b/visual_workflow_builder/backend/contracts/action_contracts.py @@ -279,10 +279,9 @@ VWB_ACTION_CONTRACTS: Dict[str, ActionContract] = { "ai_analyze_text": ActionContract( action_type="ai_analyze_text", - description="Analyser du texte avec IA", - required_params=["visual_anchor", "analysis_prompt"], - optional_params=["model", "output_variable"], - param_validators={"visual_anchor": lambda p: has_visual_anchor({"visual_anchor": p})} + description="Analyser du texte ou une image avec IA", + required_params=[], # prompt est vérifié manuellement (accept prompt ou analysis_prompt) + optional_params=["prompt", "analysis_prompt", "visual_anchor", "input_text", "model", "output_variable", "temperature", "timeout_ms"], ), "db_save_data": ActionContract( diff --git a/visual_workflow_builder/backend/services/intelligent_executor.py b/visual_workflow_builder/backend/services/intelligent_executor.py index 2b617b1b8..5a4df5d47 100644 --- a/visual_workflow_builder/backend/services/intelligent_executor.py +++ b/visual_workflow_builder/backend/services/intelligent_executor.py @@ -817,36 +817,20 @@ def find_and_click( except Exception as seeclick_err: print(f"⚠️ [Vision] Erreur SeeClick: {seeclick_err}") - # === STRATÉGIE 5: Coordonnées statiques (dernier recours) === + # === Toutes les méthodes visuelles ont échoué === if anchor_bbox: best_conf = max(global_result.get('confidence', 0), 0) - - # Utiliser coordonnées statiques seulement si confiance > 0.5 - if best_conf >= 0.5: - print(f"⚠️ [Vision] Fallback: coordonnées statiques (confiance: {best_conf:.2f})") - center_x = anchor_bbox.get('x', 0) + anchor_bbox.get('width', 0) // 2 - center_y = anchor_bbox.get('y', 0) + anchor_bbox.get('height', 0) // 2 - return { - 'found': True, - 'coordinates': {'x': int(center_x), 'y': int(center_y)}, - 'bbox': anchor_bbox, - 'confidence': best_conf, - 'method': 'static_fallback', - 'search_time_ms': (_time.time() - start_time) * 1000, - 'candidates': [] - } - else: - print(f"❌ [Vision] Ancre non trouvée (confiance: {best_conf:.2f})") - return { - 'found': False, - 'coordinates': None, - 'bbox': anchor_bbox, - 'confidence': best_conf, - 'method': 'not_found', - 'search_time_ms': (_time.time() - start_time) * 1000, - 'candidates': [], - 'reason': 'Ancre non trouvée à l\'écran' - } + print(f"❌ [Vision] Ancre non trouvée à l'écran (meilleure confiance: {best_conf:.2f})") + return { + 'found': False, + 'coordinates': None, + 'bbox': anchor_bbox, + 'confidence': best_conf, + 'method': 'not_found', + 'search_time_ms': (_time.time() - start_time) * 1000, + 'candidates': [], + 'reason': 'Aucune méthode visuelle n\'a trouvé l\'ancre à l\'écran' + } # Pas de bbox, impossible de chercher return {