Skip to content
Open
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
90 changes: 61 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 @@ -57,6 +58,10 @@ def __init__(
self._client: AsyncClient | 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 @@ -89,34 +94,39 @@ async def connect(self, timeout: float = 10.0) -> None:

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 @@ -149,7 +159,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 @@ -166,7 +176,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 @@ -184,7 +199,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 @@ -200,7 +215,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 @@ -230,7 +250,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 @@ -315,6 +340,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
9 changes: 9 additions & 0 deletions src/uipath/agent/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,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,
generate_conversational_agent_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",
"generate_conversational_agent_system_prompt",
]
Loading