Skip to content
Merged
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
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
[project]
name = "uipath-dev"
version = "0.0.11"
version = "0.0.12"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.0.9, <0.1.0",
"uipath-runtime>=0.1.3, <0.2.0",
"uipath-runtime>=0.2.0, <0.3.0",
"textual>=6.7.1, <7.0.0",
"pyperclip>=1.11.0, <2.0.0",
]
Expand Down
29 changes: 22 additions & 7 deletions src/uipath/dev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,21 @@ async def handle_chat_input(self, event: Input.Submitted) -> None:

details_panel = self.query_one("#details-panel", RunDetailsPanel)
if details_panel and details_panel.current_run:
status = details_panel.current_run.status
current_run = details_panel.current_run
status = current_run.status
if status == "running":
self.app.notify(
"Wait for agent response...", timeout=1.5, severity="warning"
)
return

if details_panel.current_run.status == "suspended":
details_panel.current_run.resume_data = {"value": user_text}
if current_run.status == "suspended":
resume_input: Any = {}
try:
resume_input = json.loads(user_text)
except json.JSONDecodeError:
resume_input = user_text
current_run.resume_data = resume_input
else:
msg = get_user_message(user_text)
msg_ev = get_user_message_event(user_text)
Expand All @@ -154,13 +160,18 @@ async def handle_chat_input(self, event: Input.Submitted) -> None:
ChatMessage(
event=msg_ev,
message=msg,
run_id=details_panel.current_run.id,
run_id=current_run.id,
)
)
details_panel.current_run.add_event(msg_ev)
details_panel.current_run.input_data = {"messages": [msg]}
current_run.add_event(msg_ev)
current_run.input_data = {"messages": [msg]}

asyncio.create_task(self._execute_runtime(details_panel.current_run))
if current_run.mode == ExecutionMode.DEBUG:
asyncio.create_task(
self._resume_runtime(current_run, current_run.resume_data)
)
else:
asyncio.create_task(self._execute_runtime(current_run))

event.input.clear()

Expand Down Expand Up @@ -244,6 +255,10 @@ async def _execute_runtime(self, run: ExecutionRun) -> None:
"""Wrapper that delegates execution to RunService."""
await self.run_service.execute(run)

async def _resume_runtime(self, run: ExecutionRun, resume_data: Any) -> None:
"""Wrapper that delegates execution to RunService."""
await self.run_service.resume_debug(run, resume_data)

