CORVO-AI commited on
Commit
7409b02
·
verified ·
1 Parent(s): 6f53717

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -71
app.py CHANGED
@@ -1,17 +1,7 @@
1
  """
2
  MindStudio WebSocket Streaming Client — Flask API Endpoint
3
- Exposes:
4
- GET /connect → pre-connect WebSocket
5
- POST /chat → send message, stream response
6
-
7
- Headers (on /chat):
8
- Authorization: Bearer <access_token>
9
-
10
- Body (on /chat):
11
- {
12
- "message": "your message here",
13
- "threadId": "your-thread-id"
14
- }
15
  """
16
 
17
  import asyncio
@@ -40,17 +30,12 @@ TARGET_MODEL = "claude-4-7-opus"
40
  WS_URL = "wss://api-socket.mindstudio.ai/"
41
 
42
  # ─────────────────────────────────────────────
43
- # GLOBAL STATE — one WS client per access token
44
  # ─────────────────────────────────────────────
45
 
46
- # { access_token: MindStudioClient }
47
  _clients: dict = {}
48
  _clients_lock = threading.Lock()
49
 
50
- # Each client runs its own asyncio event loop in a dedicated thread
51
- # { access_token: asyncio.AbstractEventLoop }
52
- _loops: dict = {}
53
-
54
 
55
  # ─────────────────────────────────────────────
56
  # HELPER
@@ -63,6 +48,30 @@ def safe_get_model(debug_info: dict):
63
  return None
64
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  # ─────────────────────────────────────────────
67
  # MINDSTUDIO CLIENT
68
  # ─────────────────────────────────────────────
@@ -74,9 +83,7 @@ class MindStudioClient:
74
  self.continuation_token = INITIAL_CONTINUATION_TOKEN
75
  self.ws = None
76
  self.loop = asyncio.new_event_loop()
77
- self._ws_ready = threading.Event()
78
 
79
- # Start the event loop in a background thread
80
  self._thread = threading.Thread(target=self._run_loop, daemon=True)
81
  self._thread.start()
82
 
@@ -85,12 +92,11 @@ class MindStudioClient:
85
  self.loop.run_forever()
86
 
87
  def connect_sync(self):
88
- """Connect WebSocket synchronously (blocks until connected)."""
89
  future = asyncio.run_coroutine_threadsafe(self._connect(), self.loop)
90
- future.result(timeout=15)
91
 
92
  async def _connect(self):
93
- print(f"[WS] Connecting...")
94
  extra_headers = {
95
  "Origin": "https://app.mindstudio.ai",
96
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
@@ -102,21 +108,23 @@ class MindStudioClient:
102
  ping_interval=None,
103
  ping_timeout=None,
104
  )
105
- print(f"[WS] Connected! Subprotocol: {self.ws.subprotocol}")
106
- self._ws_ready.set()
 
 
 
 
 
 
107
 
108
  def is_connected(self) -> bool:
109
- return self.ws is not None and not self.ws.closed
110
 
111
  def send_message_stream(self, message: str, thread_id: str):
112
- """
113
- Generator that yields SSE-formatted chunks of the AI response.
114
- Runs async logic in the client's dedicated event loop.
115
- """
116
  import queue
117
 
118
  chunk_queue = queue.Queue()
119
- done_event = threading.Event()
120
 
121
  async def _run():
122
  try:
@@ -127,12 +135,12 @@ class MindStudioClient:
127
  await self._post_message(message, thread_id)
128
  await listen_task
129
  except Exception as e:
 
130
  chunk_queue.put(f"[ERROR] {e}")
131
  done_event.set()
132
 
133
  asyncio.run_coroutine_threadsafe(_run(), self.loop)
134
 
135
- # Yield chunks as they arrive
136
  while not done_event.is_set() or not chunk_queue.empty():
137
  try:
138
  chunk = chunk_queue.get(timeout=0.5)
@@ -165,6 +173,8 @@ class MindStudioClient:
165
  async with httpx.AsyncClient() as client:
166
  resp = await client.post(post_url, json=payload, headers=headers)
167
  print(f"[POST] Status: {resp.status_code}")
 
 
168
 
169
  async def _listen_for_response(self, chunk_queue, done_event):
170
  full_response = ""
@@ -180,6 +190,9 @@ class MindStudioClient:
180
  except websockets.exceptions.ConnectionClosed as e:
181
  print(f"[WS] Connection closed: {e}")
182
  break
 
 
 
183
 
184
  try:
185
  event = json.loads(raw_message)
@@ -188,12 +201,12 @@ class MindStudioClient:
188
 
189
  event_type = event.get("type", "")
190
 
