diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 63c4777a2..e891545da 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.18" +version = "2.11.19" 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/_voice_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py index b575404b3..b19394131 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py @@ -6,6 +6,7 @@ import logging import os from collections.abc import Awaitable, Callable +from copy import deepcopy from enum import Enum from typing import Any from urllib.parse import urlparse @@ -82,6 +83,12 @@ def __init__( self._done = asyncio.Event() self._in_flight: set[asyncio.Task[None]] = set() self._end_reason: VoiceSessionEndReason | None = None + self._end_detail: dict[str, Any] = {} + + @property + def end_detail(self) -> dict[str, Any]: + """CAS payload from voice_session_ended, preserved for the job runtime.""" + return deepcopy(self._end_detail) async def run(self) -> VoiceSessionEndReason: """Connect, dispatch tool calls until session ends, then disconnect. @@ -214,8 +221,19 @@ async def _execute_tool_call(self, call: UiPathVoiceToolCallRequest) -> None: tool_result.is_error, ) - async def _handle_session_ended(self, _data: Any, *_: Any) -> None: - logger.info("[Voice] voice_session_ended received") + async def _handle_session_ended(self, data: Any = None, *_: Any) -> None: + if self._done.is_set(): + return + + detail = deepcopy(data) if isinstance(data, dict) else {} + self._end_detail = detail + logger.info( + "[Voice] voice_session_ended received " + "(endedBy=%s, callEnded=%s, reason=%s)", + detail.get("endedBy"), + detail.get("callEnded"), + detail.get("reason"), + ) self._end_session(VoiceSessionEndReason.COMPLETED) diff --git a/packages/uipath/tests/cli/chat/test_voice_bridge.py b/packages/uipath/tests/cli/chat/test_voice_bridge.py index a9c8c1baf..dda7acd31 100644 --- a/packages/uipath/tests/cli/chat/test_voice_bridge.py +++ b/packages/uipath/tests/cli/chat/test_voice_bridge.py @@ -46,6 +46,47 @@ async def test_session_ended_sets_completed(self) -> None: await session._handle_session_ended(None) assert session._end_reason == VoiceSessionEndReason.COMPLETED + async def test_session_ended_preserves_payload_opaquely(self) -> None: + session = _make_session() + payload = { + "callContext": { + "type": "phone", + "id": "CA123", + "conversationId": "conv-1", + }, + "endedBy": "agent", + "callEnded": False, + "reason": "agent_completed", + "someFutureKey": {"nested": True}, + } + + await session._handle_session_ended(payload) + + assert session.end_detail == payload + assert session._end_reason == VoiceSessionEndReason.COMPLETED + returned_detail = session.end_detail + returned_detail["reason"] = "mutated" + returned_detail["callContext"]["id"] = "CA999" + payload["endedBy"] = "system" + assert session.end_detail["reason"] == "agent_completed" + assert session.end_detail["callContext"]["id"] == "CA123" + assert session.end_detail["endedBy"] == "agent" + + async def test_session_ended_non_dict_payload_is_empty_detail(self) -> None: + session = _make_session() + await session._handle_session_ended("not-a-dict") + assert session.end_detail == {} + assert session._end_reason == VoiceSessionEndReason.COMPLETED + + async def test_late_session_ended_does_not_overwrite_terminal_state(self) -> None: + session = _make_session() + await session._handle_session_ended({"endedBy": "agent", "callEnded": False}) + + await session._handle_session_ended({"endedBy": "system", "callEnded": True}) + + assert session.end_detail == {"endedBy": "agent", "callEnded": False} + assert session._end_reason == VoiceSessionEndReason.COMPLETED + async def test_disconnect_sets_disconnected(self) -> None: session = _make_session() await session._handle_disconnect() diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 0c265e9d0..5fef843c9 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.18" +version = "2.11.19" source = { editable = "." } dependencies = [ { name = "applicationinsights" },