Skip to content
Draft
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
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description = "Python SDK and CLI for UiPath Platform, enabling programmatic int
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.2.3, <0.3.0",
"uipath-runtime>=0.6.2, <0.7.0",
"uipath-core>=0.3.0, <0.4.0",
"uipath-runtime==0.7.0.dev1000780259",
"click>=8.3.1",
"httpx>=0.28.1",
"pyjwt>=2.10.1",
Expand Down Expand Up @@ -147,3 +147,6 @@ name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath-runtime = { index = "testpypi" }
55 changes: 43 additions & 12 deletions src/uipath/_cli/_chat/_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
UiPathConversationEvent,
UiPathConversationExchangeEndEvent,
UiPathConversationExchangeEvent,
UiPathConversationInterruptEndEvent,
UiPathConversationInterruptEvent,
UiPathConversationInterruptStartEvent,
UiPathConversationMessageEvent,
UiPathConversationToolCallConfirmationInterruptStartEvent,
UiPathConversationToolCallConfirmationValue,
)
from uipath.runtime import UiPathRuntimeResult
from uipath.runtime import UiPathResumeTrigger
from uipath.runtime.chat import UiPathChatProtocol
from uipath.runtime.context import UiPathRuntimeContext

Expand Down Expand Up @@ -56,6 +58,11 @@ def __init__(
self._client: Any | None = None
self._connected_event = asyncio.Event()

# Interrupt state for HITL round-trip
self._interrupt_end_event = asyncio.Event()
self._interrupt_end_value: UiPathConversationInterruptEndEvent | None = None
self._current_message_id: str | None = None

# 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"
Expand Down Expand Up @@ -239,9 +246,10 @@ async def emit_exchange_end_event(self) -> None:
logger.error(f"Error sending conversation event to WebSocket: {e}")
raise RuntimeError(f"Failed to send conversation event: {e}") from e

async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger):
if self._client and self._connected_event.is_set():
try:
# Clear previous interrupt state and generate new interrupt_id
self._interrupt_id = str(uuid.uuid4())

interrupt_event = UiPathConversationEvent(
Expand All @@ -252,14 +260,15 @@ async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
message_id=self._current_message_id,
interrupt=UiPathConversationInterruptEvent(
interrupt_id=self._interrupt_id,
start=UiPathConversationInterruptStartEvent(
type="coded-agent-interrupt",
value=runtime_result.output,
start=UiPathConversationToolCallConfirmationInterruptStartEvent(
type="uipath_cas_tool_call_confirmation",
value=UiPathConversationToolCallConfirmationValue(**resume_trigger.api_resume.request),
),
),
),
),
),
)
)

event_data = interrupt_event.model_dump(
mode="json", exclude_none=True, by_alias=True
)
Expand All @@ -278,6 +287,13 @@ async def wait_for_resume(self) -> dict[str, Any]:
Returns:
Resume data from the interrupt end event
"""
self._interrupt_end_event.clear()
self._interrupt_end_value = None

await self._interrupt_end_event.wait()

if self._interrupt_end_value:
return self._interrupt_end_value.model_dump(mode="python", by_alias=False)
return {}

@property
Expand Down Expand Up @@ -306,10 +322,25 @@ async def _handle_connect_error(self, data: Any) -> None:
async def _handle_conversation_event(
self, event: dict[str, Any], _sid: str
) -> None:
"""Handle received ConversationEvent events."""
error_event = event.get("conversationError")
if error_event:
logger.error(f"Conversation error: {json.dumps(error_event)}")

try:
parsed_event = UiPathConversationEvent(**event)
if (
parsed_event.exchange
and parsed_event.exchange.message
and parsed_event.exchange.message.interrupt
and parsed_event.exchange.message.interrupt.end
):
interrupt = parsed_event.exchange.message.interrupt

if interrupt.interrupt_id == self._interrupt_id:
logger.info(
f"Received endInterrupt for interrupt_id: {self._interrupt_id}"
)
self._interrupt_end_value = interrupt.end
self._interrupt_end_event.set()
except Exception as e:
logger.warning(f"Error parsing conversation event: {e}")

async def _cleanup_client(self) -> None:
"""Clean up client resources."""
Expand Down
Loading
Loading