191
- # ── Update continuation token ──
192
  if event_type == "Apps/ThreadContinuationActionUpdated":
193
  data = event.get("appThreadContinuationActionUpdated", {})
194
  new_token = data.get("continuationAction", {}).get("token")
195
  if new_token:
196
  self.continuation_token = new_token
 
197
 
198
  underlying = (
199
  data.get("continuationAction", {})
@@ -204,7 +217,6 @@ class MindStudioClient:
204
  if not detected_model:
205
  detected_model = underlying["model"]
206
 
207
- # ── Capture system post ID ──
208
  elif event_type == "Apps/ThreadPostsCreated":
209
  posts = event.get("appThreadPostsCreated", {}).get("posts", [])
210
  for post in posts:
@@ -230,12 +242,11 @@ class MindStudioClient:
230
  previous_content_length = len(content)
231
  full_response = content
232
 
233
- # ── Stream tokens ──
234
  elif event_type == "Apps/ThreadPostUpdated":
235
  post_data = event.get("appThreadPostUpdated", {})
236
- new_post = post_data.get("newPost", {})
237
- post_id = post_data.get("postId")
238
- chat_msg = new_post.get("chatMessage", {})
239
 
240
  if chat_msg.get("source") != "system":
241
  continue
@@ -249,7 +260,7 @@ class MindStudioClient:
249
  detected_model = model
250
 
251
  current_content = chat_msg.get("content", "")
252
- is_in_progress = chat_msg.get("isInProgress", True)
253
 
254
  if len(current_content) > previous_content_length:
255
  delta = current_content[previous_content_length:]
@@ -264,21 +275,22 @@ class MindStudioClient:
264
 
265
  done_event.set()
266
 
267
- async def close(self):
268
  if self.ws:
269
  await self.ws.close()
270
 
271
 
272
  # ─────────────────────────────────────────────
273
- # HELPER: get or create client for a token
274
  # ─────────────────────────────────────────────
275
 
276
  def get_or_create_client(access_token: str) -> MindStudioClient:
277
  with _clients_lock:
278
  client = _clients.get(access_token)
279
  if client and client.is_connected():
 
280
  return client
281
- # Create new client and connect
282
  client = MindStudioClient(access_token=access_token)
283
  client.connect_sync()
284
  _clients[access_token] = client
@@ -289,12 +301,17 @@ def get_or_create_client(access_token: str) -> MindStudioClient:
289
  # ROUTES
290
  # ─────────────────────────────────────────────
291
 
 
 
 
 
 
 
 
 
 
292
  @app.route("/connect", methods=["GET"])
293
  def connect():
294
- """
295
- Pre-warm the WebSocket connection.
296
- Pass Authorization: Bearer <token> header.
297
- """
298
  auth = request.headers.get("Authorization", "")
299
  if not auth.startswith("Bearer "):
300
  return jsonify({"error": "Missing or invalid Authorization header"}), 401
@@ -302,26 +319,20 @@ def connect():
302
  access_token = auth.removeprefix("Bearer ").strip()
303
 
304
  try:
305
- get_or_create_client(access_token)
306
- return jsonify({"status": "connected", "model": TARGET_MODEL}), 200
 
 
 
 
 
307
  except Exception as e:
 
308
  return jsonify({"error": str(e)}), 500
309
 
310
 
311
  @app.route("/chat", methods=["POST"])
312
  def chat():
313
- """
314
- Send a message and stream the AI response as SSE.
315
-
316
- Headers:
317
- Authorization: Bearer <access_token>
318
-
319
- Body JSON:
320
- {
321
- "message": "Hello!",
322
- "threadId": "your-thread-id"
323
- }
324
- """
325
  auth = request.headers.get("Authorization", "")
326
  if not auth.startswith("Bearer "):
327
  return jsonify({"error": "Missing or invalid Authorization header"}), 401
@@ -332,7 +343,7 @@ def chat():
332
  if not data:
333
  return jsonify({"error": "Invalid JSON body"}), 400
334
 
335
- message = data.get("message", "").strip()
336
  thread_id = data.get("threadId", "").strip()
337
 
338
  if not message:
@@ -361,19 +372,13 @@ def chat():
361
  )
362
 
363
 
364
- @app.route("/health", methods=["GET"])
365
- def health():
366
- return jsonify({"status": "ok", "model": TARGET_MODEL}), 200
367
-
368
-
369
  # ─────────────────────────────────────────────
370
  # ENTRY POINT
371
  # ─────────────────────────────────────────────
372
 
373
  if __name__ == "__main__":
