Skip to content

Commit cedc9c5

Browse files
feat: conversational agent support
1 parent e834a60 commit cedc9c5

5 files changed

Lines changed: 81 additions & 31 deletions

File tree

src/uipath/_cli/_chat/_bridge.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Chat bridge implementations for conversational agents."""
22

33
import asyncio
4+
import json
45
import logging
56
import os
67
import uuid
@@ -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._debug_mode = 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
@@ -89,34 +94,37 @@ async def connect(self, timeout: float = 10.0) -> None:
8994

9095
self._connected_event.clear()
9196

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,
103-
)
97+
if self._debug_mode:
98+
logger.warning("SocketIOChatBridge is in debug mode. Not connecting websocket.")
99+
else:
100+
try:
101+
# Attempt to connect with timeout
102+
await asyncio.wait_for(
103+
self._client.connect(
104+
url=self.websocket_url,
105+
socketio_path=self.websocket_path,
106+
headers=self.headers,
107+
auth=self.auth,
108+
transports=["websocket"],
109+
),
110+
timeout=timeout,
111+
)
104112

105-
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
113+
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
106114

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
115+
except asyncio.TimeoutError as e:
116+
error_message = (
117+
f"Failed to connect to WebSocket server within {timeout}s timeout"
118+
)
119+
logger.error(error_message)
120+
await self._cleanup_client()
121+
raise RuntimeError(error_message) from e
114122

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
123+
except Exception as e:
124+
error_message = f"Failed to connect to WebSocket server: {e}"
125+
logger.error(error_message)
126+
await self._cleanup_client()
127+
raise RuntimeError(error_message) from e
120128

121129
async def disconnect(self) -> None:
122130
"""Close the WebSocket connection gracefully.
@@ -149,7 +157,7 @@ async def emit_message_event(
149157
if self._client is None:
150158
raise RuntimeError("WebSocket client not connected. Call connect() first.")
151159

152-
if not self._connected_event.is_set():
160+
if not self._connected_event.is_set() and not self._debug_mode:
153161
raise RuntimeError("WebSocket client not in connected state")
154162

155163
try:
@@ -166,7 +174,10 @@ async def emit_message_event(
166174
mode="json", exclude_none=True, by_alias=True
167175
)
168176

169-
await self._client.emit("ConversationEvent", event_data)
177+
if self._debug_mode:
178+
logger.info(f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}")
179+
else:
180+
await self._client.emit("ConversationEvent", event_data)
170181

171182
# Store the current message ID, used for emitting interrupt events.
172183
self._current_message_id = message_event.message_id
@@ -184,7 +195,7 @@ async def emit_exchange_end_event(self) -> None:
184195
if self._client is None:
185196
raise RuntimeError("WebSocket client not connected. Call connect() first.")
186197

187-
if not self._connected_event.is_set():
198+
if not self._connected_event.is_set() and not self._debug_mode:
188199
raise RuntimeError("WebSocket client not in connected state")
189200

190201
try:
@@ -200,7 +211,10 @@ async def emit_exchange_end_event(self) -> None:
200211
mode="json", exclude_none=True, by_alias=True
201212
)
202213

203-
await self._client.emit("ConversationEvent", event_data)
214+
if self._debug_mode:
215+
logger.info(f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}")
216+
else:
217+
await self._client.emit("ConversationEvent", event_data)
204218

205219
except Exception as e:
206220
logger.error(f"Error sending conversation event to WebSocket: {e}")
@@ -230,7 +244,10 @@ async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
230244
event_data = interrupt_event.model_dump(
231245
mode="json", exclude_none=True, by_alias=True
232246
)
233-
await self._client.emit("ConversationEvent", event_data)
247+
if self._debug_mode:
248+
logger.info(f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}")
249+
else:
250+
await self._client.emit("ConversationEvent", event_data)
234251
except Exception as e:
235252
logger.warning(f"Error sending interrupt event: {e}")
236253

