Skip to content

Commit c9fdba4

Browse files
fix: propogate errors to cas [JAR-9409, JAR-9415] (#1421)
1 parent 10bc126 commit c9fdba4

File tree

3 files changed

+123
-7
lines changed

3 files changed

+123
-7
lines changed

packages/uipath/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.10.30"
3+
version = "2.10.31"
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.5.2, <0.6.0",
9-
"uipath-runtime>=0.9.1, <0.10.0",
9+
"uipath-runtime>=0.10.0, <0.11.0",
1010
"uipath-platform>=0.1.4, <0.2.0",
1111
"click>=8.3.1",
1212
"httpx>=0.28.1",

packages/uipath/src/uipath/_cli/_chat/_bridge.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from urllib.parse import urlparse
1010

1111
from uipath.core.chat import (
12+
UiPathConversationErrorEvent,
13+
UiPathConversationErrorStartEvent,
1214
UiPathConversationEvent,
1315
UiPathConversationExchangeEndEvent,
1416
UiPathConversationExchangeEvent,
@@ -25,6 +27,72 @@
2527
logger = logging.getLogger(__name__)
2628

2729

30+
class CASErrorId:
31+
"""Error IDs for the Conversational Agent Service (CAS), matching the Temporal backend."""
32+
33+
LICENSING = "AGENT_LICENSING_CONSUMPTION_VALIDATION_FAILED"
34+
INCOMPLETE_RESPONSE = "AGENT_RESPONSE_IS_INCOMPLETE"
35+
MAX_STEPS_REACHED = "AGENT_MAXIMUM_SEQUENTIAL_STEPS_REACHED"
36+
INVALID_INPUT = "AGENT_INVALID_INPUT"
37+
DEFAULT_ERROR = "AGENT_RUNTIME_ERROR"
38+
39+
40+
# User-facing messages for each CAS error ID, matching the Temporal backend.
41+
_CAS_ERROR_MESSAGES: dict[str, str] = {
42+
CASErrorId.LICENSING: "Your action could not be completed. You've used all your units for this period. Please contact your administrator to add more units or wait until your allowance replenishes, then try again.",
43+
CASErrorId.INCOMPLETE_RESPONSE: "Could not obtain a full response from the model through streamed completion call.",
44+
CASErrorId.MAX_STEPS_REACHED: "Maximum number of sequential steps reached. You may send a new message to tell the agent to continue.",
45+
CASErrorId.DEFAULT_ERROR: "An unexpected error has occurred.",
46+
}
47+
48+
# Error code suffix mappings to CAS error IDs.
49+
_CAS_ERROR_ID_MAP: dict[str, str] = {
50+
"LICENSE_NOT_AVAILABLE": CASErrorId.LICENSING,
51+
"UNSUCCESSFUL_STOP_REASON": CASErrorId.INCOMPLETE_RESPONSE,
52+
"TERMINATION_MAX_ITERATIONS": CASErrorId.MAX_STEPS_REACHED,
53+
"INVALID_INPUT_FILE_EXTENSION": CASErrorId.INVALID_INPUT,
54+
"MISSING_INPUT_FILE": CASErrorId.INVALID_INPUT,
55+
"INPUT_INVALID_JSON": CASErrorId.INVALID_INPUT,
56+
}
57+
58+
59+
def _extract_error_info(error: Exception) -> tuple[str, str]:
60+
"""Extract an error code and a user-facing message from an exception.
61+
62+
For UiPathBaseRuntimeError (structured errors), extracts code and builds
63+
a message from title + detail. For other exceptions, returns defaults.
64+
"""
65+
from uipath.runtime.errors import UiPathBaseRuntimeError
66+
67+
if isinstance(error, UiPathBaseRuntimeError):
68+
info = error.error_info
69+
code = info.code or CASErrorId.DEFAULT_ERROR
70+
title = info.title or ""
71+
detail = info.detail.split("\n")[0] if info.detail else ""
72+
if title and detail:
73+
message = f"{title}. {detail}"
74+
else:
75+
message = title or detail or _CAS_ERROR_MESSAGES[CASErrorId.DEFAULT_ERROR]
76+
return code, message
77+
78+
return CASErrorId.DEFAULT_ERROR, _CAS_ERROR_MESSAGES[CASErrorId.DEFAULT_ERROR]
79+
80+
81+
def _resolve_cas_error(error: Exception) -> tuple[str, str]:
82+
"""Map an exception to a CAS error ID and user-facing message.
83+
84+
Extracts the error code from the exception, then checks the code suffix
85+
against known mappings. For recognized errors, uses a hardcoded message
86+
matching the Temporal backend. For unrecognized errors, passes through
87+
the extracted message.
88+
"""
89+
error_code, error_message = _extract_error_info(error)
90+
suffix = error_code.rsplit(".", 1)[-1] if error_code else ""
91+
cas_error_id = _CAS_ERROR_ID_MAP.get(suffix, CASErrorId.DEFAULT_ERROR)
92+
cas_message = _CAS_ERROR_MESSAGES.get(cas_error_id) or error_message
93+
return cas_error_id, cas_message
94+
95+
2896
class SocketIOChatBridge:
2997
"""WebSocket-based chat bridge for streaming conversational events to CAS.
3098
@@ -246,6 +314,54 @@ async def emit_exchange_end_event(self) -> None:
246314
logger.error(f"Error sending conversation event to WebSocket: {e}")
247315
raise RuntimeError(f"Failed to send conversation event: {e}") from e
248316

317+
async def emit_exchange_error_event(self, error: Exception) -> None:
318+
"""Send an exchange error event to signal an error to the UI.
319+
320+
Extracts error information from the exception and maps it to
321+
CAS-specific error IDs and messages matching the Temporal backend
322+
for frontend consistency.
323+
324+
Args:
325+
error: The exception that caused the error.
326+
"""
327+
if self._client is None:
328+
raise RuntimeError("WebSocket client not connected. Call connect() first.")
329+
330+
if not self._connected_event.is_set() and not self._websocket_disabled:
331+
raise RuntimeError("WebSocket client not in connected state")
332+
333+
# Extract and map error to CAS-specific error ID and message.
334+
cas_error_id, cas_message = _resolve_cas_error(error)
335+
336+
try:
337+
exchange_error_event = UiPathConversationEvent(
338+
conversation_id=self.conversation_id,
339+
exchange=UiPathConversationExchangeEvent(
340+
exchange_id=self.exchange_id,
341+
error=UiPathConversationErrorEvent(
342+
error_id=cas_error_id,
343+
start=UiPathConversationErrorStartEvent(
344+
message=cas_message,
345+
),
346+
),
347+
),
348+
)
349+
350+
event_data = exchange_error_event.model_dump(
351+
mode="json", exclude_none=True, by_alias=True
352+
)
353+
354+
if self._websocket_disabled:
355+
logger.info(
356+
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
357+
)
358+
else:
359+
await self._client.emit("ConversationEvent", event_data)
360+
361+
except Exception as e:
362+
logger.error(f"Error sending exchange error event to WebSocket: {e}")
363+
raise RuntimeError(f"Failed to send exchange error event: {e}") from e
364+
249365
async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger):
250366
if self._client and self._connected_event.is_set():
251367
try:

packages/uipath/uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)