374
- print("=" * 60)
375
- print(f" MindStudio Flask Streaming API")
376
- print(f" Model: {TARGET_MODEL}")
377
- print(f" Visit GET /connect first to pre-warm WebSocket")
378
- print("=" * 60)
379
  app.run(host="0.0.0.0", port=7860, debug=False, threaded=True)
 
1
  """
2
  MindStudio WebSocket Streaming Client — Flask API Endpoint
3
+ FIXED: 'ClientConnection' object has no attribute 'closed'
4
+ Compatible with websockets >= 11.x and older versions
 
 
 
 
 
 
 
 
 
 
5
  """
6
 
7
  import asyncio
 
30
  WS_URL = "wss://api-socket.mindstudio.ai/"
31
 
32
  # ─────────────────────────────────────────────
33
+ # GLOBAL STATE
34
  # ─────────────────────────────────────────────
35
 
 
36
  _clients: dict = {}
37
  _clients_lock = threading.Lock()
38
 
 
 
 
 
39
 
40
  # ─────────────────────────────────────────────
41
  # HELPER
 
48
  return None
49
 
50
 
51
+ def check_ws_open(ws) -> bool:
52
+ """
53
+ Universal check for WebSocket open state.
54
+ Handles both old and new websockets library versions.
55
+ """
56
+ if ws is None:
57
+ return False
58
+ try:
59
+ # websockets >= 11.x — ClientConnection
60
+ if hasattr(ws, 'close_code'):
61
+ return ws.close_code is None
62
+ # websockets < 11.x — WebSocketClientProtocol
63
+ if hasattr(ws, 'closed'):
64
+ return not ws.closed
65
+ # websockets >= 13.x — may use 'state'
66
+ if hasattr(ws, 'state'):
67
+ import websockets.connection
68
+ return ws.state == websockets.connection.State.OPEN
69
+ # fallback
70
+ return True
71
+ except Exception:
72
+ return False
73
+
74
+
75
  # ─────────────────────────────────────────────
76
  # MINDSTUDIO CLIENT
77
  # ─────────────────────────────────────────────
 
83
  self.continuation_token = INITIAL_CONTINUATION_TOKEN
84
  self.ws = None
85
  self.loop = asyncio.new_event_loop()
 
86
 
 
87
  self._thread = threading.Thread(target=self._run_loop, daemon=True)
88
  self._thread.start()
89
 
 
92
  self.loop.run_forever()
93
 
94
  def connect_sync(self):
 
95
  future = asyncio.run_coroutine_threadsafe(self._connect(), self.loop)
96
+ future.result(timeout=20)
97
 
98
  async def _connect(self):
99
+ print(f"[WS] Connecting to {WS_URL} ...")
100
  extra_headers = {
101
  "Origin": "https://app.mindstudio.ai",
102
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
 
108
  ping_interval=None,
109
  ping_timeout=None,
110
  )
111
+ print(f"[WS] Connected! Type: {type(self.ws).__name__}")
112
+ print(f"[WS] Subprotocol: {self.ws.subprotocol}")
113
+
114
+ # Log which attributes are available for debugging
115
+ has_closed = hasattr(self.ws, 'closed')
116
+ has_closecode = hasattr(self.ws, 'close_code')
117
+ has_state = hasattr(self.ws, 'state')
118
+ print(f"[WS] Attrs → closed={has_closed} close_code={has_closecode} state={has_state}")
119
 
120
  def is_connected(self) -> bool:
121
+ return check_ws_open(self.ws)
122
 
123
  def send_message_stream(self, message: str, thread_id: str):
 
 
 
 
124
  import queue
125
 
126
  chunk_queue = queue.Queue()
127
+ done_event = threading.Event()
128
 
129
  async def _run():
130
  try:
 
135
  await self._post_message(message, thread_id)
136
  await listen_task
137
  except Exception as e:
138
+ print(f"[ERROR] send_message_stream: {e}")
139
  chunk_queue.put(f"[ERROR] {e}")
140
  done_event.set()
141
 
142
  asyncio.run_coroutine_threadsafe(_run(), self.loop)
143
 
 
144
  while not done_event.is_set() or not chunk_queue.empty():
145
  try:
146
  chunk = chunk_queue.get(timeout=0.5)
 
173
  async with httpx.AsyncClient() as client:
174
  resp = await client.post(post_url, json=payload, headers=headers)
175
  print(f"[POST] Status: {resp.status_code}")
176
+ if resp.status_code not in (200, 204):
177
+ print(f"[POST] Body: {resp.text}")
178
 
179
  async def _listen_for_response(self, chunk_queue, done_event):
180
  full_response = ""
 
190
  except websockets.exceptions.ConnectionClosed as e:
191
  print(f"[WS] Connection closed: {e}")
