Skip to content

Commit 04348d0

Browse files
mike-deem-uipathcristipufu
authored andcommitted
feat: conversational agent support
1 parent c79653a commit 04348d0

File tree

13 files changed

+1256
-37
lines changed

13 files changed

+1256
-37
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[project]
22
name = "uipath"
3-
version = "2.4.24"
3+
version = "2.5"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
88
"uipath-core>=0.1.4, <0.2.0",
9-
"uipath-runtime>=0.4.1, <0.5.0",
9+
"uipath-runtime>=0.5, <0.6.0",
1010
"click>=8.3.1",
1111
"httpx>=0.28.1",
1212
"pyjwt>=2.10.1",

src/uipath/_cli/_chat/_bridge.py

Lines changed: 70 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
@@ -55,6 +56,10 @@ def __init__(
5556
self._client: Any | None = None
5657
self._connected_event = asyncio.Event()
5758

59+
# Set CAS_WEBSOCKET_DISABLED when using the debugger to prevent websocket errors from
60+
# interrupting the debugging session. Events will be logged instead of being sent.
61+
self._websocket_disabled = os.environ.get("CAS_WEBSOCKET_DISABLED") == "true"
62+
5863
async def connect(self, timeout: float = 10.0) -> None:
5964
"""Establish WebSocket connection to the server.
6065
@@ -87,37 +92,43 @@ async def connect(self, timeout: float = 10.0) -> None:
8792
self._client.on("connect", self._handle_connect)
8893
self._client.on("disconnect", self._handle_disconnect)
8994
self._client.on("connect_error", self._handle_connect_error)
95+
self._client.on("ConversationEvent", self._handle_conversation_event)
9096

9197
self._connected_event.clear()
9298

93-
try:
94-
# Attempt to connect with timeout
95-
await asyncio.wait_for(
96-
self._client.connect(
97-
url=self.websocket_url,
98-
socketio_path=self.websocket_path,
99-
headers=self.headers,
100-
auth=self.auth,
101-
transports=["websocket"],
102-
),
103-
timeout=timeout,
99+
if self._websocket_disabled:
100+
logger.warning(
101+
"SocketIOChatBridge is in debug mode. Not connecting websocket."
104102
)
103+
else:
104+
try:
105+
# Attempt to connect with timeout
106+
await asyncio.wait_for(
107+
self._client.connect(
108+
url=self.websocket_url,
109+
socketio_path=self.websocket_path,
110+
headers=self.headers,
111+
auth=self.auth,
112+
transports=["websocket"],
113+
),
114+
timeout=timeout,
115+
)
105116

106-
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
117+
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
107118

108-
except asyncio.TimeoutError as e:
109-
error_message = (
110-
f"Failed to connect to WebSocket server within {timeout}s timeout"
111-
)
112-
logger.error(error_message)
113-
await self._cleanup_client()
114-
raise RuntimeError(error_message) from e
119+
except asyncio.TimeoutError as e:
120+
error_message = (
121+
f"Failed to connect to WebSocket server within {timeout}s timeout"
122+
)
123+
logger.error(error_message)
124+
await self._cleanup_client()
125+
raise RuntimeError(error_message) from e
115126

116-
except Exception as e:
117-
error_message = f"Failed to connect to WebSocket server: {e}"
118-
logger.error(error_message)
119-
await self._cleanup_client()
120-
raise RuntimeError(error_message) from e
127+
except Exception as e:
128+
error_message = f"Failed to connect to WebSocket server: {e}"
129+
logger.error(error_message)
130+
await self._cleanup_client()
131+
raise RuntimeError(error_message) from e
121132

122133
async def disconnect(self) -> None:
123134
"""Close the WebSocket connection gracefully.
@@ -150,7 +161,7 @@ async def emit_message_event(
150161
if self._client is None:
151162
raise RuntimeError("WebSocket client not connected. Call connect() first.")
152163

153-
if not self._connected_event.is_set():
164+
if not self._connected_event.is_set() and not self._websocket_disabled:
154165
raise RuntimeError("WebSocket client not in connected state")
155166

156167
try:
@@ -167,7 +178,12 @@ async def emit_message_event(
167178
mode="json", exclude_none=True, by_alias=True
168179
)
169180

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

172188
# Store the current message ID, used for emitting interrupt events.
173189
self._current_message_id = message_event.message_id
@@ -185,7 +201,7 @@ async def emit_exchange_end_event(self) -> None:
185201
if self._client is None:
186202
raise RuntimeError("WebSocket client not connected. Call connect() first.")
187203

188-
if not self._connected_event.is_set():
204+
if not self._connected_event.is_set() and not self._websocket_disabled:
189205
raise RuntimeError("WebSocket client not in connected state")
190206

191207
try:
@@ -201,7 +217,12 @@ async def emit_exchange_end_event(self) -> None:
201217
mode="json", exclude_none=True, by_alias=True
202218
)
203219

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

206227
except Exception as e:
207228
logger.error(f"Error sending conversation event to WebSocket: {e}")
@@ -231,7 +252,12 @@ async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
231252
event_data = interrupt_event.model_dump(
232253
mode="json", exclude_none=True, by_alias=True
233254
)
234-
await self._client.emit("ConversationEvent", event_data)
255+
if self._websocket_disabled:
256+
logger.info(
257+
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
258+
)
259+
else:
260+
await self._client.emit("ConversationEvent", event_data)
235261
except Exception as e:
236262
logger.warning(f"Error sending interrupt event: {e}")
237263

@@ -266,6 +292,14 @@ async def _handle_connect_error(self, data: Any) -> None:
266292
"""Handle connection error event."""
267293
logger.error(f"WebSocket connection error: {data}")
268294

295+
async def _handle_conversation_event(
296+
self, event: dict[str, Any], _sid: str
297+
) -> None:
298+
"""Handle received ConversationEvent events."""
299+
error_event = event.get("conversationError")
300+
if error_event:
301+
logger.error(f"Conversation error: {json.dumps(error_event)}")
302+
269303
async def _cleanup_client(self) -> None:
270304
"""Clean up client resources."""
271305
self._connected_event.clear()
@@ -316,6 +350,13 @@ def get_chat_bridge(
316350
websocket_url = f"wss://{host}?conversationId={context.conversation_id}"
317351
websocket_path = "autopilotforeveryone_/websocket_/socket.io"
318352

353+
if os.environ.get("CAS_WEBSOCKET_HOST"):
354+
websocket_url = f"ws://{os.environ.get('CAS_WEBSOCKET_HOST')}?conversationId={context.conversation_id}"
355+
websocket_path = "/socket.io"
356+
logger.warning(
357+
f"CAS_WEBSOCKET_HOST is set. Using websocket_url '{websocket_url}{websocket_path}'."
358+
)
359+
319360
# Build headers from context
320361
headers = {
321362
"Authorization": f"Bearer {os.environ.get('UIPATH_ACCESS_TOKEN', '')}",

src/uipath/_cli/cli_run.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@
5858
type=click.Path(exists=False),
5959
help="File path where the trace spans will be written (JSON Lines format)",
6060
)
61+
@click.option(
62+
"--state-file",
63+
required=False,
64+
type=click.Path(exists=True),
65+
help="File path where the state file is stored for persisting execution state. If not provided, a temporary file will be used.",
66+
)
6167
@click.option(
6268
"--debug",
6369
is_flag=True,
@@ -69,6 +75,11 @@
6975
default=5678,
7076
help="Port for the debug server (default: 5678)",
7177
)
78+
@click.option(
79+
"--keep-state-file",
80+
is_flag=True,
81+
help="Keep the temporary state file even when not resuming and no job id is provided",
82+
)
7283
def run(
7384
entrypoint: str | None,
7485
input: str | None,
@@ -77,8 +88,10 @@ def run(
7788
input_file: str | None,
7889
output_file: str | None,
7990
trace_file: str | None,
91+
state_file: str | None,
8092
debug: bool,
8193
debug_port: int,
94+
keep_state_file: bool,
8295
) -> None:
8396
"""Execute the project."""
8497
input_file = file or input_file
@@ -95,8 +108,10 @@ def run(
95108
input_file=input_file,
96109
output_file=output_file,
97110
trace_file=trace_file,
111+
state_file=state_file,
98112
debug=debug,
99113
debug_port=debug_port,
114+
keep_state_file=keep_state_file,
100115
)
101116

102117
if result.error_message:
@@ -144,6 +159,8 @@ async def execute() -> None:
144159
input_file=file or input_file,
145160
output_file=output_file,
146161
trace_file=trace_file,
162+
state_file_path=state_file,
163+
keep_state_file=keep_state_file,
147164
resume=resume,
148165
command="run",
149166
trace_manager=trace_manager,

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+
get_chat_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+
"get_chat_system_prompt",
2228
]

0 commit comments

Comments
 (0)