Files
rpa_vision_v3/visual_workflow_builder/backend/api/executions.py
Dom 38a1a5ddd8 feat(coaching): Implement complete COACHING mode infrastructure
Add comprehensive COACHING mode system with:

Backend:
- core/coaching module with session persistence and metrics
- CoachingSessionPersistence for pause/resume sessions
- CoachingMetricsCollector with learning progress tracking
- REST API blueprint for coaching sessions management
- Execution integration with COACHING mode support

Frontend:
- CoachingPanel component with keyboard shortcuts
- Decision buttons (accept/reject/correct/manual/skip)
- Real-time stats display and correction editor
- CorrectionPacksDashboard for pack visualization
- WebSocket hooks for real-time COACHING events

Metrics & Monitoring:
- WorkflowLearningMetrics with confidence scoring
- GlobalCoachingMetrics for system-wide analytics
- AUTO mode readiness detection (85% acceptance threshold)
- Learning progress levels (OBSERVATION → COACHING → AUTO)

Tests:
- E2E tests for complete OBSERVATION → AUTO journey
- Session persistence and recovery tests
- Metrics threshold validation tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 08:40:54 +01:00

268 lines
7.4 KiB
Python

"""
Executions API Blueprint
Provides REST endpoints for workflow execution management.
Exigences: 6.1, 6.2, 6.3, 6.4
"""
from flask import Blueprint, jsonify, request
from services.execution_integration import get_executor
executions_bp = Blueprint('executions', __name__)
@executions_bp.route('/', methods=['POST'])
def start_execution():
"""
Lance l'exécution d'un workflow.
Body JSON:
workflow_id: str - ID du workflow à exécuter
variables: dict (optionnel) - Variables d'entrée
mode: str (optionnel) - 'normal' ou 'coaching' (défaut: 'normal')
Returns:
execution_id: str - ID de l'exécution lancée
"""
data = request.get_json() or {}
workflow_id = data.get('workflow_id')
variables = data.get('variables', {})
mode = data.get('mode', 'normal')
if not workflow_id:
return jsonify({'error': 'workflow_id requis'}), 400
try:
executor = get_executor()
if mode == 'coaching':
execution_id = executor.execute_workflow_coaching(
workflow_id=workflow_id,
variables=variables
)
else:
execution_id = executor.execute_workflow(
workflow_id=workflow_id,
variables=variables
)
return jsonify({
'execution_id': execution_id,
'workflow_id': workflow_id,
'mode': mode,
'status': 'started'
}), 201
except ValueError as e:
return jsonify({'error': str(e)}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@executions_bp.route('/coaching', methods=['POST'])
def start_coaching_execution():
"""
Lance l'exécution d'un workflow en mode COACHING.
En mode COACHING, chaque étape est soumise à l'utilisateur pour
validation/correction avant exécution.
Body JSON:
workflow_id: str - ID du workflow à exécuter
variables: dict (optionnel) - Variables d'entrée
Returns:
execution_id: str - ID de l'exécution COACHING lancée
"""
data = request.get_json() or {}
workflow_id = data.get('workflow_id')
variables = data.get('variables', {})
if not workflow_id:
return jsonify({'error': 'workflow_id requis'}), 400
try:
executor = get_executor()
execution_id = executor.execute_workflow_coaching(
workflow_id=workflow_id,
variables=variables
)
return jsonify({
'execution_id': execution_id,
'workflow_id': workflow_id,
'mode': 'coaching',
'status': 'started',
'message': 'Connectez-vous via WebSocket pour recevoir les suggestions'
}), 201
except ValueError as e:
return jsonify({'error': str(e)}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@executions_bp.route('/', methods=['GET'])
def list_executions():
"""
Liste les exécutions.
Query params:
workflow_id: str (optionnel) - Filtrer par workflow
mode: str (optionnel) - Filtrer par mode ('normal', 'coaching')
Returns:
executions: list - Liste des exécutions
"""
workflow_id = request.args.get('workflow_id')
mode = request.args.get('mode')
executor = get_executor()
executions = executor.list_executions(workflow_id)
# Filtrer par mode si demandé
if mode:
if mode == 'coaching':
executions = [
e for e in executions
if executor.is_coaching_execution(e['execution_id'])
]
elif mode == 'normal':
executions = [
e for e in executions
if not executor.is_coaching_execution(e['execution_id'])
]
return jsonify({'executions': executions})
@executions_bp.route('/<execution_id>', methods=['GET'])
def get_execution(execution_id):
"""
Récupère le statut et les détails d'une exécution.
Returns:
Détails de l'exécution incluant statut, progression, logs
"""
executor = get_executor()
result = executor.get_execution_status(execution_id)
if result is None:
return jsonify({'error': f'Exécution {execution_id} introuvable'}), 404
response = result.to_dict()
response['is_coaching'] = executor.is_coaching_execution(execution_id)
return jsonify(response)
@executions_bp.route('/<execution_id>/cancel', methods=['POST'])
def cancel_execution(execution_id):
"""
Annule une exécution en cours.
Returns:
success: bool - Si l'annulation a réussi
"""
executor = get_executor()
if executor.cancel_execution(execution_id):
return jsonify({
'execution_id': execution_id,
'status': 'cancelled',
'message': 'Exécution annulée avec succès'
})
else:
return jsonify({
'error': 'Impossible d\'annuler l\'exécution (déjà terminée ou inexistante)'
}), 400
@executions_bp.route('/<execution_id>/coaching/decision', methods=['POST'])
def submit_coaching_decision(execution_id):
"""
Soumet une décision COACHING pour une exécution.
Alternative REST à WebSocket pour soumettre une décision.
Body JSON:
decision: str - 'accept' | 'reject' | 'correct' | 'manual' | 'skip'
correction: dict (optionnel) - Correction si decision == 'correct'
feedback: str (optionnel) - Commentaire utilisateur
Returns:
success: bool
"""
data = request.get_json() or {}
decision = data.get('decision')
if not decision:
return jsonify({'error': 'decision requis'}), 400
valid_decisions = ['accept', 'reject', 'correct', 'manual', 'skip']
if decision not in valid_decisions:
return jsonify({
'error': f'decision invalide. Valeurs acceptées: {valid_decisions}'
}), 400
executor = get_executor()
if not executor.is_coaching_execution(execution_id):
return jsonify({
'error': f'{execution_id} n\'est pas une exécution COACHING'
}), 400
decision_response = {
'decision': decision,
'correction': data.get('correction'),
'feedback': data.get('feedback'),
'executed_manually': decision == 'manual'
}
success = executor.submit_coaching_decision(execution_id, decision_response)
if success:
return jsonify({
'execution_id': execution_id,
'decision': decision,
'status': 'accepted'
})
else:
return jsonify({
'error': 'Impossible de soumettre la décision'
}), 400
@executions_bp.route('/<execution_id>/coaching/stats', methods=['GET'])
def get_coaching_stats(execution_id):
"""
Récupère les statistiques COACHING d'une exécution.
Returns:
stats: dict - Statistiques (suggestions, accepted, rejected, etc.)
"""
executor = get_executor()
if not executor.is_coaching_execution(execution_id):
return jsonify({
'error': f'{execution_id} n\'est pas une exécution COACHING'
}), 400
stats = executor.get_coaching_stats(execution_id)
if stats is None:
stats = {
'suggestions_made': 0,
'accepted': 0,
'rejected': 0,
'corrected': 0,
'manual_executions': 0
}
return jsonify({
'execution_id': execution_id,
'stats': stats
})