@@ -315,6 +332,11 @@ def get_chat_bridge(
315332
websocket_url = f"wss://{host}?conversationId={context.conversation_id}"
316333
websocket_path = "autopilotforeveryone_/websocket_/socket.io"
317334

335+
if os.environ.get("CAS_WEBSOCKET_HOST"):
336+
websocket_url = f"ws://{os.environ.get('CAS_WEBSOCKET_HOST')}?conversationId={context.conversation_id}"
337+
websocket_path = "/socket.io"
338+
logger.warning(f"CAS_WEBSOCKET_HOST is set. Using websocket_url '{websocket_url}{websocket_path}'.")
339+
318340
# Build headers from context
319341
headers = {
320342
"Authorization": f"Bearer {os.environ.get('UIPATH_ACCESS_TOKEN', '')}",

src/uipath/agent/models/agent.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,11 @@ class AgentDefinition(BaseModel):
795795
validate_by_name=True, validate_by_alias=True, extra="allow"
796796
)
797797

798+
@property
799+
def is_conversational(self) -> bool:
800+
"""Checks the settings.engine property to determine if the agent is conversational."""
801+
return self.settings.engine == "conversational-v1"
802+
798803
@staticmethod
799804
def _normalize_guardrails(v: Dict[str, Any]) -> None:
800805
guards = v.get("guardrails")

src/uipath/agent/react/prompts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
99
{{systemPrompt}}
1010
11-
Your adhere strictly to the following rules to ensure accuracy and data validity:
11+
You adhere strictly to the following rules to ensure accuracy and data validity:
1212
1313
<rules>
1414
Data Verification and Tool Analysis:

src/uipath/platform/common/interrupt_models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,7 @@ class WaitDocumentExtraction(BaseModel):
134134
"""Model representing a wait document extraction task creation."""
135135

136136
extraction: StartExtractionResponse
137+
138+
class UserMessageWait(BaseModel):
139+
"""Model representing a wait for a new user input message."""
140+

src/uipath/platform/resume_triggers/_protocol.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
WaitJob,
3636
WaitTask,
3737
)
38+
from uipath.platform.common.interrupt_models import UserMessageWait
3839
from uipath.platform.context_grounding import DeepRagStatus
3940
from uipath.platform.errors import (
4041
BatchTransformNotCompleteException,
@@ -362,6 +363,10 @@ async def create_trigger(self, suspend_value: Any) -> UiPathResumeTrigger:
362363
await self._handle_ixp_extraction_trigger(
363364
suspend_value, resume_trigger, uipath
364365
)
366+
case UiPathResumeTriggerType.USER_MESSAGE_WAIT:
367+
self._handle_user_message_wait_trigger(
368+
suspend_value, resume_trigger, uipath
369+
)
365370
case _:
366371
raise UiPathFaultedTriggerError(
367372
ErrorCategory.SYSTEM,
@@ -395,6 +400,8 @@ def _determine_trigger_type(self, value: Any) -> UiPathResumeTriggerType:
395400
return UiPathResumeTriggerType.BATCH_RAG
396401
if isinstance(value, (DocumentExtraction, WaitDocumentExtraction)):
397402
return UiPathResumeTriggerType.IXP_EXTRACTION
403+
if isinstance(value, UserMessageWait):
404+
return UiPathResumeTriggerType.USER_MESSAGE_WAIT
398405
# default to API trigger
399406
return UiPathResumeTriggerType.API
400407

@@ -419,6 +426,8 @@ def _determine_trigger_name(self, value: Any) -> UiPathResumeTriggerName:
419426
return UiPathResumeTriggerName.BATCH_RAG
420427
if isinstance(value, (DocumentExtraction, WaitDocumentExtraction)):
421428
return UiPathResumeTriggerName.EXTRACTION
429+
if isinstance(value, UserMessageWait):
430+
return UiPathResumeTriggerName.USER_MESSAGE_WAIT
422431
# default to API trigger
423432
return UiPathResumeTriggerName.API
424433

@@ -579,7 +588,17 @@ def _handle_api_trigger(
579588
inbox_id=str(uuid.uuid4()), request=serialize_object(value)
580589
)
581590

591+
def _handle_user_message_wait_trigger(
592+
self, value: Any, resume_trigger: UiPathResumeTrigger, uipath: UiPath
593+
) -> None:
594+
"""Handle user message wait resume triggers.
582595
596+
Args:
597+
value: The suspend value
598+
resume_trigger: The resume trigger to populate
599+
"""
600+
# Nothing to do?
601+
583602
class UiPathResumeTriggerHandler:
584603
"""Combined handler for creating and reading resume triggers.
585604

0 commit comments

Comments
 (0)