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
4 changes: 2 additions & 2 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[project]
name = "uipath"
version = "2.13.21"
version = "2.13.22"
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.5.30, <0.6.0",
"uipath-runtime>=0.12.2, <0.13.0",
"uipath-runtime>=0.13.0, <0.14.0",
"uipath-platform>=0.2.14, <0.3.0",
"click>=8.3.1",
"httpx>=0.28.1",
Expand Down
57 changes: 39 additions & 18 deletions packages/uipath/src/uipath/_cli/_chat/_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,14 @@
finally:
await self._cleanup_client()

def _require_client(self) -> Any:
client = self._client
if client is None:
raise RuntimeError("WebSocket client not connected. Call connect() first.")
if not self._connected_event.is_set() and not self._websocket_disabled:
raise RuntimeError("WebSocket client not in connected state")
return client

async def emit_message_event(
self, message_event: UiPathConversationMessageEvent
) -> None:
Expand All @@ -284,11 +292,7 @@
Raises:
RuntimeError: If client is not connected
"""
if self._client is None:
raise RuntimeError("WebSocket client not connected. Call connect() first.")

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

try:
# Wrap message event with conversation/exchange IDs
Expand All @@ -309,15 +313,40 @@
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)
await client.emit("ConversationEvent", event_data)

# Store the current message ID, used for emitting interrupt events.
self._current_message_id = message_event.message_id

except Exception as e:
logger.error(f"Error sending conversation event to WebSocket: {e}")

Check failure on line 322 in packages/uipath/src/uipath/_cli/_chat/_bridge.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "logging.exception()" instead.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-python&issues=AZ_Mt2gysaf3_bqviSmS&open=AZ_Mt2gysaf3_bqviSmS&pullRequest=1843
raise RuntimeError(f"Failed to send conversation event: {e}") from e

async def emit_meta_event(self, meta_event: dict[str, Any]) -> None:
"""Send an exchange-scoped conversation metadata event."""
client = self._require_client()

try:
event = UiPathConversationEvent(
conversation_id=self.conversation_id,
exchange=UiPathConversationExchangeEvent(
exchange_id=self.exchange_id,
meta_event=meta_event,
),
)
event_data = event.model_dump(mode="json", exclude_none=True, by_alias=True)

if self._websocket_disabled:
logger.info(
"SocketIOChatBridge is in debug mode. Not sending event: %s",
json.dumps(event_data),
)
else:
await client.emit("ConversationEvent", event_data)
except Exception as e:
logger.error(f"Error sending conversation event to WebSocket: {e}")
raise RuntimeError(f"Failed to send conversation event: {e}") from e

async def emit_exchange_end_event(self) -> None:
"""Send an exchange end event.

Expand All @@ -331,11 +360,7 @@
logger.info("end_exchange is False; leaving the exchange open.")
return

if self._client is None:
raise RuntimeError("WebSocket client not connected. Call connect() first.")

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

try:
exchange_end_event = UiPathConversationEvent(
Expand All @@ -355,7 +380,7 @@
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)
await client.emit("ConversationEvent", event_data)

