diff --git a/.github/scripts/test_check_version_uniqueness.py b/.github/scripts/test_check_version_uniqueness.py index 94a2b3cc0..af4298af1 100644 --- a/.github/scripts/test_check_version_uniqueness.py +++ b/.github/scripts/test_check_version_uniqueness.py @@ -5,7 +5,6 @@ import urllib.error from unittest import mock -import pytest from check_version_uniqueness import ( get_package_info, diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 1a5c34ab7..387818039 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/chat/__init__.py b/packages/uipath-core/src/uipath/core/chat/__init__.py index 476cb9352..c93a7aeb1 100644 --- a/packages/uipath-core/src/uipath/core/chat/__init__.py +++ b/packages/uipath-core/src/uipath/core/chat/__init__.py @@ -77,20 +77,6 @@ UiPathConversationExchangeEvent, UiPathConversationExchangeStartEvent, ) -from .interrupt import ( - InterruptTypeEnum, - UiPathConversationGenericInterruptEndEvent, - UiPathConversationGenericInterruptStartEvent, - UiPathConversationInterrupt, - UiPathConversationInterruptData, - UiPathConversationInterruptEndEvent, - UiPathConversationInterruptEvent, - UiPathConversationInterruptStartEvent, - UiPathConversationToolCallConfirmationEndValue, - UiPathConversationToolCallConfirmationInterruptEndEvent, - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationToolCallConfirmationValue, -) from .message import ( UiPathConversationMessage, UiPathConversationMessageData, @@ -108,6 +94,8 @@ ) from .tool import ( UiPathConversationToolCall, + UiPathConversationToolCallConfirmation, + UiPathConversationToolCallConfirmationEvent, UiPathConversationToolCallData, UiPathConversationToolCallEndEvent, UiPathConversationToolCallEvent, @@ -141,19 +129,6 @@ "UiPathConversationMessageEvent", "UiPathConversationMessageData", "UiPathConversationMessage", - # Interrupt - "InterruptTypeEnum", - "UiPathConversationInterruptStartEvent", - "UiPathConversationInterruptEndEvent", - "UiPathConversationInterruptEvent", - "UiPathConversationToolCallConfirmationValue", - "UiPathConversationToolCallConfirmationEndValue", - "UiPathConversationToolCallConfirmationInterruptStartEvent", - "UiPathConversationToolCallConfirmationInterruptEndEvent", - "UiPathConversationGenericInterruptStartEvent", - "UiPathConversationGenericInterruptEndEvent", - "UiPathConversationInterruptData", - "UiPathConversationInterrupt", # Content "UiPathConversationContentPartChunkEvent", "UiPathConversationContentPartStartEvent", @@ -178,6 +153,8 @@ # Tool "UiPathConversationToolCallStartEvent", "UiPathConversationToolCallEndEvent", + "UiPathConversationToolCallConfirmation", + "UiPathConversationToolCallConfirmationEvent", "UiPathConversationToolCallEvent", "UiPathConversationToolCallResult", "UiPathConversationToolCallData", diff --git a/packages/uipath-core/src/uipath/core/chat/interrupt.py b/packages/uipath-core/src/uipath/core/chat/interrupt.py deleted file mode 100644 index a2ce3e13f..000000000 --- a/packages/uipath-core/src/uipath/core/chat/interrupt.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Interrupt events for human-in-the-loop patterns.""" - -from enum import Enum -from typing import Any, Literal, Union - -from pydantic import BaseModel, ConfigDict, Field - - -class InterruptTypeEnum(str, Enum): - """Enum of known interrupt types.""" - - TOOL_CALL_CONFIRMATION = "uipath_cas_tool_call_confirmation" - - -class UiPathConversationToolCallConfirmationValue(BaseModel): - """Schema for tool call confirmation interrupt value.""" - - tool_call_id: str = Field(..., alias="toolCallId") - tool_name: str = Field(..., alias="toolName") - input_schema: Any = Field(..., alias="inputSchema") - input_value: Any | None = Field(None, alias="inputValue") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationToolCallConfirmationInterruptStartEvent(BaseModel): - """Tool call confirmation interrupt start event with strong typing.""" - - type: Literal["uipath_cas_tool_call_confirmation"] - value: UiPathConversationToolCallConfirmationValue - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationGenericInterruptStartEvent(BaseModel): - """Generic interrupt start event for custom interrupt types.""" - - type: str - value: Any - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -UiPathConversationInterruptStartEvent = Union[ - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationGenericInterruptStartEvent, -] - - -class UiPathConversationToolCallConfirmationEndValue(BaseModel): - """Schema for tool call confirmation end value.""" - - approved: bool - input: Any | None = None - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationToolCallConfirmationInterruptEndEvent(BaseModel): - """Tool call confirmation interrupt end event with strong typing.""" - - type: Literal["uipath_cas_tool_call_confirmation"] - value: UiPathConversationToolCallConfirmationEndValue - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationGenericInterruptEndEvent(BaseModel): - """Generic interrupt end event for custom interrupt types.""" - - type: str - value: Any - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -UiPathConversationInterruptEndEvent = Union[ - UiPathConversationToolCallConfirmationInterruptEndEvent, - UiPathConversationGenericInterruptEndEvent, -] - - -class UiPathConversationInterruptEvent(BaseModel): - """Encapsulates interrupt-related events within a message.""" - - interrupt_id: str = Field(..., alias="interruptId") - start: UiPathConversationInterruptStartEvent | None = Field( - None, alias="startInterrupt" - ) - end: UiPathConversationInterruptEndEvent | None = Field(None, alias="endInterrupt") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationInterruptData(BaseModel): - """Represents the core data of an interrupt within a message - a pause point where the agent needs external input.""" - - type: str - interrupt_value: Any = Field(..., alias="interruptValue") - end_value: Any | None = Field(None, alias="endValue") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationInterrupt(UiPathConversationInterruptData): - """Represents an interrupt within a message - a pause point where the agent needs external input.""" - - interrupt_id: str = Field(..., alias="interruptId") - created_at: str = Field(..., alias="createdAt") - updated_at: str = Field(..., alias="updatedAt") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/message.py b/packages/uipath-core/src/uipath/core/chat/message.py index 48e79171f..9d6aa248d 100644 --- a/packages/uipath-core/src/uipath/core/chat/message.py +++ b/packages/uipath-core/src/uipath/core/chat/message.py @@ -10,11 +10,6 @@ UiPathConversationContentPartEvent, ) from .error import UiPathConversationErrorEvent -from .interrupt import ( - UiPathConversationInterrupt, - UiPathConversationInterruptData, - UiPathConversationInterruptEvent, -) from .tool import ( UiPathConversationToolCall, UiPathConversationToolCallData, @@ -53,7 +48,6 @@ class UiPathConversationMessageEvent(BaseModel): None, alias="contentPart" ) tool_call: UiPathConversationToolCallEvent | None = Field(None, alias="toolCall") - interrupt: UiPathConversationInterruptEvent | None = None meta_event: dict[str, Any] | None = Field(None, alias="metaEvent") error: UiPathConversationErrorEvent | None = Field(None, alias="messageError") @@ -68,7 +62,6 @@ class UiPathConversationMessageData(BaseModel): ..., alias="contentParts" ) tool_calls: Sequence[UiPathConversationToolCallData] = Field(..., alias="toolCalls") - interrupts: Sequence[UiPathConversationInterruptData] model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -86,6 +79,5 @@ class UiPathConversationMessage(UiPathConversationMessageData): ..., alias="contentParts" ) tool_calls: Sequence[UiPathConversationToolCall] = Field(..., alias="toolCalls") - interrupts: Sequence[UiPathConversationInterrupt] model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/tool.py b/packages/uipath-core/src/uipath/core/chat/tool.py index 9c9e911bd..818f5574f 100644 --- a/packages/uipath-core/src/uipath/core/chat/tool.py +++ b/packages/uipath-core/src/uipath/core/chat/tool.py @@ -25,6 +25,8 @@ class UiPathConversationToolCallStartEvent(BaseModel): timestamp: str | None = None input: dict[str, Any] | None = None metadata: dict[str, Any] | None = Field(None, alias="metaData") + require_confirmation: bool | None = Field(None, alias="requireConfirmation") + input_schema: Any | None = Field(None, alias="inputSchema") model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -41,6 +43,25 @@ class UiPathConversationToolCallEndEvent(BaseModel): model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) +class UiPathConversationToolCallConfirmationEvent(BaseModel): + """Signals a tool call confirmation (approve/reject) from the client.""" + + approved: bool + input: Any | None = None + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationToolCallConfirmation(BaseModel): + """Represents the stored confirmation state on a tool call.""" + + approved: bool + input: Any | None = None + confirmed_at: str | None = Field(None, alias="confirmedAt") + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + class UiPathConversationToolCallEvent(BaseModel): """Encapsulates the data related to a tool call event.""" @@ -49,6 +70,9 @@ class UiPathConversationToolCallEvent(BaseModel): None, alias="startToolCall" ) end: UiPathConversationToolCallEndEvent | None = Field(None, alias="endToolCall") + confirm: UiPathConversationToolCallConfirmationEvent | None = Field( + None, alias="confirmToolCall" + ) meta_event: dict[str, Any] | None = Field(None, alias="metaEvent") error: UiPathConversationErrorEvent | None = Field(None, alias="toolCallError") @@ -61,6 +85,9 @@ class UiPathConversationToolCallData(BaseModel): name: str input: dict[str, Any] | None = None result: UiPathConversationToolCallResult | None = None + require_confirmation: bool | None = Field(None, alias="requireConfirmation") + input_schema: Any | None = Field(None, alias="inputSchema") + confirmation: UiPathConversationToolCallConfirmation | None = None model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index e17802fc7..807653ed8 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/tests/services/test_conversations_service.py b/packages/uipath-platform/tests/services/test_conversations_service.py index 31aa4a653..37e08bdfa 100644 --- a/packages/uipath-platform/tests/services/test_conversations_service.py +++ b/packages/uipath-platform/tests/services/test_conversations_service.py @@ -38,7 +38,6 @@ async def test_retrieve_message( "role": "assistant", "contentParts": [], "toolCalls": [], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, @@ -95,7 +94,6 @@ async def test_retrieve_message_with_content_parts( } ], "toolCalls": [], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, @@ -145,7 +143,6 @@ async def test_retrieve_message_with_tool_calls( "updatedAt": "2024-01-01T00:00:00Z", } ], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index dce3eb8e9..6a299d6ec 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7a7bc20ee..626941f70 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.48" +version = "2.10.49" 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" diff --git a/packages/uipath/src/uipath/_cli/_chat/_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_bridge.py index 24c1be024..2a382a59e 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_bridge.py @@ -4,7 +4,6 @@ import json import logging import os -import uuid from typing import Any from urllib.parse import urlparse @@ -14,11 +13,8 @@ UiPathConversationEvent, UiPathConversationExchangeEndEvent, UiPathConversationExchangeEvent, - UiPathConversationInterruptEndEvent, - UiPathConversationInterruptEvent, UiPathConversationMessageEvent, - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationToolCallConfirmationValue, + UiPathConversationToolCallConfirmationEvent, ) from uipath.core.triggers import UiPathResumeTrigger from uipath.runtime.chat import UiPathChatProtocol @@ -126,9 +122,10 @@ 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._tool_confirmation_event = asyncio.Event() + self._tool_confirmation_value: ( + UiPathConversationToolCallConfirmationEvent | None + ) = None self._current_message_id: str | None = None # Set CAS_WEBSOCKET_DISABLED when using the debugger to prevent websocket errors from @@ -363,67 +360,35 @@ async def emit_exchange_error_event(self, error: Exception) -> None: raise RuntimeError(f"Failed to send exchange error event: {e}") from e 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()) - - # Ensure we have a valid message_id - if self._current_message_id is None: - raise RuntimeError( - "Cannot emit interrupt event: no current message_id set" - ) - - # Ensure api_resume is not None - if resume_trigger.api_resume is None: - raise RuntimeError( - "Cannot emit interrupt event: api_resume is None" - ) - - interrupt_event = UiPathConversationEvent( - conversation_id=self.conversation_id, - exchange=UiPathConversationExchangeEvent( - exchange_id=self.exchange_id, - message=UiPathConversationMessageEvent( - message_id=self._current_message_id, - interrupt=UiPathConversationInterruptEvent( - interrupt_id=self._interrupt_id, - 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 - ) - if self._websocket_disabled: - logger.info( - f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}" - ) - else: - await self._client.emit("ConversationEvent", event_data) - except Exception as e: - logger.warning(f"Error sending interrupt event: {e}") + """No-op. + + Tool confirmation — the only interrupt pattern CAS uses today — is + handled end-to-end via ``startToolCall`` with ``requireConfirmation: + true`` paired with ``wait_for_resume()``. This is deliberately + simpler than the old interrupt-based flow: CAS needs + ``requireConfirmation`` on the tool call event itself to render the + confirmation UI, so a parallel ``startInterrupt`` event would be + redundant. + + The only hypothetical reason to put work here is a generic, + non-tool-call agent interrupt (e.g. a coded agent calling + ``interrupt("do you want to continue?")``). Nothing uses that today + and it's not a near-term requirement — the method is kept for + generic flexibility. + """ + return None async def wait_for_resume(self) -> dict[str, Any]: - """Wait for the interrupt_end event to be received. - - Returns: - Resume data from the interrupt end event - """ - self._interrupt_end_event.clear() - self._interrupt_end_value = None + """Wait for a confirmToolCall event to be received.""" + self._tool_confirmation_event.clear() + self._tool_confirmation_value = None - await self._interrupt_end_event.wait() + await self._tool_confirmation_event.wait() - if self._interrupt_end_value: - return self._interrupt_end_value.model_dump(mode="python", by_alias=False) + if self._tool_confirmation_value: + return self._tool_confirmation_value.model_dump( + mode="python", by_alias=False + ) return {} @property @@ -458,17 +423,14 @@ async def _handle_conversation_event( if ( parsed_event.exchange and parsed_event.exchange.message - and parsed_event.exchange.message.interrupt - and parsed_event.exchange.message.interrupt.end + and (tool_call := parsed_event.exchange.message.tool_call) + and (confirm := tool_call.confirm) ): - 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() + logger.info( + f"Received confirmToolCall for tool_call_id: {tool_call.tool_call_id}, approved: {confirm.approved}" + ) + self._tool_confirmation_value = confirm + self._tool_confirmation_event.set() except Exception as e: logger.warning(f"Error parsing conversation event: {e}") diff --git a/packages/uipath/src/uipath/eval/models/_conversational_utils.py b/packages/uipath/src/uipath/eval/models/_conversational_utils.py index d4dbf0cdd..9e3523acc 100644 --- a/packages/uipath/src/uipath/eval/models/_conversational_utils.py +++ b/packages/uipath/src/uipath/eval/models/_conversational_utils.py @@ -168,7 +168,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="user", content_parts=content_parts, tool_calls=[], - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -215,7 +214,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="assistant", content_parts=content_parts, tool_calls=tool_calls, - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -259,7 +257,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="user", content_parts=content_parts, tool_calls=[], - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -301,7 +298,6 @@ def legacy_conversational_eval_output_to_uipath_message_data_list( role="assistant", content_parts=content_parts, tool_calls=tool_calls, - interrupts=[], ) ) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 93c875332..c8ab797c3 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.48" +version = "2.10.49" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" },