Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[project]
name = "uipath"
version = "2.4.24"
version = "2.5"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.1.4, <0.2.0",
"uipath-runtime>=0.4.1, <0.5.0",
"uipath-runtime>=0.5, <0.6.0",
"click>=8.3.1",
"httpx>=0.28.1",
"pyjwt>=2.10.1",
Expand Down
99 changes: 70 additions & 29 deletions src/uipath/_cli/_chat/_bridge.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Chat bridge implementations for conversational agents."""

import asyncio
import json
import logging
import os
import uuid
Expand Down Expand Up @@ -55,6 +56,10 @@ def __init__(
self._client: Any | None = None
self._connected_event = asyncio.Event()

# Set CAS_WEBSOCKET_DISABLED when using the debugger to prevent websocket errors from
# interrupting the debugging session. Events will be logged instead of being sent.
self._websocket_disabled = os.environ.get("CAS_WEBSOCKET_DISABLED") == "true"

async def connect(self, timeout: float = 10.0) -> None:
"""Establish WebSocket connection to the server.

Expand Down Expand Up @@ -87,37 +92,43 @@ async def connect(self, timeout: float = 10.0) -> None:
self._client.on("connect", self._handle_connect)
self._client.on("disconnect", self._handle_disconnect)
self._client.on("connect_error", self._handle_connect_error)
self._client.on("ConversationEvent", self._handle_conversation_event)

self._connected_event.clear()

try:
# Attempt to connect with timeout
await asyncio.wait_for(
self._client.connect(
url=self.websocket_url,
socketio_path=self.websocket_path,
headers=self.headers,
auth=self.auth,
transports=["websocket"],
),
timeout=timeout,
if self._websocket_disabled:
logger.warning(
"SocketIOChatBridge is in debug mode. Not connecting websocket."
)
else:
try:
# Attempt to connect with timeout
await asyncio.wait_for(
self._client.connect(
url=self.websocket_url,
socketio_path=self.websocket_path,
headers=self.headers,
auth=self.auth,
transports=["websocket"],
),
timeout=timeout,
)

await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)

except asyncio.TimeoutError as e:
error_message = (
f"Failed to connect to WebSocket server within {timeout}s timeout"
)
logger.error(error_message)
await self._cleanup_client()
raise RuntimeError(error_message) from e
except asyncio.TimeoutError as e:
error_message = (
f"Failed to connect to WebSocket server within {timeout}s timeout"
)
logger.error(error_message)
await self._cleanup_client()
raise RuntimeError(error_message) from e

except Exception as e:
error_message = f"Failed to connect to WebSocket server: {e}"
logger.error(error_message)
await self._cleanup_client()
raise RuntimeError(error_message) from e
except Exception as e:
error_message = f"Failed to connect to WebSocket server: {e}"
logger.error(error_message)
await self._cleanup_client()
raise RuntimeError(error_message) from e

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

if not self._connected_event.is_set():
if not self._connected_event.is_set() and not self._websocket_disabled:
raise RuntimeError("WebSocket client not in connected state")

try:
Expand All @@ -167,7 +178,12 @@ async def emit_message_event(
mode="json", exclude_none=True, by_alias=True
)

await self._client.emit("ConversationEvent", event_data)
if self._websocket_disabled:
logger.info(
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)

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

if not self._connected_event.is_set():
if not self._connected_event.is_set() and not self._websocket_disabled:
raise RuntimeError("WebSocket client not in connected state")

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

await self._client.emit("ConversationEvent", event_data)
if self._websocket_disabled:
logger.info(
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)

except Exception as e:
logger.error(f"Error sending conversation event to WebSocket: {e}")
Expand Down Expand Up @@ -231,7 +252,12 @@ async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
event_data = interrupt_event.model_dump(
mode="json", exclude_none=True, by_alias=True
)
await self._client.emit("ConversationEvent", event_data)
if self._websocket_disabled:
logger.info(
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)
except Exception as e:
logger.warning(f"Error sending interrupt event: {e}")

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