except Exception as e:
logger.error(f"Error sending conversation event to WebSocket: {e}")
Expand All @@ -371,11 +396,7 @@
Args:
error: The exception that caused the error.
"""
if self._client is None:
raise RuntimeError("WebSocket client not connected. Call connect() first.")

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

# Extract and map error to CAS-specific error ID and message.
cas_error_id, cas_message = _resolve_cas_error(error)
Expand Down Expand Up @@ -403,7 +424,7 @@
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)
await client.emit("ConversationEvent", event_data)

except Exception as e:
logger.error(f"Error sending exchange error event to WebSocket: {e}")
Expand Down
7 changes: 6 additions & 1 deletion packages/uipath/testcases/langchain-cross/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ uipath-platform = { path = "../../../uipath-platform", editable = true }
# the published uipath-langchain package. Mirrors the uv override used in the
# cross-repo test workflows.
[tool.uv]
override-dependencies = ["uipath", "uipath-core", "uipath-platform"]
override-dependencies = [
"uipath",
"uipath-core",
"uipath-platform",
"uipath-runtime>=0.13.0,<0.14.0",
]
125 changes: 125 additions & 0 deletions packages/uipath/tests/cli/chat/test_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from uipath._cli._chat._bridge import SocketIOChatBridge, get_chat_bridge
from uipath._cli._debug._bridge import SignalRDebugBridge
from uipath.core.chat import UiPathConversationMessageEvent
from uipath.core.triggers import UiPathApiTrigger, UiPathResumeTrigger
from uipath.platform.constants import (
HEADER_INTERNAL_ACCOUNT_ID,
Expand Down Expand Up @@ -362,6 +363,130 @@ async def test_emit_exchange_end_raises_without_client(self) -> None:

assert "not connected" in str(exc_info.value).lower()

@pytest.mark.anyio
async def test_emit_message_event_sends_when_connected(self) -> None:
bridge = SocketIOChatBridge(
websocket_url="wss://test.example.com",
websocket_path="/socket.io",
conversation_id="conv-123",
exchange_id="exch-456",
headers={},
)
bridge._client = AsyncMock()
bridge._connected_event.set()

await bridge.emit_message_event(
UiPathConversationMessageEvent(message_id="msg-123")
)

bridge._client.emit.assert_awaited_once()

@pytest.mark.anyio
async def test_emit_exchange_error_event_sends_when_connected(self) -> None:
bridge = SocketIOChatBridge(
websocket_url="wss://test.example.com",
websocket_path="/socket.io",
conversation_id="conv-123",
exchange_id="exch-456",
headers={},
)
bridge._client = AsyncMock()
bridge._connected_event.set()

await bridge.emit_exchange_error_event(ValueError("failed"))

bridge._client.emit.assert_awaited_once()

@pytest.mark.anyio
async def test_emit_meta_event_sends_exchange_scoped_event(self) -> None:
bridge = SocketIOChatBridge(
websocket_url="wss://test.example.com",
websocket_path="/socket.io",
conversation_id="conv-123",
exchange_id="exch-456",
headers={},
)
bridge._client = AsyncMock()
bridge._connected_event.set()

await bridge.emit_meta_event(
{"workspaceFiles": [{"path": "plan.md", "attachmentKey": "key-1"}]}
)

bridge._client.emit.assert_awaited_once_with(
"ConversationEvent",
{
"conversationId": "conv-123",
"exchange": {
"exchangeId": "exch-456",
"metaEvent": {
"workspaceFiles": [
{"path": "plan.md", "attachmentKey": "key-1"}
]
},
},
},
)

@pytest.mark.anyio
async def test_emit_meta_event_requires_client(self) -> None:
bridge = SocketIOChatBridge(
websocket_url="wss://test.example.com",
websocket_path="/socket.io",
conversation_id="conv-123",
exchange_id="exch-456",
headers={},
)

with pytest.raises(RuntimeError, match="not connected"):
await bridge.emit_meta_event({})

@pytest.mark.anyio
async def test_emit_meta_event_requires_connected_client(self) -> None:
bridge = SocketIOChatBridge(
websocket_url="wss://test.example.com",
websocket_path="/socket.io",
conversation_id="conv-123",
exchange_id="exch-456",
headers={},
)
bridge._client = AsyncMock()

with pytest.raises(RuntimeError, match="not in connected state"):
await bridge.emit_meta_event({})

@pytest.mark.anyio
async def test_emit_meta_event_does_not_send_in_debug_mode(self) -> None:
bridge = SocketIOChatBridge(
websocket_url="wss://test.example.com",
websocket_path="/socket.io",
conversation_id="conv-123",
exchange_id="exch-456",
headers={},
)
bridge._client = AsyncMock()
bridge._websocket_disabled = True

await bridge.emit_meta_event({"workspaceFiles": []})

bridge._client.emit.assert_not_awaited()

@pytest.mark.anyio
async def test_emit_meta_event_wraps_send_error(self) -> None:
bridge = SocketIOChatBridge(
websocket_url="wss://test.example.com",
websocket_path="/socket.io",
conversation_id="conv-123",
exchange_id="exch-456",
headers={},
)
bridge._client = AsyncMock()
bridge._client.emit.side_effect = ValueError("socket failed")
bridge._connected_event.set()

with pytest.raises(RuntimeError, match="Failed to send conversation event"):
await bridge.emit_meta_event({})


class TestSocketIOChatBridgeEndExchange:
"""The bridge owns whether to honor the exchange-end event (CAS-specific)."""
Expand Down
12 changes: 6 additions & 6 deletions packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading