Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
22 changes: 20 additions & 2 deletions packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Comment on lines +225 to +229
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)


Expand Down
41 changes: 41 additions & 0 deletions packages/uipath/tests/cli/chat/test_voice_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading