diff --git a/visual_workflow_builder/backend/api_v3/dag_execute.py b/visual_workflow_builder/backend/api_v3/dag_execute.py index 9d73cb204..4835ee5d0 100644 --- a/visual_workflow_builder/backend/api_v3/dag_execute.py +++ b/visual_workflow_builder/backend/api_v3/dag_execute.py @@ -1155,3 +1155,34 @@ def replay_resume_proxy(): 'detail': f'Streaming server non disponible ({streaming_url})'}), 502 except req.RequestException as e: return jsonify({'error': 'streaming_unreachable', 'detail': str(e)}), 502 + + +# --------------------------------------------------------------------------- +# QW4 — Proxy GET /api/v3/replay/state/ → streaming /replay/{id} +# Forward Bearer token vers le serveur streaming. +# Permet à App.tsx de récupérer le state du replay actif (Agent V1 Windows) +# pour afficher PauseDialog quand status = paused_need_help avec safety_checks. +# --------------------------------------------------------------------------- +@api_v3_bp.route('/replay/state/', methods=['GET']) +def replay_state_proxy(replay_id): + """Proxy QW4 vers le serveur streaming pour récupérer le state replay actif.""" + import requests as req + + streaming_url = os.environ.get('RPA_STREAMING_URL', 'http://localhost:5005') + token = os.environ.get('RPA_API_TOKEN', '') + headers = {} + if token: + headers['Authorization'] = f'Bearer {token}' + + try: + resp = req.get( + f'{streaming_url}/api/v1/traces/stream/replay/{replay_id}', + headers=headers, + timeout=5, + ) + return resp.content, resp.status_code, {'Content-Type': 'application/json'} + except req.ConnectionError: + return jsonify({'error': 'streaming_unreachable', + 'detail': f'Streaming server non disponible ({streaming_url})'}), 502 + except req.RequestException as e: + return jsonify({'error': 'streaming_unreachable', 'detail': str(e)}), 502 diff --git a/visual_workflow_builder/frontend_v4/src/App.tsx b/visual_workflow_builder/frontend_v4/src/App.tsx index b4f2c56fe..2e41e6549 100644 --- a/visual_workflow_builder/frontend_v4/src/App.tsx +++ b/visual_workflow_builder/frontend_v4/src/App.tsx @@ -62,6 +62,13 @@ function App() { const [healingCandidates, setHealingCandidates] = useState([]); const [healingStepInfo, setHealingStepInfo] = useState(null); + // QW4 — Replay streaming Windows en cours (Agent V1 distant). + // Quand un replay distant est lancé via ExecutionControls "→ Windows", + // ExecutionControls appelle setStreamingReplayId(replay_id) et un useEffect + // poll /api/v3/replay/state/ pour fusionner safety_checks + pause_* + // dans appState.execution → PauseDialog s'affiche. + const [streamingReplayId, setStreamingReplayId] = useState(null); + // Charger l'état initial const loadState = useCallback(async () => { try { @@ -123,6 +130,62 @@ function App() { return () => clearInterval(interval); }, [isExecutionRunning, loadState]); + // QW4 — Polling state replay streaming (Agent V1 Windows distant) + // Tourne dès qu'un replay distant a été lancé. Récupère safety_checks, + // pause_message, pause_reason et les fusionne dans appState.execution + // pour que PauseDialog s'affiche quand status = paused_need_help. + useEffect(() => { + if (!streamingReplayId) return; + + let stopped = false; + const pollReplay = async () => { + try { + const resp = await fetch(`/api/v3/replay/state/${streamingReplayId}`); + if (!resp.ok) return; + const state = await resp.json(); + if (stopped) return; + + // Fusionner dans appState.execution sans écraser le reste. + setAppState(prev => { + if (!prev) return prev; + const prevExec = prev.execution || { + id: streamingReplayId, + workflow_id: prev.session?.active_workflow_id || '', + status: 'pending', + progress: 0, + current_step_index: 0, + completed_steps: 0, + failed_steps: 0, + total_steps: 0, + }; + return { + ...prev, + execution: { + ...prevExec, + status: state.status || prevExec.status, + pause_message: state.pause_message || state.message, + pause_reason: state.pause_reason, + safety_checks: state.safety_checks || [], + replay_id: streamingReplayId, + }, + }; + }); + + // Stopper le polling si le replay est terminé / annulé. + if (state.status && ['completed', 'error', 'cancelled'].includes(state.status)) { + setStreamingReplayId(null); + } + } catch (err) { + // ignore (le serveur streaming peut être momentanément indispo) + } + }; + + // Tick immédiat puis toutes les 1s. + pollReplay(); + const interval = setInterval(pollReplay, 1000); + return () => { stopped = true; clearInterval(interval); }; + }, [streamingReplayId]); + // Convertir les étapes en nœuds React Flow // Les edges ne sont générées automatiquement que lors du premier chargement // d'un workflow. Ensuite, les connexions manuelles de l'utilisateur sont préservées. @@ -452,6 +515,7 @@ function App() { execution={appState?.execution || null} onStart={handleStartExecution} onStop={handleStopExecution} + onWindowsReplayStarted={(replayId) => setStreamingReplayId(replayId)} /> void; onStop: () => void; + // QW4 — Notifie App.tsx quand un replay streaming Windows est lancé, + // pour qu'il poll /api/v3/replay/state/ et affiche PauseDialog au besoin. + onWindowsReplayStarted?: (replayId: string) => void; } -export default function ExecutionControls({ execution, onStart, onStop }: Props) { +export default function ExecutionControls({ execution, onStart, onStop, onWindowsReplayStarted }: Props) { const isRunning = execution?.status === 'running' || execution?.status === 'paused'; const [windowsStatus, setWindowsStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle'); @@ -56,6 +59,11 @@ export default function ExecutionControls({ execution, onStart, onStop }: Props) const result = await resp.json(); if (result.replay_id) { setWindowsStatus('sent'); + // QW4 — propage le replay_id à App.tsx pour activer le polling + // /api/v3/replay/state/ (PauseDialog si paused_need_help). + if (onWindowsReplayStarted) { + try { onWindowsReplayStarted(result.replay_id); } catch {} + } alert('Replay lancé ! Réduisez cette fenêtre maintenant.\nLes actions commenceront dans 5 secondes.'); setTimeout(() => setWindowsStatus('idle'), 5000); } else { @@ -75,9 +83,27 @@ export default function ExecutionControls({ execution, onStart, onStop }: Props) {!isRunning ? (
{userOS === 'linux' ? ( - + <> + + + ) : (