Skip to content

Commit 049870f

Browse files
feat: conversational agent support
1 parent 594eda0 commit 049870f

File tree

10 files changed

+1191
-30
lines changed

10 files changed

+1191
-30
lines changed

src/uipath/_cli/_chat/_bridge.py

Lines changed: 61 additions & 29 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._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
@@ -89,34 +94,39 @@ 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,
97+
if self._websocket_disabled:
98+
logger.warning(
99+
"SocketIOChatBridge is in debug mode. Not connecting websocket."
103100
)
101+
else:
102+
try:
103+
# Attempt to connect with timeout
104+
await asyncio.wait_for(
105+
self._client.connect(
106+
url=self.websocket_url,
107+
socketio_path=self.websocket_path,
108+
headers=self.headers,
109+
auth=self.auth,
110+
transports=["websocket"],
111+
),
112+
timeout=timeout,
113+
)
104114

105-
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
115+
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
106116

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

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

121131
async def disconnect(self) -> None:
122132
"""Close the WebSocket connection gracefully.
@@ -149,7 +159,7 @@ async def emit_message_event(
149159
if self._client is None:
150160
raise RuntimeError("WebSocket client not connected. Call connect() first.")
151161

152-
if not self._connected_event.is_set():
162+
if not self._connected_event.is_set() and not self._websocket_disabled:
153163
raise RuntimeError("WebSocket client not in connected state")
154164

155165
try:
@@ -166,7 +176,12 @@ async def emit_message_event(
166176
mode="json", exclude_none=True, by_alias=True
167177
)
168178

169-
await self._client.emit("ConversationEvent", event_data)
179+
if self._websocket_disabled:
180+
logger.info(
181+
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
182+
)
183+
else:
184+
await self._client.emit("ConversationEvent", event_data)
170185

171186
# Store the current message ID, used for emitting interrupt events.
172187
self._current_message_id = message_event.message_id
@@ -184,7 +199,7 @@ async def emit_exchange_end_event(self) -> None:
184199
if self._client is None:
185200
raise RuntimeError("WebSocket client not connected. Call connect() first.")
186201

187-
if not self._connected_event.is_set():
202+
if not self._connected_event.is_set() and not self._websocket_disabled:
188203
raise RuntimeError("WebSocket client not in connected state")
189204

190205
try:
@@ -200,7 +215,12 @@ async def emit_exchange_end_event(self) -> None:
200215
mode="json", exclude_none=True, by_alias=True
201216
)
202217

203-
await self._client.emit("ConversationEvent", event_data)
218+
if self._websocket_disabled:
219+
logger.info(
220+
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
221+
)
222+
else:
223+
await self._client.emit("ConversationEvent", event_data)
204224

205225
except Exception as e:
206226
logger.error(f"Error sending conversation event to WebSocket: {e}")
@@ -230,7 +250,12 @@ async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
230250
event_data = interrupt_event.model_dump(
231251
mode="json", exclude_none=True, by_alias=True
232252
)
233-
await self._client.emit("ConversationEvent", event_data)
253+
if self._websocket_disabled:
254+
logger.info(
255+
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
256+
)
257+
else:
258+
await self._client.emit("ConversationEvent", event_data)
234259
except Exception as e:
235260
logger.warning(f"Error sending interrupt event: {e}")
236261

@@ -315,6 +340,13 @@ def get_chat_bridge(
315340
websocket_url = f"wss://{host}?conversationId={context.conversation_id}"
316341
websocket_path = "autopilotforeveryone_/websocket_/socket.io"
317342

343+
if os.environ.get("CAS_WEBSOCKET_HOST"):
344+
websocket_url = f"ws://{os.environ.get('CAS_WEBSOCKET_HOST')}?conversationId={context.conversation_id}"
345+
websocket_path = "/socket.io"
346+
logger.warning(
347+
f"CAS_WEBSOCKET_HOST is set. Using websocket_url '{websocket_url}{websocket_path}'."
348+
)
349+
318350
# Build headers from context
319351
headers = {
320352
"Authorization": f"Bearer {os.environ.get('UIPATH_ACCESS_TOKEN', '')}",

src/uipath/agent/models/agent.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,15 @@ class AgentDefinition(BaseModel):
803803
validate_by_name=True, validate_by_alias=True, extra="allow"
804804
)
805805

806+
@property
807+
def is_conversational(self) -> bool:
808+
"""Checks the settings.engine property to determine if the agent is conversational."""
809+
if hasattr(self, "metadata") and self.metadata:
810+
metadata = self.metadata
811+
if hasattr(metadata, "is_conversational"):
812+
return metadata.is_conversational
813+
return False
814+
806815
@staticmethod
807816
def _normalize_guardrails(v: Dict[str, Any]) -> None:
808817
guards = v.get("guardrails")

src/uipath/agent/react/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
This module includes UiPath ReAct Agent Loop constructs such as prompts, tools
44
"""
55

6+
from .conversational_prompts import (
7+
PromptUserSettings,
8+
generate_conversational_agent_system_prompt,
9+
)
610
from .prompts import AGENT_SYSTEM_PROMPT_TEMPLATE
711
from .tools import (
812
END_EXECUTION_TOOL,
@@ -19,4 +23,6 @@
1923
"RAISE_ERROR_TOOL",
2024
"EndExecutionToolSchemaModel",
2125
"RaiseErrorToolSchemaModel",
26+
"PromptUserSettings",
27+
"generate_conversational_agent_system_prompt",
2228
]

0 commit comments

Comments
 (0)