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>
This commit is contained in:
267
visual_workflow_builder/backend/api/executions.py
Normal file
267
visual_workflow_builder/backend/api/executions.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
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
|
||||
})
|
||||
Reference in New Issue
Block a user