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:
Dom
2026-02-17 10:56:17 +01:00
parent 3ff36e3c79
commit 4c9a6d293f
5 changed files with 202 additions and 49 deletions

View File

@@ -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