Skip to content

Commit 87a6484

Browse files
propogate errors to cas
1 parent fde17bc commit 87a6484

File tree

2 files changed

+108
-51
lines changed

2 files changed

+108
-51
lines changed

src/uipath/runtime/chat/protocol.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ async def emit_exchange_end_event(self) -> None:
4747
"""Send an exchange end event."""
4848
...
4949

50+
async def emit_exchange_error_event(
51+
self, error_id: str, message: str, details: Any | None = None
52+
) -> None:
53+
"""Emit an exchange error event."""
54+
...
55+
5056
async def wait_for_resume(self) -> dict[str, Any]:
5157
"""Wait for the interrupt_end event to be received."""
5258
...

src/uipath/runtime/chat/runtime.py

Lines changed: 102 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,37 @@
1919
UiPathRuntimeResult,
2020
UiPathRuntimeStatus,
2121
)
22+
from uipath.runtime.errors import UiPathBaseRuntimeError, UiPathErrorContract
2223
from uipath.runtime.schema import UiPathRuntimeSchema
2324

2425
logger = logging.getLogger(__name__)
2526

27+
_DEFAULT_ERROR_ID = "AGENT_RUNTIME_ERROR"
28+
_DEFAULT_ERROR_MESSAGE = "An unexpected error occurred."
29+
30+
31+
def _extract_error_from_exception(e: Exception) -> tuple[str, str]:
32+
"""Extract error_id and user-facing message from an exception."""
33+
if isinstance(e, UiPathBaseRuntimeError):
34+
return _extract_error_from_contract(e.error_info)
35+
return _DEFAULT_ERROR_ID, _DEFAULT_ERROR_MESSAGE
36+
37+
38+
def _extract_error_from_contract(
39+
error: UiPathErrorContract | None,
40+
) -> tuple[str, str]:
41+
"""Extract error_id and user-facing message from an error contract."""
42+
if not error:
43+
return _DEFAULT_ERROR_ID, _DEFAULT_ERROR_MESSAGE
44+
error_id = error.code or _DEFAULT_ERROR_ID
45+
title = error.title or ""
46+
detail = error.detail.split("\n")[0] if error.detail else ""
47+
if title and detail:
48+
error_message = f"{title}. {detail}"
49+
else:
50+
error_message = title or detail or _DEFAULT_ERROR_MESSAGE
51+
return error_id, error_message
52+
2653

2754
class UiPathChatRuntime:
2855
"""Specialized runtime for chat mode that streams message events to a chat bridge."""
@@ -65,62 +92,86 @@ async def stream(
6592
options: UiPathStreamOptions | None = None,
6693
) -> AsyncGenerator[UiPathRuntimeEvent, None]:
6794
"""Stream execution events with chat support."""
68-
await self.chat_bridge.connect()
69-
70-
execution_completed = False
71-
current_input = input
72-
current_options = UiPathStreamOptions(
73-
resume=options.resume if options else False,
74-
breakpoints=options.breakpoints if options else None,
75-
)
76-
77-
while not execution_completed:
78-
async for event in self.delegate.stream(
79-
current_input, options=current_options
80-
):
81-
if isinstance(event, UiPathRuntimeMessageEvent):
82-
if event.payload:
83-
await self.chat_bridge.emit_message_event(event.payload)
84-
85-
if isinstance(event, UiPathRuntimeResult):
86-
runtime_result = event
87-
88-
if (
89-
runtime_result.status == UiPathRuntimeStatus.SUSPENDED
90-
and runtime_result.triggers
91-
):
92-
api_triggers = [
93-
t
94-
for t in runtime_result.triggers
95-
if t.trigger_type == UiPathResumeTriggerType.API
96-
]
97-
98-
if api_triggers:
99-
resume_map: dict[str, Any] = {}
100-
101-
for trigger in api_triggers:
102-
await self.chat_bridge.emit_interrupt_event(trigger)
103-
104-
resume_data = await self.chat_bridge.wait_for_resume()
105-
106-
assert trigger.interrupt_id is not None, (
107-
"Trigger interrupt_id cannot be None"
108-
)
109-
resume_map[trigger.interrupt_id] = resume_data
110-
111-
current_input = resume_map
112-
current_options.resume = True
113-
break
95+
try:
96+
await self.chat_bridge.connect()
97+
98+
execution_completed = False
99+
current_input = input
100+
current_options = UiPathStreamOptions(
101+
resume=options.resume if options else False,
102+
breakpoints=options.breakpoints if options else None,
103+
)
104+
105+
while not execution_completed:
106+
async for event in self.delegate.stream(
107+
current_input, options=current_options
108+
):
109+
if isinstance(event, UiPathRuntimeMessageEvent):
110+
if event.payload:
111+
await self.chat_bridge.emit_message_event(event.payload)
112+
113+
if isinstance(event, UiPathRuntimeResult):
114+
runtime_result = event
115+
116+
if (
117+
runtime_result.status == UiPathRuntimeStatus.SUSPENDED
118+
and runtime_result.triggers
119+
):
120+
api_triggers = [
121+
t
122+
for t in runtime_result.triggers
123+
if t.trigger_type == UiPathResumeTriggerType.API
124+
]
125+
126+
if api_triggers:
127+
resume_map: dict[str, Any] = {}
128+
129+
for trigger in api_triggers:
130+
await self.chat_bridge.emit_interrupt_event(trigger)
131+
132+
resume_data = (await self.chat_bridge.wait_for_resume())
133+
134+
assert trigger.interrupt_id is not None, (
135+
"Trigger interrupt_id cannot be None"
136+
)
137+
resume_map[trigger.interrupt_id] = resume_data
138+
139+
current_input = resume_map
140+
current_options.resume = True
141+
break
142+
else:
143+
# No API triggers - yield result and complete
144+
yield event
145+
execution_completed = True
146+
elif runtime_result.status == UiPathRuntimeStatus.FAULTED:
147+
await self._emit_error_event(
148+
*_extract_error_from_contract(runtime_result.error)
149+
)
150+
yield event
151+
execution_completed = True
114152
else:
115-
# No API triggers - yield result and complete
116153
yield event
117154
execution_completed = True
155+
await self.chat_bridge.emit_exchange_end_event()
118156
else:
119157
yield event
120-
execution_completed = True
121-
await self.chat_bridge.emit_exchange_end_event()
122-
else:
123-
yield event
158+
159+
except Exception as e:
160+
error_id, error_message = _extract_error_from_exception(e)
161+
await self._emit_error_event(error_id, error_message)
162+
raise
163+
164+
async def _emit_error_event(self, error_id: str, message: str) -> None:
165+
"""Emit an exchange error event to the chat bridge."""
166+
try:
167+
await self.chat_bridge.emit_exchange_error_event(
168+
error_id=error_id,
169+
message=message,
170+
)
171+
except Exception:
172+
logger.warning(
173+
"Failed to emit exchange error event", exc_info=True
174+
)
124175

125176
async def get_schema(self) -> UiPathRuntimeSchema:
126177
"""Get schema from the delegate runtime."""

0 commit comments

Comments
 (0)