From bc78e7a58e9ed5c2c6f14fa7ec96075b07bf07df Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Fri, 21 Nov 2025 16:01:32 +0200 Subject: [PATCH] fix: handle debug controls --- src/uipath/dev/__init__.py | 25 +- src/uipath/dev/_demo/mock_context_runtime.py | 527 +++++++++++++----- src/uipath/dev/services/run_service.py | 73 ++- src/uipath/dev/ui/panels/run_details_panel.py | 12 +- src/uipath/dev/ui/styles/terminal.tcss | 4 +- 5 files changed, 448 insertions(+), 193 deletions(-) diff --git a/src/uipath/dev/__init__.py b/src/uipath/dev/__init__.py index 46bbe01..9d056af 100644 --- a/src/uipath/dev/__init__.py +++ b/src/uipath/dev/__init__.py @@ -210,40 +210,21 @@ async def action_debug_step(self) -> None: details_panel = self.query_one("#details-panel", RunDetailsPanel) if details_panel and details_panel.current_run: run = details_panel.current_run - - # Get the debug bridge for this run - debug_bridge = self.run_service.get_debug_bridge(run.id) - if debug_bridge: - # Step mode = break on all nodes - debug_bridge.set_breakpoints("*") - # Resume execution (will pause at next node) - debug_bridge.resume() + self.run_service.step_debug(run) async def action_debug_continue(self) -> None: """Continue execution without stopping at breakpoints.""" details_panel = self.query_one("#details-panel", RunDetailsPanel) if details_panel and details_panel.current_run: run = details_panel.current_run - - # Get the debug bridge for this run - debug_bridge = self.run_service.get_debug_bridge(run.id) - if debug_bridge: - # Clear breakpoints = run to completion - debug_bridge.set_breakpoints([]) - # Resume execution - debug_bridge.resume() + self.run_service.continue_debug(run) async def action_debug_stop(self) -> None: """Stop debug execution.""" details_panel = self.query_one("#details-panel", RunDetailsPanel) if details_panel and details_panel.current_run: run = details_panel.current_run - - # Get the debug bridge for this run - debug_bridge = self.run_service.get_debug_bridge(run.id) - if debug_bridge: - # Signal quit - debug_bridge.quit() + self.run_service.stop_debug(run) async def action_clear_history(self) -> None: """Clear run history.""" diff --git a/src/uipath/dev/_demo/mock_context_runtime.py b/src/uipath/dev/_demo/mock_context_runtime.py index 6d71eac..ea9c4b2 100644 --- a/src/uipath/dev/_demo/mock_context_runtime.py +++ b/src/uipath/dev/_demo/mock_context_runtime.py @@ -23,6 +23,20 @@ class MockContextRuntime: def __init__(self, entrypoint: str = ENTRYPOINT_CONTEXT) -> None: self.entrypoint = entrypoint self.tracer = trace.get_tracer("uipath.dev.mock.context") + # State tracking for breakpoints + self.current_step_index: int = 0 + self.steps = [ + ("initialize.environment", "initialize-environment", "init"), + ("validate.input", "validate-input", "validation"), + ("preprocess.data", "preprocess-data", "preprocess"), + ("compute.result", "compute-result", "compute"), + ("compute.embeddings", "compute-embeddings", "compute-subtask"), + ("query.knowledgebase", "query-knowledgebase", "io"), + ("postprocess.results", "postprocess-results", "postprocess"), + ("generate.output", "generate-output", "postprocess-subtask"), + ("persist.artifacts", "persist-artifacts", "io"), + ("cleanup.resources", "cleanup-resources", "cleanup"), + ] async def get_schema(self) -> UiPathRuntimeSchema: return UiPathRuntimeSchema( @@ -46,12 +60,24 @@ async def execute( input: Optional[dict[str, Any]] = None, options: Optional[UiPathExecuteOptions] = None, ) -> UiPathRuntimeResult: - payload = input or {} + from uipath.runtime.debug import UiPathBreakpointResult + payload = input or {} entrypoint = "mock-entrypoint" message = str(payload.get("message", "")) message_length = len(message) + # Check breakpoints + breakpoints = options.breakpoints if options else None + should_break_all = breakpoints == "*" + should_break_on = set(breakpoints) if isinstance(breakpoints, list) else set() + is_resuming = options.resume if options else False + + # If resuming, skip to next step + if is_resuming: + self.current_step_index += 1 + + # Root span for entire execution with self.tracer.start_as_current_span( "mock-runtime.execute", attributes={ @@ -62,141 +88,353 @@ async def execute( "uipath.input.has_message": "message" in payload, }, ) as root_span: - logger.info( - "MockRuntime: starting execution", - extra={ - "uipath.runtime.entrypoint": entrypoint, - }, - ) - print(f"[MockRuntime] Starting execution with payload={payload!r}") - - # Stage 1: Initialization - with self.tracer.start_as_current_span( - "initialize.environment", - attributes={ - "uipath.step.name": "initialize-environment", - "uipath.step.kind": "init", - }, - ): - logger.info("MockRuntime: initializing environment") - print("[MockRuntime] Initializing environment...") - await asyncio.sleep(0.5) - - # Stage 2: Validation - with self.tracer.start_as_current_span( - "validate.input", - attributes={ - "uipath.step.name": "validate-input", - "uipath.step.kind": "validation", - "uipath.input.has_message": "message" in payload, - }, - ) as validate_span: - logger.info("MockRuntime: validating input") - print("[MockRuntime] Validating input...") - await asyncio.sleep(0.5) - - if "message" not in payload: - logger.warning("MockRuntime: missing 'message' in payload") - validate_span.set_attribute( - "uipath.validation.missing_field", "message" + # Execute from current step + while self.current_step_index < len(self.steps): + span_name, step_name, step_kind = self.steps[self.current_step_index] + + # Create nested spans based on step + if step_name == "initialize-environment": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Load config + with self.tracer.start_as_current_span( + "init.load_config", + attributes={"uipath.config.source": "default"}, + ): + await asyncio.sleep(2) + + # Nested: Setup resources + with self.tracer.start_as_current_span( + "init.setup_resources", + attributes={"uipath.resources.count": 3}, + ): + await asyncio.sleep(2) + + # Nested: Initialize connections + with self.tracer.start_as_current_span( + "init.connections", + attributes={"uipath.connections.type": "http"}, + ): + await asyncio.sleep(2) + + elif step_name == "validate-input": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Schema validation + with self.tracer.start_as_current_span( + "validate.schema", + attributes={"uipath.schema.valid": True}, + ): + await asyncio.sleep(2) + + # Nested: Type checking + with self.tracer.start_as_current_span( + "validate.types", + attributes={"uipath.types.checked": 2}, + ): + await asyncio.sleep(2) + + elif step_name == "preprocess-data": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Normalize text + with self.tracer.start_as_current_span( + "preprocess.normalize", + attributes={"uipath.text.normalized": True}, + ): + await asyncio.sleep(2) + + # Nested: Tokenization + with self.tracer.start_as_current_span( + "preprocess.tokenize", + attributes={"uipath.tokens.count": 42}, + ): + await asyncio.sleep(2) + + elif step_name == "compute-result": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Feature extraction + with self.tracer.start_as_current_span( + "compute.extract_features", + attributes={"uipath.features.count": 128}, + ): + await asyncio.sleep(2) + + # Nested: Model inference + with self.tracer.start_as_current_span( + "compute.inference", + attributes={ + "uipath.model.name": "mock-model-v1", + "uipath.inference.batch_size": 1, + }, + ): + # Deeply nested: Load weights + with self.tracer.start_as_current_span( + "inference.load_weights", + attributes={"uipath.weights.size_mb": 150}, + ): + await asyncio.sleep(2) + + # Deeply nested: Forward pass + with self.tracer.start_as_current_span( + "inference.forward_pass", + attributes={"uipath.layers.executed": 12}, + ): + await asyncio.sleep(2) + + elif step_name == "compute-embeddings": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Encode text + with self.tracer.start_as_current_span( + "embeddings.encode", + attributes={"uipath.embedding.dim": 768}, + ): + await asyncio.sleep(2) + + # Nested: Normalize vectors + with self.tracer.start_as_current_span( + "embeddings.normalize", + attributes={"uipath.normalization.method": "l2"}, + ): + await asyncio.sleep(2) + + elif step_name == "query-knowledgebase": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Build query + with self.tracer.start_as_current_span( + "kb.build_query", + attributes={"uipath.query.type": "vector_search"}, + ): + await asyncio.sleep(2) + + # Nested: Execute search + with self.tracer.start_as_current_span( + "kb.search", + attributes={ + "uipath.kb.index": "documents-v2", + "uipath.kb.top_k": 5, + }, + ): + # Deeply nested: Vector similarity + with self.tracer.start_as_current_span( + "search.vector_similarity", + attributes={"uipath.similarity.metric": "cosine"}, + ): + await asyncio.sleep(2) + + # Deeply nested: Rank results + with self.tracer.start_as_current_span( + "search.rank_results", + attributes={"uipath.ranking.algorithm": "bm25"}, + ): + await asyncio.sleep(2) + + # Nested: Filter results + with self.tracer.start_as_current_span( + "kb.filter_results", + attributes={"uipath.results.filtered": 3}, + ): + await asyncio.sleep(2) + + elif step_name == "postprocess-results": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Format output + with self.tracer.start_as_current_span( + "postprocess.format", + attributes={"uipath.format.type": "json"}, + ): + await asyncio.sleep(2) + + # Nested: Apply templates + with self.tracer.start_as_current_span( + "postprocess.templates", + attributes={"uipath.template.name": "standard_response"}, + ): + await asyncio.sleep(2) + + elif step_name == "generate-output": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Serialize data + with self.tracer.start_as_current_span( + "output.serialize", + attributes={"uipath.serialization.format": "json"}, + ): + await asyncio.sleep(2) + + # Nested: Add metadata + with self.tracer.start_as_current_span( + "output.add_metadata", + attributes={"uipath.metadata.fields": 5}, + ): + await asyncio.sleep(2) + + elif step_name == "persist-artifacts": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Compress data + with self.tracer.start_as_current_span( + "persist.compress", + attributes={"uipath.compression.algorithm": "gzip"}, + ): + await asyncio.sleep(2) + + # Nested: Write to storage + with self.tracer.start_as_current_span( + "persist.write", + attributes={"uipath.storage.backend": "s3"}, + ): + await asyncio.sleep(2) + + elif step_name == "cleanup-resources": + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + + # Nested: Close connections + with self.tracer.start_as_current_span( + "cleanup.close_connections", + attributes={"uipath.connections.closed": 3}, + ): + await asyncio.sleep(2) + + # Nested: Free memory + with self.tracer.start_as_current_span( + "cleanup.free_memory", + attributes={"uipath.memory.freed_mb": 512}, + ): + await asyncio.sleep(2) + + else: + # Default simple span for any other steps + with self.tracer.start_as_current_span( + span_name, + attributes={ + "uipath.step.name": step_name, + "uipath.step.kind": step_kind, + }, + ): + logger.info(f"MockRuntime: executing {step_name}") + print(f"[MockRuntime] ▶️ Executing: {step_name}") + await asyncio.sleep(2) + + # Check if we should break AFTER executing this step + if should_break_all or step_name in should_break_on: + logger.info(f"MockRuntime: hitting breakpoint at {step_name}") + print(f"[MockRuntime] 🔴 Breakpoint hit: {step_name}") + + # Determine next nodes + next_nodes = [] + if self.current_step_index + 1 < len(self.steps): + next_nodes = [self.steps[self.current_step_index + 1][1]] + + return UiPathBreakpointResult( + status=UiPathRuntimeStatus.SUSPENDED, + breakpoint_node=step_name, + breakpoint_type="after", + current_state={ + "paused_at": step_name, + "step_index": self.current_step_index, + "payload": payload, + "message": message, + }, + next_nodes=next_nodes, + output={ + "paused_at": step_name, + "step_index": self.current_step_index, + }, ) - # Stage 3: Preprocessing - with self.tracer.start_as_current_span( - "preprocess.data", - attributes={ - "uipath.step.name": "preprocess-data", - "uipath.step.kind": "preprocess", - "uipath.input.size.bytes": len(str(payload).encode("utf-8")), - }, - ): - logger.info("MockRuntime: preprocessing data") - print("[MockRuntime] Preprocessing data...") - await asyncio.sleep(0.5) - - # Stage 4: Compute / reasoning - with self.tracer.start_as_current_span( - "compute.result", - attributes={ - "uipath.step.name": "compute-result", - "uipath.step.kind": "compute", - }, - ): - logger.info("MockRuntime: compute phase started") - print("[MockRuntime] Compute phase...") - - # Subtask: embedding computation - with self.tracer.start_as_current_span( - "compute.embeddings", - attributes={ - "uipath.step.name": "compute-embeddings", - "uipath.step.kind": "compute-subtask", - }, - ): - logger.info("MockRuntime: computing embeddings") - print("[MockRuntime] Computing embeddings...") - await asyncio.sleep(0.5) - - # Subtask: KB query - with self.tracer.start_as_current_span( - "query.knowledgebase", - attributes={ - "uipath.step.name": "query-knowledgebase", - "uipath.step.kind": "io", - "uipath.kb.query.length": message_length, - }, - ): - logger.info("MockRuntime: querying knowledge base") - print("[MockRuntime] Querying knowledge base...") - await asyncio.sleep(0.5) - - # Stage 5: Post-processing - with self.tracer.start_as_current_span( - "postprocess.results", - attributes={ - "uipath.step.name": "postprocess-results", - "uipath.step.kind": "postprocess", - }, - ): - logger.info("MockRuntime: post-processing results") - print("[MockRuntime] Post-processing results...") - await asyncio.sleep(0.4) - - with self.tracer.start_as_current_span( - "generate.output", - attributes={ - "uipath.step.name": "generate-output", - "uipath.step.kind": "postprocess-subtask", - }, - ): - logger.info("MockRuntime: generating structured output") - print("[MockRuntime] Generating output...") - await asyncio.sleep(0.4) - - # Stage 6: Persistence - with self.tracer.start_as_current_span( - "persist.artifacts", - attributes={ - "uipath.step.name": "persist-artifacts", - "uipath.step.kind": "io", - "uipath.persistence.enabled": False, - }, - ): - logger.info("MockRuntime: persisting artifacts (mock)") - print("[MockRuntime] Persisting artifacts (mock)...") - await asyncio.sleep(0.4) - - # Stage 7: Cleanup - with self.tracer.start_as_current_span( - "cleanup.resources", - attributes={ - "uipath.step.name": "cleanup-resources", - "uipath.step.kind": "cleanup", - }, - ): - logger.info("MockRuntime: cleaning up resources") - print("[MockRuntime] Cleaning up resources...") - await asyncio.sleep(0.3) + # Move to next step + self.current_step_index += 1 + + # All steps completed - reset state + self.current_step_index = 0 + + root_span.set_attribute("uipath.runtime.status", "success") + root_span.set_attribute("uipath.runtime.steps_executed", len(self.steps)) result_payload = { "result": f"Mock runtime processed: {payload.get('message', '')}", @@ -206,26 +444,11 @@ async def execute( }, } - root_span.set_attribute("uipath.runtime.status", "success") - root_span.set_attribute("uipath.runtime.duration.approx", "5s") - root_span.set_attribute("uipath.output.has_error", False) - root_span.set_attribute( - "uipath.output.message_length", len(str(result_payload)) + return UiPathRuntimeResult( + output=result_payload, + status=UiPathRuntimeStatus.SUCCESSFUL, ) - logger.info( - "MockRuntime: execution completed successfully", - extra={ - "uipath.runtime.status": "success", - }, - ) - print(f"[MockRuntime] Finished successfully with result={result_payload!r}") - - return UiPathRuntimeResult( - output=result_payload, - status=UiPathRuntimeStatus.SUCCESSFUL, - ) - async def stream( self, input: Optional[dict[str, Any]] = None, diff --git a/src/uipath/dev/services/run_service.py b/src/uipath/dev/services/run_service.py index cf54fc1..e94a3cf 100644 --- a/src/uipath/dev/services/run_service.py +++ b/src/uipath/dev/services/run_service.py @@ -115,6 +115,12 @@ async def execute(self, run: ExecutionRun) -> None: debug_bridge.on_breakpoint_hit = lambda bp: self._handle_breakpoint_hit( run.id, bp ) + debug_bridge.on_execution_started = lambda: self._handle_debug_started( + run.id + ) + debug_bridge.on_execution_error = lambda error: self._add_error_log( + run, error + ) # Store bridge so UI can access it self.debug_bridges[run.id] = debug_bridge @@ -178,6 +184,34 @@ async def execute(self, run: ExecutionRun) -> None: if run.id in self.debug_bridges: del self.debug_bridges[run.id] + def step_debug(self, run: ExecutionRun) -> None: + """Step to next breakpoint in debug mode.""" + debug_bridge = self.debug_bridges.get(run.id) + if debug_bridge: + # Step mode = break on all nodes + debug_bridge.set_breakpoints("*") + # Resume execution (will pause at next node) + run.status = "running" + self._emit_run_updated(run) + debug_bridge.resume() + + def continue_debug(self, run: ExecutionRun) -> None: + """Continue execution without stopping at breakpoints.""" + debug_bridge = self.debug_bridges.get(run.id) + if debug_bridge: + # Clear breakpoints = run to completion + debug_bridge.set_breakpoints([]) + # Resume execution + run.status = "running" + self._emit_run_updated(run) + debug_bridge.resume() + + def stop_debug(self, run: ExecutionRun) -> None: + """Stop debug execution.""" + debug_bridge = self.debug_bridges.get(run.id) + if debug_bridge: + debug_bridge.quit() + def handle_log(self, log_msg: LogMessage) -> None: """Entry point for all logs (runtime, traces, stderr).""" run = self.runs.get(log_msg.run_id) @@ -214,6 +248,13 @@ def _handle_state_update(self, run_id: str, event) -> None: # You can add more logic here later if needed pass + def _handle_debug_started(self, run_id: str) -> None: + """Handle debug started event.""" + run = self.runs.get(run_id) + if run: + run.status = "suspended" + self._emit_run_updated(run) + def _handle_breakpoint_hit(self, run_id: str, bp) -> None: """Handle breakpoint hit from debug runtime.""" run = self.runs.get(run_id) @@ -236,17 +277,25 @@ def _add_info_log(self, run: ExecutionRun, message: str) -> None: ) self.handle_log(log_msg) - def _add_error_log(self, run: ExecutionRun) -> None: - from rich.traceback import Traceback + def _add_error_log(self, run: ExecutionRun, error: str | None = None) -> None: + if error is None: + from rich.traceback import Traceback - tb = Traceback( - show_locals=False, - max_frames=4, - ) - log_msg = LogMessage( - run_id=run.id, - level="ERROR", - message=tb, - timestamp=datetime.now(), - ) + tb = Traceback( + show_locals=False, + max_frames=4, + ) + log_msg = LogMessage( + run_id=run.id, + level="ERROR", + message=tb, + timestamp=datetime.now(), + ) + else: + log_msg = LogMessage( + run_id=run.id, + level="ERROR", + message=error, + timestamp=datetime.now(), + ) self.handle_log(log_msg) diff --git a/src/uipath/dev/ui/panels/run_details_panel.py b/src/uipath/dev/ui/panels/run_details_panel.py index 3bf9999..9ff17fe 100644 --- a/src/uipath/dev/ui/panels/run_details_panel.py +++ b/src/uipath/dev/ui/panels/run_details_panel.py @@ -137,7 +137,7 @@ def compose(self) -> ComposeResult: yield Button( "⏭ Continue", id="debug-continue-btn", - variant="default", + variant="success", classes="action-btn", ) yield Button( @@ -159,8 +159,6 @@ def update_run(self, run: ExecutionRun): def show_run(self, run: ExecutionRun): """Display traces and logs for a specific run.""" - self.update_debug_controls_visibility(run.debug) - # Populate run details tab self._show_run_details(run) @@ -178,11 +176,14 @@ def switch_tab(self, tab_id: str) -> None: tabbed = self.query_one(TabbedContent) tabbed.active = tab_id - def update_debug_controls_visibility(self, show: bool): + def update_debug_controls_visibility(self, run: ExecutionRun): """Show or hide debug controls based on whether run is in debug mode.""" debug_controls = self.query_one("#debug-controls", Container) - if show: + if run.debug: debug_controls.remove_class("hidden") + is_enabled = run.status == "suspended" + for button in debug_controls.query(Button): + button.disabled = not is_enabled else: debug_controls.add_class("hidden") @@ -241,6 +242,7 @@ def _write_block( def _show_run_details(self, run: ExecutionRun): """Display detailed information about the run in the Details tab.""" + self.update_debug_controls_visibility(run) run_details_log = self.query_one("#run-details-log", RichLog) run_details_log.clear() diff --git a/src/uipath/dev/ui/styles/terminal.tcss b/src/uipath/dev/ui/styles/terminal.tcss index 4980c4a..f56ab81 100644 --- a/src/uipath/dev/ui/styles/terminal.tcss +++ b/src/uipath/dev/ui/styles/terminal.tcss @@ -154,13 +154,13 @@ TabPane { } .spans-tree-section { - width: 40%; + width: 50%; height: 100%; padding-right: 1; } .span-details-section { - width: 60%; + width: 50%; height: 100%; padding-left: 1; }