feat(vwb): Améliorer outils IA et supprimer fallback statique
Backend: - analyser_avec_ia.py: centraliser URL Ollama via os.environ.get() - action_contracts.py: assouplir le contrat ai_analyze_text (mode texte sans ancre visuelle, accepter prompt ou analysis_prompt) - intelligent_executor.py: supprimer le fallback coordonnées statiques quand la vision échoue — renvoyer not_found pour self-healing - workflow.py: ajouter endpoints validate et export-training run.sh: - Corriger les ports (3000 → 3002) et le venv (venv_v3 → .venv) - Lancer run_v4.sh au lieu de l'ancien run.sh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
28
run.sh
28
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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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/<workflow_id>/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/<workflow_id>/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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user