11"""Chat bridge implementations for conversational agents."""
22
33import asyncio
4+ import json
45import logging
56import os
67import uuid
7- from typing import Any
8+ from typing import Any , Dict
89from urllib .parse import urlparse
910
1011import socketio # type: ignore[import-untyped]
@@ -57,6 +58,10 @@ def __init__(
5758 self ._client : AsyncClient | None = None
5859 self ._connected_event = asyncio .Event ()
5960
61+ # Set CAS_WEBSOCKET_DISABLED when using the debugger to prevent websocket errors from
62+ # interrupting the debugging session. Events will be logged instead of being sent.
63+ self ._websocket_disabled = os .environ .get ("CAS_WEBSOCKET_DISABLED" ) == "true"
64+
6065 async def connect (self , timeout : float = 10.0 ) -> None :
6166 """Establish WebSocket connection to the server.
6267
@@ -86,37 +91,43 @@ async def connect(self, timeout: float = 10.0) -> None:
8691 self ._client .on ("connect" , self ._handle_connect )
8792 self ._client .on ("disconnect" , self ._handle_disconnect )
8893 self ._client .on ("connect_error" , self ._handle_connect_error )
94+ self ._client .on ("ConversationEvent" , self ._handle_conversation_event )
8995
9096 self ._connected_event .clear ()
9197
92- try :
93- # Attempt to connect with timeout
94- await asyncio .wait_for (
95- self ._client .connect (
96- url = self .websocket_url ,
97- socketio_path = self .websocket_path ,
98- headers = self .headers ,
99- auth = self .auth ,
100- transports = ["websocket" ],
101- ),
102- timeout = timeout ,
98+ if self ._websocket_disabled :
99+ logger .warning (
100+ "SocketIOChatBridge is in debug mode. Not connecting websocket."
103101 )
102+ else :
103+ try :
104+ # Attempt to connect with timeout
105+ await asyncio .wait_for (
106+ self ._client .connect (
107+ url = self .websocket_url ,
108+ socketio_path = self .websocket_path ,
109+ headers = self .headers ,
110+ auth = self .auth ,
111+ transports = ["websocket" ],
112+ ),
113+ timeout = timeout ,
114+ )
104115
105- await asyncio .wait_for (self ._connected_event .wait (), timeout = timeout )
116+ await asyncio .wait_for (self ._connected_event .wait (), timeout = timeout )
106117
107- except asyncio .TimeoutError as e :
108- error_message = (
109- f"Failed to connect to WebSocket server within { timeout } s timeout"
110- )
111- logger .error (error_message )
112- await self ._cleanup_client ()
113- raise RuntimeError (error_message ) from e
118+ except asyncio .TimeoutError as e :
119+ error_message = (
120+ f"Failed to connect to WebSocket server within { timeout } s timeout"
121+ )
122+ logger .error (error_message )
123+ await self ._cleanup_client ()
124+ raise RuntimeError (error_message ) from e
114125
115- except Exception as e :
116- error_message = f"Failed to connect to WebSocket server: { e } "
117- logger .error (error_message )
118- await self ._cleanup_client ()
119- raise RuntimeError (error_message ) from e
126+ except Exception as e :
127+ error_message = f"Failed to connect to WebSocket server: { e } "
128+ logger .error (error_message )
129+ await self ._cleanup_client ()
130+ raise RuntimeError (error_message ) from e
120131
121132 async def disconnect (self ) -> None :
122133 """Close the WebSocket connection gracefully.
@@ -149,7 +160,7 @@ async def emit_message_event(
149160 if self ._client is None :
150161 raise RuntimeError ("WebSocket client not connected. Call connect() first." )
151162
152- if not self ._connected_event .is_set ():
163+ if not self ._connected_event .is_set () and not self . _websocket_disabled :
153164 raise RuntimeError ("WebSocket client not in connected state" )
154165
155166 try :
@@ -166,7 +177,12 @@ async def emit_message_event(
166177 mode = "json" , exclude_none = True , by_alias = True
167178 )
168179
169- await self ._client .emit ("ConversationEvent" , event_data )
180+ if self ._websocket_disabled :
181+ logger .info (
182+ f"SocketIOChatBridge is in debug mode. Not sending event: { json .dumps (event_data )} "
183+ )
184+ else :
185+ await self ._client .emit ("ConversationEvent" , event_data )
170186
171187 # Store the current message ID, used for emitting interrupt events.
172188 self ._current_message_id = message_event .message_id
@@ -184,7 +200,7 @@ async def emit_exchange_end_event(self) -> None:
184200 if self ._client is None :
185201 raise RuntimeError ("WebSocket client not connected. Call connect() first." )
186202
187- if not self ._connected_event .is_set ():
203+ if not self ._connected_event .is_set () and not self . _websocket_disabled :
188204 raise RuntimeError ("WebSocket client not in connected state" )
189205
190206 try :
@@ -200,7 +216,12 @@ async def emit_exchange_end_event(self) -> None:
200216 mode = "json" , exclude_none = True , by_alias = True
201217 )
202218
203- await self ._client .emit ("ConversationEvent" , event_data )
219+ if self ._websocket_disabled :
220+ logger .info (
221+ f"SocketIOChatBridge is in debug mode. Not sending event: { json .dumps (event_data )} "
222+ )
223+ else :
224+ await self ._client .emit ("ConversationEvent" , event_data )
204225
205226 except Exception as e :
206227 logger .error (f"Error sending conversation event to WebSocket: { e } " )
@@ -230,7 +251,12 @@ async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
230251 event_data = interrupt_event .model_dump (
231252 mode = "json" , exclude_none = True , by_alias = True
232253 )
233- await self ._client .emit ("ConversationEvent" , event_data )
254+ if self ._websocket_disabled :
255+ logger .info (
256+ f"SocketIOChatBridge is in debug mode. Not sending event: { json .dumps (event_data )} "
257+ )
258+ else :
259+ await self ._client .emit ("ConversationEvent" , event_data )
234260 except Exception as e :
235261 logger .warning (f"Error sending interrupt event: { e } " )
236262
@@ -265,6 +291,14 @@ async def _handle_connect_error(self, data: Any) -> None:
265291 """Handle connection error event."""
266292 logger .error (f"WebSocket connection error: { data } " )
267293
294+ async def _handle_conversation_event (
295+ self , event : Dict [str , Any ], _sid : str
296+ ) -> None :
297+ """Handle received ConversationEvent events."""
298+ error_event = event .get ("conversationError" )
299+ if error_event :
300+ logger .error (f"Conversation error: { json .dumps (error_event )} " )
301+
268302 async def _cleanup_client (self ) -> None :
269303 """Clean up client resources."""
270304 self ._connected_event .clear ()
@@ -315,6 +349,13 @@ def get_chat_bridge(
315349 websocket_url = f"wss://{ host } ?conversationId={ context .conversation_id } "
316350 websocket_path = "autopilotforeveryone_/websocket_/socket.io"
317351
352+ if os .environ .get ("CAS_WEBSOCKET_HOST" ):
353+ websocket_url = f"ws://{ os .environ .get ('CAS_WEBSOCKET_HOST' )} ?conversationId={ context .conversation_id } "
354+ websocket_path = "/socket.io"
355+ logger .warning (
356+ f"CAS_WEBSOCKET_HOST is set. Using websocket_url '{ websocket_url } { websocket_path } '."
357+ )
358+
318359 # Build headers from context
319360 headers = {
320361 "Authorization" : f"Bearer { os .environ .get ('UIPATH_ACCESS_TOKEN' , '' )} " ,
0 commit comments