def _on_run_updated(self, run: ExecutionRun) -> None:
"""Called whenever a run changes (status, times, logs, traces)."""
# Update the run in history
Expand Down
2 changes: 1 addition & 1 deletion src/uipath/dev/models/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def add(

existing_tool_call.result = UiPathConversationToolCallResult(
timestamp=tc_event.end.timestamp,
value=tc_event.end.result,
value=tc_event.end.output,
is_error=tc_event.end.is_error,
cancelled=tc_event.end.cancelled,
)
Expand Down
4 changes: 3 additions & 1 deletion src/uipath/dev/models/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(
self.entrypoint = entrypoint
self.input_data = input_data
self.mode = mode
self.resume_data: dict[str, Any] | None = None
self.resume_data: Any | None = None
self.output_data: dict[str, Any] | str | None = None
self.start_time = datetime.now()
self.end_time: datetime | None = None
Expand Down Expand Up @@ -98,4 +98,6 @@ def messages(self) -> list[UiPathConversationMessage]:

def add_event(self, event: Any) -> UiPathConversationMessage | None:
"""Add a conversation event to the run's chat aggregator."""
if event is None:
return None
return self.chat_events.add(cast(UiPathConversationMessageEvent, event))
30 changes: 29 additions & 1 deletion src/uipath/dev/services/debug_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from uipath.runtime.debug import UiPathBreakpointResult, UiPathDebugQuitError
from uipath.runtime.events import UiPathRuntimeStateEvent
from uipath.runtime.result import UiPathRuntimeResult
from uipath.runtime.resumable import UiPathResumeTriggerType

logger = logging.getLogger(__name__)

Expand All @@ -18,6 +19,7 @@ def __init__(self):
"""Initialize the debug bridge."""
self._connected = False
self._resume_event = asyncio.Event()
self._resume_data: dict[str, Any] | None = None
self._terminate_event = asyncio.Event()
self._breakpoints: list[str] | Literal["*"] = "*" # Default: step mode

Expand Down Expand Up @@ -60,6 +62,29 @@ async def emit_breakpoint_hit(
if self.on_breakpoint_hit:
self.on_breakpoint_hit(breakpoint_result)

async def emit_execution_suspended(
self, runtime_result: UiPathRuntimeResult
) -> None:
"""Notify debugger that execution is suspended."""
logger.debug("Execution suspended")
if runtime_result.trigger is None:
return

if runtime_result.trigger.trigger_type == UiPathResumeTriggerType.API:
if self.on_breakpoint_hit:
self.on_breakpoint_hit(
UiPathBreakpointResult(
breakpoint_node="<suspended>",
breakpoint_type="before",
current_state=runtime_result.output,
next_nodes=[],
)
)

async def emit_execution_resumed(self, resume_data: Any) -> None:
"""Notify debugger that execution resumed."""
logger.debug("Execution resumed")

async def emit_execution_completed(
self, runtime_result: UiPathRuntimeResult
) -> None:
Expand All @@ -86,12 +111,15 @@ async def wait_for_resume(self) -> Any:
if self._terminate_event.is_set():
raise UiPathDebugQuitError("Debug session quit requested")

return self._resume_data

async def wait_for_terminate(self) -> None:
"""Wait for terminate command from debugger."""
await self._terminate_event.wait()

def resume(self) -> None:
def resume(self, resume_data: Any) -> None:
"""Signal that execution should resume (called from UI buttons)."""
self._resume_data = resume_data or {}
self._resume_event.set()

def quit(self) -> None:
Expand Down
32 changes: 19 additions & 13 deletions src/uipath/dev/services/run_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,10 @@ def get_run(self, run_id: str) -> ExecutionRun | None:
return self.runs.get(run_id)

async def execute(self, run: ExecutionRun) -> None:
"""Execute or resume a run.

This is the extracted version of the old `_execute_runtime` method.
"""
"""Execute or resume a run."""
new_runtime: UiPathRuntimeProtocol | None = None
try:
execution_input: dict[str, Any] | None = {}
execution_input: dict[str, Any] | str | None = {}
execution_options: UiPathExecuteOptions = UiPathExecuteOptions()

if run.status == "suspended":
Expand Down Expand Up @@ -179,14 +176,15 @@ async def execute(self, run: ExecutionRun) -> None:
):
run.status = "suspended"
else:
if result.output is None:
run.output_data = {}
elif isinstance(result.output, BaseModel):
run.output_data = result.output.model_dump()
else:
run.output_data = result.output
run.status = "completed"

if result.output is None:
run.output_data = {}
elif isinstance(result.output, BaseModel):
run.output_data = result.output.model_dump()
else:
run.output_data = result.output

if run.output_data:
self._add_info_log(run, f"Execution result: {run.output_data}")

Expand Down Expand Up @@ -218,6 +216,14 @@ async def execute(self, run: ExecutionRun) -> None:
if run.id in self.debug_bridges:
del self.debug_bridges[run.id]

async def resume_debug(self, run: ExecutionRun, resume_data: Any) -> None:
"""Resume debug execution from a breakpoint."""
debug_bridge = self.debug_bridges.get(run.id)
if debug_bridge:
run.status = "running"
self._emit_run_updated(run)
debug_bridge.resume(resume_data)

def step_debug(self, run: ExecutionRun) -> None:
"""Step to next breakpoint in debug mode."""
debug_bridge = self.debug_bridges.get(run.id)
Expand All @@ -227,7 +233,7 @@ def step_debug(self, run: ExecutionRun) -> None:
# Resume execution (will pause at next node)
run.status = "running"
self._emit_run_updated(run)
debug_bridge.resume()
debug_bridge.resume(resume_data={})

def continue_debug(self, run: ExecutionRun) -> None:
"""Continue execution without stopping at breakpoints."""
Expand All @@ -238,7 +244,7 @@ def continue_debug(self, run: ExecutionRun) -> None:
# Resume execution
run.status = "running"
self._emit_run_updated(run)
debug_bridge.resume()
debug_bridge.resume(resume_data={})

def stop_debug(self, run: ExecutionRun) -> None:
"""Stop debug execution."""
Expand Down
1 change: 1 addition & 0 deletions src/uipath/dev/ui/panels/run_details_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ def add_chat_message(

if not self.current_run or chat_msg.run_id != self.current_run.id:
return

self._chat_panel.add_chat_message(chat_msg)

def add_trace(self, trace_msg: TraceMessage):
Expand Down
Loading