-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Expand file tree
/
Copy pathworkflow_ws.py
More file actions
221 lines (195 loc) · 7.66 KB
/
Copy pathworkflow_ws.py
File metadata and controls
221 lines (195 loc) · 7.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
"""
Workflow WebSocket Handler
Provides real-time progress updates for running workflows
"""
import asyncio
from datetime import datetime
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from services.workflow_service import workflow_service
router = APIRouter()
class ConnectionManager:
"""Manages WebSocket connections for workflow updates"""
def __init__(self):
self.active_connections: dict[str, list[WebSocket]] = {}
async def connect(self, websocket: WebSocket, task_id: str):
await websocket.accept()
if task_id not in self.active_connections:
self.active_connections[task_id] = []
self.active_connections[task_id].append(websocket)
def disconnect(self, websocket: WebSocket, task_id: str):
if task_id in self.active_connections:
if websocket in self.active_connections[task_id]:
self.active_connections[task_id].remove(websocket)
if not self.active_connections[task_id]:
del self.active_connections[task_id]
async def broadcast(self, task_id: str, message: dict):
if task_id in self.active_connections:
for connection in self.active_connections[task_id]:
try:
await connection.send_json(message)
except Exception:
pass
manager = ConnectionManager()
TERMINAL_STATUSES = {
"completed",
"error",
"cancelled",
"interrupted",
"incomplete",
"completed_with_warnings",
}
@router.websocket("/workflow/{task_id}")
async def workflow_websocket(websocket: WebSocket, task_id: str):
"""
WebSocket endpoint for real-time workflow progress updates.
Connect to receive:
- progress: Workflow step progress updates
- complete: Workflow completion notification
- error: Error notifications
Message format:
{
"type": "progress" | "complete" | "error",
"task_id": str,
"progress": int, # 0-100
"message": str,
"timestamp": str,
"result": dict | null, # Only for complete type
"error": str | null # Only for error type
}
"""
await manager.connect(websocket, task_id)
print(f"[WorkflowWS] Connected: task={task_id[:8]}...")
# Subscribe to get our own queue for this task
queue = workflow_service.subscribe(task_id)
task = workflow_service.get_task_by_any_id(task_id)
print(
f"[WorkflowWS] Subscribed: task={task_id[:8]}... queue={queue is not None} task={task is not None}"
)
if not task:
await websocket.send_json(
{
"type": "error",
"task_id": task_id,
"error": "Task not found",
"timestamp": datetime.utcnow().isoformat(),
}
)
await websocket.close()
return
# Send current status
await websocket.send_json(
{
"type": "status",
"task_id": task_id,
"status": task.status,
"progress": task.progress,
"message": task.message,
"error": task.error,
"error_details": task.error_details,
"timestamp": datetime.utcnow().isoformat(),
}
)
# Send pending interaction if any (fixes race condition where interaction_required
# was broadcast before WebSocket connected)
if task.pending_interaction:
print(f"[WorkflowWS] Sending missed pending interaction: task={task_id[:8]}...")
await websocket.send_json(
{
"type": "interaction_required",
"task_id": task_id,
"interaction_type": task.pending_interaction.get("type"),
"title": task.pending_interaction.get("title"),
"description": task.pending_interaction.get("description"),
"data": task.pending_interaction.get("data"),
"options": task.pending_interaction.get("options"),
"required": task.pending_interaction.get("required"),
"timestamp": datetime.utcnow().isoformat(),
}
)
try:
# If task is already completed, send final status and close
if task.status in TERMINAL_STATUSES:
if task.status in {
"completed",
"incomplete",
"completed_with_warnings",
}:
await websocket.send_json(
{
"type": "complete",
"task_id": task_id,
"status": task.status,
"result": task.result,
"timestamp": datetime.utcnow().isoformat(),
}
)
elif task.status == "error":
await websocket.send_json(
{
"type": "error",
"task_id": task_id,
"error": task.error,
"error_details": task.error_details,
"timestamp": datetime.utcnow().isoformat(),
}
)
elif task.status == "cancelled":
await websocket.send_json(
{
"type": "cancelled",
"task_id": task_id,
"status": "cancelled",
"reason": task.message or "Task cancelled",
"timestamp": datetime.utcnow().isoformat(),
}
)
elif task.status == "interrupted":
await websocket.send_json(
{
"type": "interrupted",
"task_id": task_id,
"status": "interrupted",
"reason": task.message
or "Task was interrupted by a backend restart.",
"timestamp": datetime.utcnow().isoformat(),
}
)
# Close WebSocket (don't cleanup immediately - keep task for status queries)
await websocket.close()
return
# Stream progress updates
if queue:
while True:
try:
# Wait for progress update with timeout
message = await asyncio.wait_for(queue.get(), timeout=60.0)
msg_type = message.get("type")
print(
f"[WorkflowWS] Sending: task={task_id[:8]}... type={msg_type}"
)
await websocket.send_json(message)
# Check if workflow is complete
if msg_type in ("complete", "error", "cancelled"):
print(
f"[WorkflowWS] Workflow finished: task={task_id[:8]}... type={msg_type}"
)
# Wait a bit before closing to ensure frontend processes the message
await asyncio.sleep(0.5)
await websocket.close()
break
except asyncio.TimeoutError:
# Send heartbeat
await websocket.send_json(
{
"type": "heartbeat",
"task_id": task_id,
"timestamp": datetime.utcnow().isoformat(),
}
)
except WebSocketDisconnect:
pass
finally:
manager.disconnect(websocket, task_id)
# Unsubscribe from task updates
if queue:
workflow_service.unsubscribe(task_id, queue)