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:
531
visual_workflow_builder/backend/api/coaching_sessions.py
Normal file
531
visual_workflow_builder/backend/api/coaching_sessions.py
Normal file
@@ -0,0 +1,531 @@
|
||||
"""
|
||||
COACHING Sessions API Blueprint
|
||||
|
||||
Provides REST endpoints for managing COACHING session persistence:
|
||||
- List/create/load sessions
|
||||
- Pause/resume sessions
|
||||
- Session statistics
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
# Add core to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
||||
|
||||
from core.coaching import (
|
||||
CoachingSessionPersistence,
|
||||
CoachingSessionState,
|
||||
get_coaching_persistence,
|
||||
)
|
||||
from core.coaching.session_persistence import SessionStatus
|
||||
|
||||
coaching_sessions_bp = Blueprint('coaching_sessions', __name__)
|
||||
|
||||
|
||||
def get_persistence() -> CoachingSessionPersistence:
|
||||
"""Get the coaching session persistence instance."""
|
||||
return get_coaching_persistence()
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/', methods=['GET'])
|
||||
def list_sessions():
|
||||
"""
|
||||
List COACHING sessions.
|
||||
|
||||
Query params:
|
||||
workflow_id: Filter by workflow ID
|
||||
status: Filter by status (active, paused, completed, failed, abandoned)
|
||||
limit: Maximum number of sessions (default: 100)
|
||||
|
||||
Returns:
|
||||
sessions: List of session summaries
|
||||
"""
|
||||
workflow_id = request.args.get('workflow_id')
|
||||
status_str = request.args.get('status')
|
||||
limit = int(request.args.get('limit', 100))
|
||||
|
||||
status = None
|
||||
if status_str:
|
||||
try:
|
||||
status = SessionStatus(status_str)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
'error': f'Invalid status. Valid values: {[s.value for s in SessionStatus]}'
|
||||
}), 400
|
||||
|
||||
persistence = get_persistence()
|
||||
sessions = persistence.list_sessions(
|
||||
workflow_id=workflow_id,
|
||||
status=status,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return jsonify({'sessions': sessions})
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/', methods=['POST'])
|
||||
def create_session():
|
||||
"""
|
||||
Create a new COACHING session.
|
||||
|
||||
Body JSON:
|
||||
workflow_id: str - ID of the workflow
|
||||
execution_id: str - ID of the execution
|
||||
total_steps: int (optional) - Total number of steps
|
||||
variables: dict (optional) - Initial variables
|
||||
metadata: dict (optional) - Additional metadata
|
||||
|
||||
Returns:
|
||||
session: Created session state
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
workflow_id = data.get('workflow_id')
|
||||
execution_id = data.get('execution_id')
|
||||
|
||||
if not workflow_id or not execution_id:
|
||||
return jsonify({'error': 'workflow_id and execution_id are required'}), 400
|
||||
|
||||
persistence = get_persistence()
|
||||
session = persistence.create_session(
|
||||
workflow_id=workflow_id,
|
||||
execution_id=execution_id,
|
||||
total_steps=data.get('total_steps', 0),
|
||||
variables=data.get('variables', {}),
|
||||
metadata=data.get('metadata', {})
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'message': 'Session created',
|
||||
'session': session.to_dict()
|
||||
}), 201
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/<session_id>', methods=['GET'])
|
||||
def get_session(session_id: str):
|
||||
"""
|
||||
Get a COACHING session by ID.
|
||||
|
||||
Returns:
|
||||
session: Full session state
|
||||
"""
|
||||
persistence = get_persistence()
|
||||
session = persistence.load_session(session_id)
|
||||
|
||||
if session is None:
|
||||
return jsonify({'error': f'Session {session_id} not found'}), 404
|
||||
|
||||
return jsonify({'session': session.to_dict()})
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/<session_id>', methods=['PUT'])
|
||||
def update_session(session_id: str):
|
||||
"""
|
||||
Update a COACHING session.
|
||||
|
||||
Body JSON:
|
||||
current_step_index: int (optional)
|
||||
variables: dict (optional)
|
||||
metadata: dict (optional)
|
||||
|
||||
Returns:
|
||||
session: Updated session state
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
persistence = get_persistence()
|
||||
session = persistence.load_session(session_id)
|
||||
|
||||
if session is None:
|
||||
return jsonify({'error': f'Session {session_id} not found'}), 404
|
||||
|
||||
# Update allowed fields
|
||||
if 'current_step_index' in data:
|
||||
session.current_step_index = data['current_step_index']
|
||||
if 'variables' in data:
|
||||
session.variables.update(data['variables'])
|
||||
if 'metadata' in data:
|
||||
session.metadata.update(data['metadata'])
|
||||
|
||||
persistence.save_session(session)
|
||||
|
||||
return jsonify({
|
||||
'message': 'Session updated',
|
||||
'session': session.to_dict()
|
||||
})
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/<session_id>', methods=['DELETE'])
|
||||
def delete_session(session_id: str):
|
||||
"""
|
||||
Delete a COACHING session.
|
||||
|
||||
Returns:
|
||||
success: bool
|
||||
"""
|
||||
persistence = get_persistence()
|
||||
|
||||
if persistence.delete_session(session_id):
|
||||
return jsonify({
|
||||
'message': 'Session deleted',
|
||||
'session_id': session_id
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': f'Session {session_id} not found'}), 404
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/<session_id>/decisions', methods=['POST'])
|
||||
def add_decision(session_id: str):
|
||||
"""
|
||||
Add a decision to a COACHING session.
|
||||
|
||||
Body JSON:
|
||||
step_index: int
|
||||
node_id: str
|
||||
action_type: str
|
||||
decision: str (accept, reject, correct, manual, skip)
|
||||
correction: dict (optional)
|
||||
feedback: str (optional)
|
||||
execution_success: bool (optional)
|
||||
|
||||
Returns:
|
||||
session: Updated session state
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
required = ['step_index', 'node_id', 'action_type', 'decision']
|
||||
for field in required:
|
||||
if field not in data:
|
||||
return jsonify({'error': f'{field} is required'}), 400
|
||||
|
||||
valid_decisions = ['accept', 'reject', 'correct', 'manual', 'skip']
|
||||
if data['decision'] not in valid_decisions:
|
||||
return jsonify({
|
||||
'error': f'Invalid decision. Valid values: {valid_decisions}'
|
||||
}), 400
|
||||
|
||||
persistence = get_persistence()
|
||||
session = persistence.load_session(session_id)
|
||||
|
||||
if session is None:
|
||||
return jsonify({'error': f'Session {session_id} not found'}), 404
|
||||
|
||||
from core.coaching.session_persistence import CoachingDecisionRecord
|
||||
|
||||
decision = CoachingDecisionRecord(
|
||||
step_index=data['step_index'],
|
||||
node_id=data['node_id'],
|
||||
action_type=data['action_type'],
|
||||
decision=data['decision'],
|
||||
correction=data.get('correction'),
|
||||
feedback=data.get('feedback'),
|
||||
execution_success=data.get('execution_success')
|
||||
)
|
||||
|
||||
session.add_decision(decision)
|
||||
persistence.save_session(session)
|
||||
|
||||
return jsonify({
|
||||
'message': 'Decision added',
|
||||
'session': session.to_dict()
|
||||
})
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/<session_id>/pause', methods=['POST'])
|
||||
def pause_session(session_id: str):
|
||||
"""
|
||||
Pause an active COACHING session.
|
||||
|
||||
Returns:
|
||||
success: bool
|
||||
"""
|
||||
persistence = get_persistence()
|
||||
|
||||
if persistence.pause_session(session_id):
|
||||
session = persistence.load_session(session_id)
|
||||
return jsonify({
|
||||
'message': 'Session paused',
|
||||
'session': session.to_dict() if session else None
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'error': 'Cannot pause session (not active or not found)'
|
||||
}), 400
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/<session_id>/resume', methods=['POST'])
|
||||
def resume_session(session_id: str):
|
||||
"""
|
||||
Resume a paused COACHING session.
|
||||
|
||||
Returns:
|
||||
session: Resumed session state
|
||||
"""
|
||||
persistence = get_persistence()
|
||||
session = persistence.resume_session(session_id)
|
||||
|
||||
if session:
|
||||
return jsonify({
|
||||
'message': 'Session resumed',
|
||||
'session': session.to_dict()
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'error': 'Cannot resume session (not paused or not found)'
|
||||
}), 400
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/<session_id>/complete', methods=['POST'])
|
||||
def complete_session(session_id: str):
|
||||
"""
|
||||
Mark a COACHING session as completed.
|
||||
|
||||
Body JSON:
|
||||
success: bool (default: True)
|
||||
error_message: str (optional)
|
||||
|
||||
Returns:
|
||||
session: Completed session state
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
success = data.get('success', True)
|
||||
error_message = data.get('error_message')
|
||||
|
||||
persistence = get_persistence()
|
||||
session = persistence.complete_session(
|
||||
session_id,
|
||||
success=success,
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
if session:
|
||||
return jsonify({
|
||||
'message': 'Session completed',
|
||||
'session': session.to_dict()
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': f'Session {session_id} not found'}), 404
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/<session_id>/abandon', methods=['POST'])
|
||||
def abandon_session(session_id: str):
|
||||
"""
|
||||
Mark a COACHING session as abandoned.
|
||||
|
||||
Returns:
|
||||
success: bool
|
||||
"""
|
||||
persistence = get_persistence()
|
||||
|
||||
if persistence.abandon_session(session_id):
|
||||
return jsonify({
|
||||
'message': 'Session abandoned',
|
||||
'session_id': session_id
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': f'Session {session_id} not found'}), 404
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/resumable', methods=['GET'])
|
||||
def get_resumable_sessions():
|
||||
"""
|
||||
Get all resumable sessions for a workflow.
|
||||
|
||||
Query params:
|
||||
workflow_id: str (required) - Workflow ID
|
||||
|
||||
Returns:
|
||||
sessions: List of resumable session states
|
||||
"""
|
||||
workflow_id = request.args.get('workflow_id')
|
||||
|
||||
if not workflow_id:
|
||||
return jsonify({'error': 'workflow_id is required'}), 400
|
||||
|
||||
persistence = get_persistence()
|
||||
sessions = persistence.get_resumable_sessions(workflow_id)
|
||||
|
||||
return jsonify({
|
||||
'sessions': [s.to_dict() for s in sessions]
|
||||
})
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/statistics', methods=['GET'])
|
||||
def get_statistics():
|
||||
"""
|
||||
Get overall COACHING session statistics.
|
||||
|
||||
Returns:
|
||||
statistics: Overall statistics
|
||||
"""
|
||||
persistence = get_persistence()
|
||||
stats = persistence.get_statistics()
|
||||
|
||||
return jsonify({'statistics': stats})
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/cleanup', methods=['POST'])
|
||||
def cleanup_sessions():
|
||||
"""
|
||||
Clean up old COACHING sessions.
|
||||
|
||||
Body JSON:
|
||||
max_age_days: int (default: 30)
|
||||
|
||||
Returns:
|
||||
removed_count: int - Number of sessions removed
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
max_age_days = data.get('max_age_days', 30)
|
||||
|
||||
persistence = get_persistence()
|
||||
removed = persistence.cleanup_old_sessions(max_age_days)
|
||||
|
||||
return jsonify({
|
||||
'message': f'Removed {removed} old sessions',
|
||||
'removed_count': removed
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Metrics & Monitoring Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@coaching_sessions_bp.route('/metrics/global', methods=['GET'])
|
||||
def get_global_metrics():
|
||||
"""
|
||||
Get global COACHING metrics across all workflows.
|
||||
|
||||
Returns:
|
||||
metrics: GlobalCoachingMetrics with aggregated data
|
||||
"""
|
||||
try:
|
||||
from core.coaching import get_metrics_collector
|
||||
collector = get_metrics_collector()
|
||||
metrics = collector.get_global_metrics()
|
||||
return jsonify({'metrics': metrics.to_dict()})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/metrics/workflow/<workflow_id>', methods=['GET'])
|
||||
def get_workflow_metrics(workflow_id: str):
|
||||
"""
|
||||
Get detailed learning metrics for a specific workflow.
|
||||
|
||||
Returns:
|
||||
metrics: WorkflowLearningMetrics with learning progress and recommendations
|
||||
"""
|
||||
try:
|
||||
from core.coaching import get_metrics_collector
|
||||
collector = get_metrics_collector()
|
||||
metrics = collector.get_workflow_metrics(workflow_id)
|
||||
return jsonify({'metrics': metrics.to_dict()})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/metrics/ready-for-auto', methods=['GET'])
|
||||
def get_workflows_ready_for_auto():
|
||||
"""
|
||||
Get list of workflows ready for autonomous mode.
|
||||
|
||||
Returns:
|
||||
workflows: List of workflow IDs ready for AUTO mode
|
||||
"""
|
||||
try:
|
||||
from core.coaching import get_metrics_collector
|
||||
collector = get_metrics_collector()
|
||||
|
||||
# Get global metrics to find workflows
|
||||
global_metrics = collector.get_global_metrics()
|
||||
|
||||
# Check each workflow
|
||||
ready_workflows = []
|
||||
persistence = get_persistence()
|
||||
all_sessions = persistence.list_sessions(limit=10000)
|
||||
|
||||
workflow_ids = set(s.get('workflow_id') for s in all_sessions if s.get('workflow_id'))
|
||||
|
||||
for workflow_id in workflow_ids:
|
||||
metrics = collector.get_workflow_metrics(workflow_id)
|
||||
if metrics.ready_for_auto:
|
||||
ready_workflows.append({
|
||||
'workflow_id': workflow_id,
|
||||
'confidence_score': metrics.confidence_score,
|
||||
'acceptance_rate': metrics.acceptance_rate,
|
||||
'total_sessions': metrics.total_sessions
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'workflows_ready': ready_workflows,
|
||||
'total_ready': len(ready_workflows)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@coaching_sessions_bp.route('/metrics/dashboard', methods=['GET'])
|
||||
def get_metrics_dashboard():
|
||||
"""
|
||||
Get comprehensive dashboard data for monitoring.
|
||||
|
||||
Returns all metrics needed for a monitoring dashboard:
|
||||
- Global statistics
|
||||
- Recent activity
|
||||
- Top workflows
|
||||
- Recommendations
|
||||
"""
|
||||
try:
|
||||
from core.coaching import get_metrics_collector
|
||||
collector = get_metrics_collector()
|
||||
|
||||
# Get global metrics
|
||||
global_metrics = collector.get_global_metrics()
|
||||
|
||||
# Get recent activity (last 7 days sessions)
|
||||
persistence = get_persistence()
|
||||
recent_sessions = persistence.list_sessions(limit=50)
|
||||
|
||||
# Build dashboard response
|
||||
dashboard = {
|
||||
'overview': {
|
||||
'total_workflows': global_metrics.total_workflows,
|
||||
'total_sessions': global_metrics.total_sessions,
|
||||
'active_sessions': global_metrics.active_sessions,
|
||||
'workflows_ready_for_auto': global_metrics.workflows_ready_for_auto,
|
||||
'workflows_in_learning': global_metrics.workflows_in_learning,
|
||||
},
|
||||
'rates': {
|
||||
'acceptance_rate': round(global_metrics.overall_acceptance_rate * 100, 1),
|
||||
'correction_rate': round(global_metrics.overall_correction_rate * 100, 1),
|
||||
},
|
||||
'activity': {
|
||||
'sessions_last_24h': global_metrics.sessions_last_24h,
|
||||
'decisions_last_24h': global_metrics.decisions_last_24h,
|
||||
},
|
||||
'decisions': {
|
||||
'total': global_metrics.total_decisions,
|
||||
'accepted': global_metrics.total_accepted,
|
||||
'rejected': global_metrics.total_rejected,
|
||||
'corrected': global_metrics.total_corrected,
|
||||
},
|
||||
'top_workflows': {
|
||||
'by_sessions': global_metrics.top_workflows_by_sessions,
|
||||
'by_corrections': global_metrics.top_workflows_by_corrections,
|
||||
},
|
||||
'recent_sessions': [
|
||||
{
|
||||
'session_id': s.get('session_id'),
|
||||
'workflow_id': s.get('workflow_id'),
|
||||
'status': s.get('status'),
|
||||
'updated_at': s.get('updated_at'),
|
||||
}
|
||||
for s in recent_sessions[:10]
|
||||
]
|
||||
}
|
||||
|
||||
return jsonify({'dashboard': dashboard})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
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
|
||||
})
|
||||
@@ -119,6 +119,13 @@ try:
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Blueprint correction_packs désactivé: {e}")
|
||||
|
||||
try:
|
||||
from api.coaching_sessions import coaching_sessions_bp
|
||||
app.register_blueprint(coaching_sessions_bp, url_prefix='/api/coaching-sessions')
|
||||
print("✅ Blueprint coaching_sessions enregistré")
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Blueprint coaching_sessions désactivé: {e}")
|
||||
|
||||
|
||||
# Import WebSocket handlers (optional)
|
||||
try:
|
||||
|
||||
1073
visual_workflow_builder/backend/services/execution_integration.py
Normal file
1073
visual_workflow_builder/backend/services/execution_integration.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user