192
  break
193
+ except Exception as e:
194
+ print(f"[WS] Recv error: {e}")
195
+ break
196
 
197
  try:
198
  event = json.loads(raw_message)
 
201
 
202
  event_type = event.get("type", "")
203
 
 
204
  if event_type == "Apps/ThreadContinuationActionUpdated":
205
  data = event.get("appThreadContinuationActionUpdated", {})
206
  new_token = data.get("continuationAction", {}).get("token")
207
  if new_token:
208
  self.continuation_token = new_token
209
+ print(f"[WS] Continuation token updated")
210
 
211
  underlying = (
212
  data.get("continuationAction", {})
 
217
  if not detected_model:
218
  detected_model = underlying["model"]
219
 
 
220
  elif event_type == "Apps/ThreadPostsCreated":
221
  posts = event.get("appThreadPostsCreated", {}).get("posts", [])
222
  for post in posts:
 
242
  previous_content_length = len(content)
243
  full_response = content
244
 
 
245
  elif event_type == "Apps/ThreadPostUpdated":
246
  post_data = event.get("appThreadPostUpdated", {})
247
+ new_post = post_data.get("newPost", {})
248
+ post_id = post_data.get("postId")
249
+ chat_msg = new_post.get("chatMessage", {})
250
 
251
  if chat_msg.get("source") != "system":
252
  continue
 
260
  detected_model = model
261
 
262
  current_content = chat_msg.get("content", "")
263
+ is_in_progress = chat_msg.get("isInProgress", True)
264
 
265
  if len(current_content) > previous_content_length:
266
  delta = current_content[previous_content_length:]
 
275
 
276
  done_event.set()
277
 
278
+ async def _close(self):
279
  if self.ws:
280
  await self.ws.close()
281
 
282
 
283
  # ─────────────────────────────────────────────
284
+ # HELPER: get or create client
285
  # ─────────────────────────────────────────────
286
 
287
  def get_or_create_client(access_token: str) -> MindStudioClient:
288
  with _clients_lock:
289
  client = _clients.get(access_token)
290
  if client and client.is_connected():
291
+ print("[CLIENT] Reusing existing connected client")
292
  return client
293
+ print("[CLIENT] Creating new client and connecting...")
294
  client = MindStudioClient(access_token=access_token)
295
  client.connect_sync()
296
  _clients[access_token] = client
 
301
  # ROUTES
302
  # ─────────────────────────────────────────────
303
 
304
+ @app.route("/health", methods=["GET"])
305
+ def health():
306
+ return jsonify({
307
+ "status": "ok",
308
+ "model": TARGET_MODEL,
309
+ "websockets_version": websockets.__version__,
310
+ }), 200
311
+
312
+
313
  @app.route("/connect", methods=["GET"])
314
  def connect():
 
 
 
 
315
  auth = request.headers.get("Authorization", "")
316
  if not auth.startswith("Bearer "):
317
  return jsonify({"error": "Missing or invalid Authorization header"}), 401
 
319
  access_token = auth.removeprefix("Bearer ").strip()
320
 
321
  try:
322
+ client = get_or_create_client(access_token)
323
+ ws_type = type(client.ws).__name__ if client.ws else "None"
324
+ return jsonify({
325
+ "status": "connected",
326
+ "model": TARGET_MODEL,
327
+ "ws_object_type": ws_type,
328
+ }), 200
329
  except Exception as e:
330
+ print(f"[CONNECT ERROR] {type(e).__name__}: {e}")
331
  return jsonify({"error": str(e)}), 500
332
 
333
 
334
  @app.route("/chat", methods=["POST"])
335
  def chat():
 
 
 
 
 
 
 
 
 
 
 
 
336
  auth = request.headers.get("Authorization", "")
337
  if not auth.startswith("Bearer "):
338
  return jsonify({"error": "Missing or invalid Authorization header"}), 401
 
343
  if not data:
344
  return jsonify({"error": "Invalid JSON body"}), 400
345
 
346
+ message = data.get("message", "").strip()
347
  thread_id = data.get("threadId", "").strip()
348
 
349
  if not message:
 
372
  )
373
 
374
 
 
 
 
 
 
375
  # ─────────────────────────────────────────────
376
  # ENTRY POINT
377
  # ─────────────────────────────────────────────
378
 
379
  if __name__ == "__main__":
380
+ print("="*60)
381
+ print(f" MindStudio Flask API | Model: {TARGET_MODEL}")
382
+ print(f" websockets version: {websockets.__version__}")
383
+ print("="*60)
 
384
  app.run(host="0.0.0.0", port=7860, debug=False, threaded=True)