async def _handle_conversation_event(
self, event: dict[str, Any], _sid: str
) -> None:
"""Handle received ConversationEvent events."""
error_event = event.get("conversationError")
if error_event:
logger.error(f"Conversation error: {json.dumps(error_event)}")

async def _cleanup_client(self) -> None:
"""Clean up client resources."""
self._connected_event.clear()
Expand Down Expand Up @@ -316,6 +350,13 @@ def get_chat_bridge(
websocket_url = f"wss://{host}?conversationId={context.conversation_id}"
websocket_path = "autopilotforeveryone_/websocket_/socket.io"

if os.environ.get("CAS_WEBSOCKET_HOST"):
websocket_url = f"ws://{os.environ.get('CAS_WEBSOCKET_HOST')}?conversationId={context.conversation_id}"
websocket_path = "/socket.io"
logger.warning(
f"CAS_WEBSOCKET_HOST is set. Using websocket_url '{websocket_url}{websocket_path}'."
)

# Build headers from context
headers = {
"Authorization": f"Bearer {os.environ.get('UIPATH_ACCESS_TOKEN', '')}",
Expand Down
17 changes: 17 additions & 0 deletions src/uipath/_cli/cli_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
type=click.Path(exists=False),
help="File path where the trace spans will be written (JSON Lines format)",
)
@click.option(
"--state-file",
required=False,
type=click.Path(exists=True),
help="File path where the state file is stored for persisting execution state. If not provided, a temporary file will be used.",
)
@click.option(
"--debug",
is_flag=True,
Expand All @@ -69,6 +75,11 @@
default=5678,
help="Port for the debug server (default: 5678)",
)
@click.option(
"--keep-state-file",
is_flag=True,
help="Keep the temporary state file even when not resuming and no job id is provided",
)
def run(
entrypoint: str | None,
input: str | None,
Expand All @@ -77,8 +88,10 @@ def run(
input_file: str | None,
output_file: str | None,
trace_file: str | None,
state_file: str | None,
debug: bool,
debug_port: int,
keep_state_file: bool,
) -> None:
"""Execute the project."""
input_file = file or input_file
Expand All @@ -95,8 +108,10 @@ def run(
input_file=input_file,
output_file=output_file,
trace_file=trace_file,
state_file=state_file,
debug=debug,
debug_port=debug_port,
keep_state_file=keep_state_file,
)

if result.error_message:
Expand Down Expand Up @@ -144,6 +159,8 @@ async def execute() -> None:
input_file=file or input_file,
output_file=output_file,
trace_file=trace_file,
state_file_path=state_file,
keep_state_file=keep_state_file,
resume=resume,
command="run",
trace_manager=trace_manager,
Expand Down
9 changes: 9 additions & 0 deletions src/uipath/agent/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,15 @@ class AgentDefinition(BaseModel):
validate_by_name=True, validate_by_alias=True, extra="allow"
)

@property
def is_conversational(self) -> bool:
"""Checks the settings.engine property to determine if the agent is conversational."""
if hasattr(self, "metadata") and self.metadata:
metadata = self.metadata
if hasattr(metadata, "is_conversational"):
return metadata.is_conversational
return False

@staticmethod
def _normalize_guardrails(v: Dict[str, Any]) -> None:
guards = v.get("guardrails")
Expand Down
6 changes: 6 additions & 0 deletions src/uipath/agent/react/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
This module includes UiPath ReAct Agent Loop constructs such as prompts, tools
"""

from .conversational_prompts import (
PromptUserSettings,
get_chat_system_prompt,
)
from .prompts import AGENT_SYSTEM_PROMPT_TEMPLATE
from .tools import (
END_EXECUTION_TOOL,
Expand All @@ -19,4 +23,6 @@
"RAISE_ERROR_TOOL",
"EndExecutionToolSchemaModel",
"RaiseErrorToolSchemaModel",
"PromptUserSettings",
"get_chat_system_prompt",
]
Loading
Loading