Skip to content
Open
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
4 changes: 2 additions & 2 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[project]
name = "uipath"
version = "2.10.81"
version = "2.10.82"
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"
dependencies = [
"uipath-core>=0.5.17, <0.6.0",
"uipath-runtime>=0.11.0, <0.12.0",
"uipath-runtime>=0.12.0, <0.13.0",
"uipath-platform>=0.1.63, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
Expand Down
4 changes: 3 additions & 1 deletion packages/uipath/src/uipath/_cli/cli_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ async def execute_debug_runtime():
context=ctx
)
chat_runtime = UiPathChatRuntime(
delegate=delegate, chat_bridge=chat_bridge
delegate=delegate,
chat_bridge=chat_bridge,
end_exchange=ctx.end_exchange,
)
delegate = chat_runtime

Expand Down
4 changes: 3 additions & 1 deletion packages/uipath/src/uipath/_cli/cli_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,9 @@ async def execute() -> None:
context=ctx
)
chat_runtime = UiPathChatRuntime(
delegate=runtime, chat_bridge=chat_bridge
delegate=runtime,
chat_bridge=chat_bridge,
end_exchange=ctx.end_exchange,
)

ctx.result = await execute_runtime(
Expand Down
94 changes: 94 additions & 0 deletions packages/uipath/tests/cli/test_debug_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# type: ignore
"""Tests for chat runtime wiring in the debug command."""

import json
from unittest.mock import AsyncMock, Mock, patch

from click.testing import CliRunner

from uipath._cli import cli
from uipath._cli.middlewares import MiddlewareResult
from uipath.runtime import UiPathRuntimeResult, UiPathRuntimeStatus


class TestDebugConversationalExchangeEnd:
"""Debug runs pass the endExchange fps property to UiPathChatRuntime."""

def _invoke_debug(self, runner: CliRunner, chat_runtime_cls):
mock_runtime = Mock()
mock_runtime.dispose = AsyncMock()
mock_runtime.get_schema = AsyncMock(return_value=Mock(metadata=None))

mock_factory = Mock()
mock_factory.new_runtime = AsyncMock(return_value=mock_runtime)
mock_factory.get_settings = AsyncMock(return_value=Mock(trace_settings=None))
mock_factory.dispose = AsyncMock()

mock_debug_runtime = Mock()
mock_debug_runtime.dispose = AsyncMock()

mock_mock_runtime = Mock()
mock_mock_runtime.execute = AsyncMock(
return_value=UiPathRuntimeResult(status=UiPathRuntimeStatus.SUCCESSFUL)
)
mock_mock_runtime.dispose = AsyncMock()

with (
patch(
"uipath._cli.cli_debug.Middlewares.next",
return_value=MiddlewareResult(
should_continue=True,
error_message=None,
should_include_stacktrace=False,
),
),
patch(
"uipath._cli.cli_debug.UiPathRuntimeFactoryRegistry.get",
return_value=mock_factory,
),
patch("uipath._cli.cli_debug.get_debug_bridge"),
patch("uipath._cli.cli_debug.get_chat_bridge"),
patch(
"uipath._cli.cli_debug.UiPathDebugRuntime",
return_value=mock_debug_runtime,
),
patch(
"uipath._cli.cli_debug.UiPathMockRuntime",
return_value=mock_mock_runtime,
),
):
return runner.invoke(cli, ["debug", "main", "{}"])

def test_end_exchange_false_passed_to_chat_runtime(
self, runner: CliRunner, temp_dir: str, monkeypatch
):
"""endExchange=false in fpsProperties reaches the UiPathChatRuntime constructor."""
monkeypatch.setenv("UIPATH_TRACING_ENABLED", "false")
monkeypatch.delenv("UIPATH_CONFIG_PATH", raising=False)

with runner.isolated_filesystem(temp_dir=temp_dir):
with open("uipath.json", "w") as f:
json.dump(
{
"fpsProperties": {
"conversationalService.conversationId": "conv-1",
"conversationalService.exchangeId": "ex-1",
"conversationalService.endExchange": False,
}
},
f,
)

mock_chat_runtime = Mock()
mock_chat_runtime.dispose = AsyncMock()
with patch(
"uipath._cli.cli_debug.UiPathChatRuntime",
return_value=mock_chat_runtime,
) as chat_runtime_cls:
result = self._invoke_debug(runner, chat_runtime_cls)

assert result.exit_code == 0, (
f"output: {result.output!r}, exception: {result.exception}"
)
assert chat_runtime_cls.called
assert chat_runtime_cls.call_args.kwargs.get("end_exchange") is False
102 changes: 102 additions & 0 deletions packages/uipath/tests/cli/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from uipath._cli import cli
from uipath._cli.middlewares import MiddlewareResult
from uipath.runtime import UiPathRuntimeResult, UiPathRuntimeStatus


def _middleware_continue():
Expand Down Expand Up @@ -559,3 +560,104 @@ def test_simulation_disabled_does_not_wrap_runtime(
)

assert not mock_cls.called


def _make_chat_bridge_mock():
"""Create a chat bridge mock with all methods that UiPathChatRuntime uses."""
bridge = Mock()
bridge.connect = AsyncMock()
bridge.disconnect = AsyncMock()
bridge.emit_message_event = AsyncMock()
bridge.emit_interrupt_event = AsyncMock()
bridge.emit_executing_tool_call_event = AsyncMock()
bridge.emit_exchange_end_event = AsyncMock()
bridge.emit_exchange_error_event = AsyncMock()
bridge.wait_for_resume = AsyncMock()
return bridge


async def _successful_result_gen(*args, **kwargs):
"""An async generator yielding a single successful runtime result."""
yield UiPathRuntimeResult(status=UiPathRuntimeStatus.SUCCESSFUL)


class TestRunConversationalExchangeEnd:
"""Conversational runs honor the conversationalService.endExchange fps property."""

def _write_uipath_json(self, fps_properties: dict):
with open("uipath.json", "w") as f:
json.dump({"fpsProperties": fps_properties}, f)

def _invoke_conversational_run(self, runner: CliRunner, bridge):
mock_factory = _make_mock_factory(["main"])
mock_factory.new_runtime.return_value.stream = Mock(
side_effect=_successful_result_gen
)

with (
patch(
"uipath._cli.cli_run.Middlewares.next",
return_value=_middleware_continue(),
),
patch(
"uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get",
return_value=mock_factory,
),
patch(
"uipath._cli.cli_run.ResourceOverwritesContext",
side_effect=_mock_resource_overwrites_context,
),
patch("uipath._cli.cli_run.get_chat_bridge", return_value=bridge),
):
return runner.invoke(cli, ["run", "main", "{}"])

def test_exchange_end_emitted_by_default(
self, runner: CliRunner, temp_dir: str, monkeypatch
):
"""Without the endExchange fps property, the exchange end event is emitted."""
monkeypatch.setenv("UIPATH_JOB_KEY", "job-123")
monkeypatch.setenv("UIPATH_TRACING_ENABLED", "false")
monkeypatch.delenv("UIPATH_CONFIG_PATH", raising=False)

with runner.isolated_filesystem(temp_dir=temp_dir):
self._write_uipath_json(
{
"conversationalService.conversationId": "conv-1",
"conversationalService.exchangeId": "ex-1",
}
)
bridge = _make_chat_bridge_mock()

result = self._invoke_conversational_run(runner, bridge)

assert result.exit_code == 0, (
f"output: {result.output!r}, exception: {result.exception}"
)
bridge.connect.assert_awaited_once()
bridge.emit_exchange_end_event.assert_awaited_once()

def test_exchange_end_skipped_when_end_exchange_false(
self, runner: CliRunner, temp_dir: str, monkeypatch
):
"""endExchange=false in fpsProperties suppresses the exchange end event."""
monkeypatch.setenv("UIPATH_JOB_KEY", "job-123")
monkeypatch.setenv("UIPATH_TRACING_ENABLED", "false")
monkeypatch.delenv("UIPATH_CONFIG_PATH", raising=False)

with runner.isolated_filesystem(temp_dir=temp_dir):
self._write_uipath_json(
{
"conversationalService.conversationId": "conv-1",
"conversationalService.exchangeId": "ex-1",
"conversationalService.endExchange": False,
}
)
bridge = _make_chat_bridge_mock()

result = self._invoke_conversational_run(runner, bridge)

assert result.exit_code == 0, (
f"output: {result.output!r}, exception: {result.exception}"
)
bridge.connect.assert_awaited_once()
bridge.emit_exchange_end_event.assert_not_awaited()
Loading