Skip to content

Commit 5bc94c3

Browse files
Merge pull request #55 from UiPath/feature/chat-runtime-interrupt
feat: update chat runtime
2 parents c247ae7 + 7381e89 commit 5bc94c3

File tree

13 files changed

+273
-28
lines changed

13 files changed

+273
-28
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-runtime"
3-
version = "0.2.9"
3+
version = "0.3.0"
44
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/runtime/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from uipath.runtime.chat.runtime import UiPathChatRuntime
1212
from uipath.runtime.context import UiPathRuntimeContext
1313
from uipath.runtime.debug.breakpoint import UiPathBreakpointResult
14-
from uipath.runtime.debug.bridge import UiPathDebugBridgeProtocol
14+
from uipath.runtime.debug.bridge import UiPathDebugProtocol
1515
from uipath.runtime.debug.exception import UiPathDebugQuitError
1616
from uipath.runtime.debug.runtime import (
1717
UiPathDebugRuntime,
@@ -63,7 +63,7 @@
6363
"UiPathResumeTriggerType",
6464
"UiPathResumableRuntime",
6565
"UiPathDebugQuitError",
66-
"UiPathDebugBridgeProtocol",
66+
"UiPathDebugProtocol",
6767
"UiPathDebugRuntime",
6868
"UiPathBreakpointResult",
6969
"UiPathStreamNotSupportedError",

src/uipath/runtime/chat/protocol.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
"""Abstract conversation bridge interface."""
22

3-
from typing import Protocol
3+
from typing import Any, Protocol
44

5-
from uipath.core.chat import UiPathConversationMessageEvent
5+
from uipath.core.chat import (
6+
UiPathConversationMessageEvent,
7+
)
8+
9+
from uipath.runtime.result import UiPathRuntimeResult
610

711

812
class UiPathChatProtocol(Protocol):
@@ -28,3 +32,22 @@ async def emit_message_event(
2832
message_event: UiPathConversationMessageEvent to wrap and send
2933
"""
3034
...
35+
36+
async def emit_interrupt_event(
37+
self,
38+
interrupt_event: UiPathRuntimeResult,
39+
) -> None:
40+
"""Wrap and send an interrupt event.
41+
42+
Args:
43+
interrupt_event: UiPathConversationInterruptEvent to wrap and send
44+
"""
45+
...
46+
47+
async def emit_exchange_end_event(self) -> None:
48+
"""Send an exchange end event."""
49+
...
50+
51+
async def wait_for_resume(self) -> dict[str, Any]:
52+
"""Wait for the interrupt_end event to be received."""
53+
...

src/uipath/runtime/chat/runtime.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
UiPathRuntimeResult,
1818
UiPathRuntimeStatus,
1919
)
20+
from uipath.runtime.resumable.trigger import UiPathResumeTriggerType
2021
from uipath.runtime.schema import UiPathRuntimeSchema
2122

2223
logger = logging.getLogger(__name__)
@@ -65,12 +66,44 @@ async def stream(
6566
"""Stream execution events with chat support."""
6667
await self.chat_bridge.connect()
6768

68-
async for event in self.delegate.stream(input, options=options):
69-
if isinstance(event, UiPathRuntimeMessageEvent):
70-
if event.payload:
71-
await self.chat_bridge.emit_message_event(event.payload)
69+
execution_completed = False
70+
current_input = input
71+
current_options = UiPathStreamOptions(
72+
resume=options.resume if options else False,
73+
breakpoints=options.breakpoints if options else None,
74+
)
75+
76+
while not execution_completed:
77+
async for event in self.delegate.stream(
78+
current_input, options=current_options
79+
):
80+
if isinstance(event, UiPathRuntimeMessageEvent):
81+
if event.payload:
82+
await self.chat_bridge.emit_message_event(event.payload)
83+
84+
if isinstance(event, UiPathRuntimeResult):
85+
runtime_result = event
86+
87+
if (
88+
runtime_result.status == UiPathRuntimeStatus.SUSPENDED
89+
and runtime_result.trigger
90+
and runtime_result.trigger.trigger_type
91+
== UiPathResumeTriggerType.API
92+
):
93+
await self.chat_bridge.emit_interrupt_event(runtime_result)
94+
resume_data = await self.chat_bridge.wait_for_resume()
95+
96+
# Continue with resumed execution
97+
current_input = resume_data
98+
current_options.resume = True
99+
break
100+
else:
101+
yield event
102+
execution_completed = True
103+
else:
104+
yield event
72105

73-
yield event
106+
await self.chat_bridge.emit_exchange_end_event()
74107

75108
async def get_schema(self) -> UiPathRuntimeSchema:
76109
"""Get schema from the delegate runtime."""
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"""Initialization module for the debug package."""
22

33
from uipath.runtime.debug.breakpoint import UiPathBreakpointResult
4-
from uipath.runtime.debug.bridge import UiPathDebugBridgeProtocol
4+
from uipath.runtime.debug.bridge import UiPathDebugProtocol
55
from uipath.runtime.debug.exception import (
66
UiPathDebugQuitError,
77
)
88
from uipath.runtime.debug.runtime import UiPathDebugRuntime
99

1010
__all__ = [
1111
"UiPathDebugQuitError",
12-
"UiPathDebugBridgeProtocol",
12+
"UiPathDebugProtocol",
1313
"UiPathDebugRuntime",
1414
"UiPathBreakpointResult",
1515
]

src/uipath/runtime/debug/bridge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
)
1010

1111

12-
class UiPathDebugBridgeProtocol(Protocol):
12+
class UiPathDebugProtocol(Protocol):
1313
"""Abstract interface for debug communication.
1414
1515
Implementations: SignalR, Console, WebSocket, etc.

src/uipath/runtime/debug/runtime.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515
from uipath.runtime.debug import (
1616
UiPathBreakpointResult,
17-
UiPathDebugBridgeProtocol,
17+
UiPathDebugProtocol,
1818
UiPathDebugQuitError,
1919
)
2020
from uipath.runtime.events import (
@@ -42,7 +42,7 @@ class UiPathDebugRuntime:
4242
def __init__(
4343
self,
4444
delegate: UiPathRuntimeProtocol,
45-
debug_bridge: UiPathDebugBridgeProtocol,
45+
debug_bridge: UiPathDebugProtocol,
4646
trigger_poll_interval: float = 5.0,
4747
):
4848
"""Initialize the UiPathDebugRuntime.
@@ -54,7 +54,7 @@ def __init__(
5454
"""
5555
super().__init__()
5656
self.delegate = delegate
57-
self.debug_bridge: UiPathDebugBridgeProtocol = debug_bridge
57+
self.debug_bridge: UiPathDebugProtocol = debug_bridge
5858
if trigger_poll_interval < 0:
5959
raise ValueError("trigger_poll_interval must be >= 0")
6060
self.trigger_poll_interval = trigger_poll_interval

src/uipath/runtime/result.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class UiPathRuntimeResult(UiPathRuntimeEvent):
2424
output: dict[str, Any] | BaseModel | str | None = None
2525
status: UiPathRuntimeStatus = UiPathRuntimeStatus.SUCCESSFUL
2626
trigger: UiPathResumeTrigger | None = None
27+
triggers: list[UiPathResumeTrigger] | None = None
2728
error: UiPathErrorContract | None = None
2829

2930
event_type: UiPathRuntimeEventType = Field(
@@ -42,14 +43,20 @@ def to_dict(self) -> dict[str, Any]:
4243
else:
4344
output_data = self.output
4445

45-
result = {
46+
result: dict[str, Any] = {
4647
"output": output_data,
4748
"status": self.status,
4849
}
4950

5051
if self.trigger:
5152
result["resume"] = self.trigger.model_dump(by_alias=True)
5253

54+
if self.triggers:
55+
result["resumeTriggers"] = [
56+
resume_trigger.model_dump(by_alias=True)
57+
for resume_trigger in self.triggers
58+
]
59+
5360
if self.error:
5461
result["error"] = self.error.model_dump()
5562

src/uipath/runtime/resumable/protocols.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class UiPathResumableStorageProtocol(Protocol):
99
"""Protocol for storing and retrieving resume triggers."""
1010

11-
async def save_trigger(self, trigger: UiPathResumeTrigger) -> None:
11+
async def save_trigger(self, runtime_id: str, trigger: UiPathResumeTrigger) -> None:
1212
"""Save a resume trigger to storage.
1313
1414
Args:
@@ -19,7 +19,7 @@ async def save_trigger(self, trigger: UiPathResumeTrigger) -> None:
1919
"""
2020
...
2121

22-
async def get_latest_trigger(self) -> UiPathResumeTrigger | None:
22+
async def get_latest_trigger(self, runtime_id: str) -> UiPathResumeTrigger | None:
2323
"""Retrieve the most recent resume trigger from storage.
2424
2525
Returns:
@@ -30,6 +30,38 @@ async def get_latest_trigger(self) -> UiPathResumeTrigger | None:
3030
"""
3131
...
3232

33+
async def set_value(
34+
self, runtime_id: str, namespace: str, key: str, value: Any
35+
) -> None:
36+
"""Store values for a specific runtime.
37+
38+
Args:
39+
runtime_id: The runtime ID
40+
namespace: The namespace of the persisted value
41+
key: The key associated with the persisted value
42+
value: The value to persist
43+
44+
Raises:
45+
Exception: If storage operation fails
46+
"""
47+
...
48+
49+
async def get_value(self, runtime_id: str, namespace: str, key: str) -> Any:
50+
"""Retrieve values for a specific runtime from storage.
51+
52+
Args:
53+
runtime_id: The runtime ID
54+
namespace: The namespace of the persisted value
55+
key: The key associated with the persisted value
56+
57+
Returns:
58+
The value matching the method's parameters, or None if it does not exist
59+
60+
Raises:
61+
Exception: If retrieval operation fails
62+
"""
63+
...
64+
3365

3466
class UiPathResumeTriggerCreatorProtocol(Protocol):
3567
"""Protocol for creating resume triggers from suspend values."""

src/uipath/runtime/resumable/runtime.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
UiPathRuntimeProtocol,
99
UiPathStreamOptions,
1010
)
11-
from uipath.runtime.debug import UiPathBreakpointResult
11+
from uipath.runtime.debug.breakpoint import UiPathBreakpointResult
1212
from uipath.runtime.events import UiPathRuntimeEvent
1313
from uipath.runtime.result import UiPathRuntimeResult, UiPathRuntimeStatus
1414
from uipath.runtime.resumable.protocols import (
@@ -35,17 +35,20 @@ def __init__(
3535
delegate: UiPathRuntimeProtocol,
3636
storage: UiPathResumableStorageProtocol,
3737
trigger_manager: UiPathResumeTriggerProtocol,
38+
runtime_id: str,
3839
):
3940
"""Initialize the resumable runtime wrapper.
4041
4142
Args:
4243
delegate: The underlying runtime to wrap
4344
storage: Storage for persisting/retrieving resume triggers
4445
trigger_manager: Manager for creating and reading resume triggers
46+
runtime_id: Id used for runtime orchestration
4547
"""
4648
self.delegate = delegate
4749
self.storage = storage
4850
self.trigger_manager = trigger_manager
51+
self.runtime_id = runtime_id
4952

5053
async def execute(
5154
self,
@@ -115,7 +118,7 @@ async def _restore_resume_input(
115118
return input
116119

117120
# Otherwise, fetch from storage
118-
trigger = await self.storage.get_latest_trigger()
121+
trigger = await self.storage.get_latest_trigger(self.runtime_id)
119122
if not trigger:
120123
return None
121124

@@ -141,7 +144,7 @@ async def _handle_suspension(
141144

142145
# Check if trigger already exists in result
143146
if result.trigger:
144-
await self.storage.save_trigger(result.trigger)
147+
await self.storage.save_trigger(self.runtime_id, result.trigger)
145148
return result
146149

147150
suspended_result = UiPathRuntimeResult(
@@ -154,7 +157,7 @@ async def _handle_suspension(
154157
result.output
155158
)
156159

157-
await self.storage.save_trigger(suspended_result.trigger)
160+
await self.storage.save_trigger(self.runtime_id, suspended_result.trigger)
158161

159162
return suspended_result
160163

0 commit comments

Comments
 (0)