Skip to content

Commit d149ce8

Browse files
chore: test well-known interrupt model for chat
1 parent 283bd1b commit d149ce8

File tree

3 files changed

+372
-326
lines changed

3 files changed

+372
-326
lines changed

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[project]
22
name = "uipath"
3-
version = "2.7.11"
3+
version = "2.7.99"
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 = [
8-
"uipath-core>=0.2.3, <0.3.0",
8+
"uipath-core==0.2.4.dev1000310162",
99
"uipath-runtime>=0.6.2, <0.7.0",
1010
"click>=8.3.1",
1111
"httpx>=0.28.1",
@@ -147,3 +147,6 @@ name = "testpypi"
147147
url = "https://test.pypi.org/simple/"
148148
publish-url = "https://test.pypi.org/legacy/"
149149
explicit = true
150+
151+
[tool.uv.sources]
152+
uipath-core = {index = "testpypi"}

src/uipath/_cli/_chat/_bridge.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Chat bridge implementations for conversational agents."""
22

33
import asyncio
4+
import datetime
45
import json
56
import logging
67
import os
8+
import time
79
import uuid
10+
from datetime import timezone, datetime
811
from typing import Any
912
from urllib.parse import urlparse
1013

@@ -13,8 +16,10 @@
1316
UiPathConversationExchangeEndEvent,
1417
UiPathConversationExchangeEvent,
1518
UiPathConversationInterruptEvent,
16-
UiPathConversationInterruptStartEvent,
19+
UiPathConversationInterruptEndEvent,
1720
UiPathConversationMessageEvent,
21+
UiPathConversationToolCallConfirmationInterruptStart,
22+
UiPathConversationToolCallConfirmationValue
1823
)
1924
from uipath.runtime import UiPathRuntimeResult
2025
from uipath.runtime.chat import UiPathChatProtocol
@@ -56,6 +61,11 @@ def __init__(
5661
self._client: Any | None = None
5762
self._connected_event = asyncio.Event()
5863

64+
# Interrupt handling
65+
self._interrupt_id: str | None = None
66+
self._interrupt_end_event = asyncio.Event()
67+
self._interrupt_end_data: UiPathConversationInterruptEndEvent | None = None
68+
5969
# Set CAS_WEBSOCKET_DISABLED when using the debugger to prevent websocket errors from
6070
# interrupting the debugging session. Events will be logged instead of being sent.
6171
self._websocket_disabled = os.environ.get("CAS_WEBSOCKET_DISABLED") == "true"
@@ -242,7 +252,10 @@ async def emit_exchange_end_event(self) -> None:
242252
async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
243253
if self._client and self._connected_event.is_set():
244254
try:
255+
# Clear previous interrupt state and generate new interrupt_id
245256
self._interrupt_id = str(uuid.uuid4())
257+
self._interrupt_end_event.clear()
258+
self._interrupt_end_data = None
246259

247260
interrupt_event = UiPathConversationEvent(
248261
conversation_id=self.conversation_id,
@@ -252,9 +265,9 @@ async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
252265
message_id=self._current_message_id,
253266
interrupt=UiPathConversationInterruptEvent(
254267
interrupt_id=self._interrupt_id,
255-
start=UiPathConversationInterruptStartEvent(
256-
type="coded-agent-interrupt",
257-
value=runtime_result.output,
268+
start= UiPathConversationToolCallConfirmationInterruptStart(
269+
type="uipath_cas_tool_call_confirmation",
270+
value=UiPathConversationToolCallConfirmationValue(**runtime_result.triggers[0].api_resume.request),
258271
),
259272
),
260273
),
@@ -270,15 +283,27 @@ async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
270283
else:
271284
await self._client.emit("ConversationEvent", event_data)
272285
except Exception as e:
273-
logger.warning(f"Error sending interrupt event: {e}")
286+
logger.error(f"Error sending interrupt event: {e}", exc_info=True)
274287

275288
async def wait_for_resume(self) -> dict[str, Any]:
276289
"""Wait for the interrupt_end event to be received.
277290
278291
Returns:
279292
Resume data from the interrupt end event
280293
"""
281-
return {}
294+
# Wait for the end interrupt event (with timeout)
295+
await asyncio.wait_for(self._interrupt_end_event.wait(), timeout=15 * 10)
296+
297+
if self._interrupt_end_data is None:
298+
logger.error("Interrupt end event was signaled but data is None")
299+
return {"confirmed": False}
300+
301+
# Extract approved status from the end event value
302+
approved = False
303+
if hasattr(self._interrupt_end_data, 'value') and hasattr(self._interrupt_end_data.value, 'approved'):
304+
approved = self._interrupt_end_data.value.approved
305+
306+
return {"confirmed": approved}
282307

283308
@property
284309
def is_connected(self) -> bool:
@@ -310,6 +335,25 @@ async def _handle_conversation_event(
310335
error_event = event.get("conversationError")
311336
if error_event:
312337
logger.error(f"Conversation error: {json.dumps(error_event)}")
338+
return
339+
340+
# Check for endInterrupt events
341+
try:
342+
parsed_event = UiPathConversationEvent(**event)
343+
if (
344+
parsed_event.exchange
345+
and parsed_event.exchange.message
346+
and parsed_event.exchange.message.interrupt
347+
and parsed_event.exchange.message.interrupt.end
348+
):
349+
interrupt = parsed_event.exchange.message.interrupt
350+
# Check if this is the interrupt we're waiting for
351+
if interrupt.interrupt_id == self._interrupt_id:
352+
logger.info(f"Received endInterrupt for interrupt_id: {self._interrupt_id}")
353+
self._interrupt_end_data = interrupt.end
354+
self._interrupt_end_event.set()
355+
except Exception as e:
356+
logger.warning(f"Error parsing conversation event: {e}")
313357

314358
async def _cleanup_client(self) -> None:
315359
"""Clean up client resources."""

0 commit comments

Comments
 (0)