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.13.21"
version = "2.15.0"
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.30, <0.6.0",
"uipath-runtime>=0.12.2, <0.13.0",
"uipath-runtime>=0.13.0, <0.14.0",
"uipath-platform>=0.2.14, <0.3.0",
"click>=8.3.1",
"httpx>=0.28.1",
Expand Down
57 changes: 39 additions & 18 deletions packages/uipath/src/uipath/_cli/_chat/_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,14 @@ async def disconnect(self) -> None:
finally:
await self._cleanup_client()

def _require_client(self) -> Any:
client = self._client
if client is None:
raise RuntimeError("WebSocket client not connected. Call connect() first.")
if not self._connected_event.is_set() and not self._websocket_disabled:
raise RuntimeError("WebSocket client not in connected state")
return client

async def emit_message_event(
self, message_event: UiPathConversationMessageEvent
) -> None:
Expand All @@ -284,11 +292,7 @@ async def emit_message_event(
Raises:
RuntimeError: If client is not connected
"""
if self._client is None:
raise RuntimeError("WebSocket client not connected. Call connect() first.")

if not self._connected_event.is_set() and not self._websocket_disabled:
raise RuntimeError("WebSocket client not in connected state")
client = self._require_client()

try:
# Wrap message event with conversation/exchange IDs
Expand All @@ -309,7 +313,7 @@ async def emit_message_event(
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)
await client.emit("ConversationEvent", event_data)

# Store the current message ID, used for emitting interrupt events.
self._current_message_id = message_event.message_id
Expand All @@ -318,6 +322,31 @@ async def emit_message_event(
logger.error(f"Error sending conversation event to WebSocket: {e}")
raise RuntimeError(f"Failed to send conversation event: {e}") from e

async def emit_meta_event(self, meta_event: dict[str, Any]) -> None:
"""Send an exchange-scoped conversation metadata event."""
client = self._require_client()

try:
event = UiPathConversationEvent(
conversation_id=self.conversation_id,
exchange=UiPathConversationExchangeEvent(
exchange_id=self.exchange_id,
meta_event=meta_event,
),
)
event_data = event.model_dump(mode="json", exclude_none=True, by_alias=True)

if self._websocket_disabled:
logger.info(
"SocketIOChatBridge is in debug mode. Not sending event: %s",
json.dumps(event_data),
)
else:
await client.emit("ConversationEvent", event_data)
except Exception as e:
logger.error(f"Error sending conversation event to WebSocket: {e}")
raise RuntimeError(f"Failed to send conversation event: {e}") from e

async def emit_exchange_end_event(self) -> None:
"""Send an exchange end event.

Expand All @@ -331,11 +360,7 @@ async def emit_exchange_end_event(self) -> None:
logger.info("end_exchange is False; leaving the exchange open.")
return

if self._client is None:
raise RuntimeError("WebSocket client not connected. Call connect() first.")

if not self._connected_event.is_set() and not self._websocket_disabled:
raise RuntimeError("WebSocket client not in connected state")
client = self._require_client()

try:
exchange_end_event = UiPathConversationEvent(
Expand All @@ -355,7 +380,7 @@ async def emit_exchange_end_event(self) -> None:
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)
await client.emit("ConversationEvent", event_data)

except Exception as e:
logger.error(f"Error sending conversation event to WebSocket: {e}")
Expand All @@ -371,11 +396,7 @@ async def emit_exchange_error_event(self, error: Exception) -> None:
Args:
error: The exception that caused the error.
"""
if self._client is None:
raise RuntimeError("WebSocket client not connected. Call connect() first.")

if not self._connected_event.is_set() and not self._websocket_disabled:
raise RuntimeError("WebSocket client not in connected state")
client = self._require_client()

# Extract and map error to CAS-specific error ID and message.
cas_error_id, cas_message = _resolve_cas_error(error)
Expand Down Expand Up @@ -403,7 +424,7 @@ async def emit_exchange_error_event(self, error: Exception) -> None:
f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}"
)
else:
await self._client.emit("ConversationEvent", event_data)
await client.emit("ConversationEvent", event_data)

except Exception as e:
logger.error(f"Error sending exchange error event to WebSocket: {e}")
Expand Down
175 changes: 126 additions & 49 deletions packages/uipath/src/uipath/_cli/cli_debug.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import logging
from contextlib import AsyncExitStack
from typing import Any, cast, get_args

import click
Expand All @@ -11,17 +12,23 @@
from uipath._cli._utils._tracing import create_trace_manager
from uipath.eval.mocks import UiPathMockRuntime
from uipath.eval.mocks._mock_runtime import load_simulation_config
from uipath.platform import UiPath
from uipath.platform.common import (
ExecutionSourceContext,
ResourceOverwritesContext,
UiPathConfig,
)
from uipath.runtime import (
ConversationalWorkspaceRuntime,
HydrationRuntime,
UiPathExecuteOptions,
UiPathRuntimeContext,
UiPathRuntimeFactoryProtocol,
UiPathRuntimeFactoryRegistry,
UiPathRuntimeProtocol,
Workspace,
WorkspaceHydrator,
WorkspaceRegistryStore,
)
from uipath.runtime.chat import UiPathChatProtocol, UiPathChatRuntime
from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugRuntime
Expand Down Expand Up @@ -180,69 +187,139 @@ async def execute_debug_runtime():

async def execute_debug_runtime():
chat_runtime: UiPathRuntimeProtocol | None = None
debug_bridge: UiPathDebugProtocol = get_debug_bridge(
ctx, attach=attach_mode
)
new_runtime_kwargs: dict[str, Any] = {}
if governance_bootstrap is not None:
new_runtime_kwargs["evaluator"] = (
governance_bootstrap.evaluator
workspace: Workspace | None = None
hydration_runtime: HydrationRuntime | None = None
conversational_workspace_runtime: (
ConversationalWorkspaceRuntime | None
) = None
debug_runtime: UiPathRuntimeProtocol | None = None
mock_runtime: UiPathRuntimeProtocol | None = None
runtime: UiPathRuntimeProtocol | None = None
try:
debug_bridge: UiPathDebugProtocol = get_debug_bridge(
ctx, attach=attach_mode
)
runtime = await factory.new_runtime(
entrypoint,
governance_runtime_id,
**new_runtime_kwargs,
)

if governance_bootstrap is not None:
runtime = governance_bootstrap.wrap_runtime(
runtime,
agent_name=entrypoint,
runtime_id=governance_runtime_id,
new_runtime_kwargs: dict[str, Any] = {}
if governance_bootstrap is not None:
new_runtime_kwargs["evaluator"] = (
governance_bootstrap.evaluator
)
runtime = await factory.new_runtime(
entrypoint,
governance_runtime_id,
**new_runtime_kwargs,
)

delegate = runtime
if ctx.conversation_id and ctx.exchange_id:
chat_bridge: UiPathChatProtocol = get_chat_bridge(
context=ctx
)
chat_runtime = UiPathChatRuntime(
delegate=delegate, chat_bridge=chat_bridge
)
delegate = chat_runtime
if governance_bootstrap is not None:
runtime = governance_bootstrap.wrap_runtime(
runtime,
agent_name=entrypoint,
runtime_id=governance_runtime_id,
)

debug_runtime = UiPathDebugRuntime(
delegate=delegate,
debug_bridge=debug_bridge,
trigger_poll_interval=trigger_poll_interval,
)
delegate = runtime
if (
ctx.job_id is not None
and factory_settings is not None
and factory_settings.managed_workspace
):
storage = await factory.get_storage()
if storage is None:
raise RuntimeError(
"Runtime factory advertises managed workspace "
"support but provides no storage"
)

# Build mocking context with agent model for simulations
schema = await runtime.get_schema()
agent_model = None
if schema.metadata and "settings" in schema.metadata:
agent_model = schema.metadata["settings"].get("model")
client = UiPath()
workspace = Workspace.create()
workspace.path = workspace.path.resolve()
hydrator = WorkspaceHydrator(
workspace_path=workspace.path,
attachments=client.attachments,
jobs=client.jobs,
current_job_key=ctx.job_id,
folder_key=ctx.folder_key,
)
Comment on lines +233 to +242
registry_store = WorkspaceRegistryStore(
storage, ctx.job_id
)
hydration_runtime = HydrationRuntime(
runtime,
workspace=workspace,
hydrator=hydrator,
registry_store=registry_store,
)
delegate = hydration_runtime

mocking_context = load_simulation_config(
agent_model=agent_model
)
if (
ctx.conversation_id is not None
and ctx.exchange_id is not None
):
conversational_workspace_runtime = (
ConversationalWorkspaceRuntime(
hydration_runtime,
hydrator=hydrator,
)
)
delegate = conversational_workspace_runtime

mock_runtime = UiPathMockRuntime(
delegate=debug_runtime,
mocking_context=mocking_context,
)
if ctx.conversation_id and ctx.exchange_id:
chat_bridge: UiPathChatProtocol = get_chat_bridge(
context=ctx
)
chat_runtime = UiPathChatRuntime(
delegate=delegate, chat_bridge=chat_bridge
)
delegate = chat_runtime

debug_runtime = UiPathDebugRuntime(
delegate=delegate,
debug_bridge=debug_bridge,
trigger_poll_interval=trigger_poll_interval,
)

schema = await runtime.get_schema()
agent_model = None
if schema.metadata and "settings" in schema.metadata:
agent_model = schema.metadata["settings"].get(
"model"
)

mocking_context = load_simulation_config(
agent_model=agent_model
)

mock_runtime = UiPathMockRuntime(
delegate=debug_runtime,
mocking_context=mocking_context,
)

try:
ctx.result = await mock_runtime.execute(
ctx.get_input(),
options=UiPathExecuteOptions(resume=resume),
)
finally:
await mock_runtime.dispose()
await debug_runtime.dispose()
cleanup = AsyncExitStack()
if hydration_runtime is None:
if runtime is not None:
cleanup.push_async_callback(runtime.dispose)
if workspace is not None:
cleanup.push_async_callback(workspace.dispose)
if hydration_runtime is not None:
cleanup.push_async_callback(
hydration_runtime.dispose
)
if conversational_workspace_runtime is not None:
cleanup.push_async_callback(
conversational_workspace_runtime.dispose
)
if chat_runtime:
await chat_runtime.dispose()
await runtime.dispose()
cleanup.push_async_callback(chat_runtime.dispose)
if debug_runtime is not None:
cleanup.push_async_callback(debug_runtime.dispose)
if mock_runtime is not None:
cleanup.push_async_callback(mock_runtime.dispose)
await cleanup.aclose()

if project_id := UiPathConfig.project_id:
studio_client = StudioClient(project_id)
Expand Down
Loading
Loading