uipath-dev 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uipath/dev/__init__.py ADDED
@@ -0,0 +1,329 @@
1
+ """UiPath Dev Terminal Application."""
2
+
3
+ import asyncio
4
+ import json
5
+ import traceback
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any, Optional
9
+
10
+ import pyperclip # type: ignore[import-untyped]
11
+ from pydantic import BaseModel
12
+ from rich.traceback import Traceback
13
+ from textual import on
14
+ from textual.app import App, ComposeResult
15
+ from textual.binding import Binding
16
+ from textual.containers import Container, Horizontal
17
+ from textual.widgets import Button, Footer, Input, ListView, RichLog
18
+ from uipath.core.tracing import UiPathTraceManager
19
+ from uipath.runtime import (
20
+ UiPathExecuteOptions,
21
+ UiPathExecutionRuntime,
22
+ UiPathRuntimeFactory,
23
+ UiPathRuntimeStatus,
24
+ )
25
+ from uipath.runtime.errors import UiPathErrorContract, UiPathRuntimeError
26
+
27
+ from uipath.dev.components.details import RunDetailsPanel
28
+ from uipath.dev.components.history import RunHistoryPanel
29
+ from uipath.dev.components.new import NewRunPanel
30
+ from uipath.dev.models.execution import ExecutionRun
31
+ from uipath.dev.models.messages import LogMessage, TraceMessage
32
+
33
+ from ._utils._exporter import RunContextExporter
34
+ from ._utils._logger import patch_textual_stderr
35
+
36
+
37
+ class UiPathDeveloperConsole(App[Any]):
38
+ """UiPath debugging terminal interface."""
39
+
40
+ TITLE = "UiPath Debugging Terminal"
41
+ SUB_TITLE = "Interactive debugging interface for UiPath Python projects"
42
+ CSS_PATH = Path(__file__).parent / "_styles" / "terminal.tcss"
43
+
44
+ BINDINGS = [
45
+ Binding("q", "quit", "Quit"),
46
+ Binding("n", "new_run", "New"),
47
+ Binding("r", "execute_run", "Run"),
48
+ Binding("c", "copy", "Copy"),
49
+ Binding("h", "clear_history", "Clear History"),
50
+ Binding("escape", "cancel", "Cancel"),
51
+ ]
52
+
53
+ def __init__(
54
+ self,
55
+ runtime_factory: UiPathRuntimeFactory[Any],
56
+ trace_manager: UiPathTraceManager,
57
+ **kwargs,
58
+ ):
59
+ """Initialize the UiPath Dev Terminal App."""
60
+ self._stderr_write_fd: int = patch_textual_stderr(self._add_subprocess_log)
61
+
62
+ super().__init__(**kwargs)
63
+
64
+ self.initial_entrypoint: str = "main.py"
65
+ self.initial_input: str = '{\n "message": "Hello World"\n}'
66
+ self.runs: dict[str, ExecutionRun] = {}
67
+ self.runtime_factory = runtime_factory
68
+ self.trace_manager = trace_manager
69
+ self.trace_manager.add_span_exporter(
70
+ RunContextExporter(
71
+ on_trace=self._handle_trace_message,
72
+ on_log=self._handle_log_message,
73
+ ),
74
+ batch=False,
75
+ )
76
+
77
+ def compose(self) -> ComposeResult:
78
+ """Compose the UI layout."""
79
+ with Horizontal():
80
+ # Left sidebar - run history
81
+ with Container(classes="run-history"):
82
+ yield RunHistoryPanel(id="history-panel")
83
+
84
+ # Main content area
85
+ with Container(classes="main-content"):
86
+ # New run panel (initially visible)
87
+ yield NewRunPanel(
88
+ id="new-run-panel",
89
+ classes="new-run-panel",
90
+ )
91
+
92
+ # Run details panel (initially hidden)
93
+ yield RunDetailsPanel(id="details-panel", classes="hidden")
94
+
95
+ yield Footer()
96
+
97
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
98
+ """Handle button press events."""
99
+ if event.button.id == "new-run-btn":
100
+ await self.action_new_run()
101
+ elif event.button.id == "execute-btn":
102
+ await self.action_execute_run()
103
+ elif event.button.id == "cancel-btn":
104
+ await self.action_cancel()
105
+
106
+ async def on_list_view_selected(self, event: ListView.Selected) -> None:
107
+ """Handle run selection from history."""
108
+ if event.list_view.id == "run-list" and event.item:
109
+ run_id = getattr(event.item, "run_id", None)
110
+ if run_id:
111
+ history_panel = self.query_one("#history-panel", RunHistoryPanel)
112
+ run = history_panel.get_run_by_id(run_id)
113
+ if run:
114
+ self._show_run_details(run)
115
+
116
+ @on(Input.Submitted, "#chat-input")
117
+ async def handle_chat_input(self, event: Input.Submitted) -> None:
118
+ """Handle user submitting text into the chat."""
119
+ user_text = event.value.strip()
120
+ if not user_text:
121
+ return
122
+
123
+ details_panel = self.query_one("#details-panel", RunDetailsPanel)
124
+ if details_panel and details_panel.current_run:
125
+ status = details_panel.current_run.status
126
+ if status == "running":
127
+ self.app.notify(
128
+ "Wait for agent response...", timeout=1.5, severity="warning"
129
+ )
130
+ return
131
+ if details_panel.current_run.status == "suspended":
132
+ details_panel.current_run.resume_data = {"message": user_text}
133
+ asyncio.create_task(self._execute_runtime(details_panel.current_run))
134
+ event.input.clear()
135
+
136
+ async def action_new_run(self) -> None:
137
+ """Show new run panel."""
138
+ new_panel = self.query_one("#new-run-panel")
139
+ details_panel = self.query_one("#details-panel")
140
+
141
+ new_panel.remove_class("hidden")
142
+ details_panel.add_class("hidden")
143
+
144
+ async def action_cancel(self) -> None:
145
+ """Cancel and return to new run view."""
146
+ await self.action_new_run()
147
+
148
+ async def action_execute_run(self) -> None:
149
+ """Execute a new run with UiPath runtime."""
150
+ new_run_panel = self.query_one("#new-run-panel", NewRunPanel)
151
+ entrypoint, input_data, conversational = new_run_panel.get_input_values()
152
+
153
+ if not entrypoint:
154
+ return
155
+
156
+ input: dict[str, Any] = {}
157
+ try:
158
+ input = json.loads(input_data)
159
+ except json.JSONDecodeError:
160
+ return
161
+
162
+ run = ExecutionRun(entrypoint, input, conversational)
163
+
164
+ self.runs[run.id] = run
165
+
166
+ self._add_run_in_history(run)
167
+
168
+ self._show_run_details(run)
169
+
170
+ if not run.conversational:
171
+ asyncio.create_task(self._execute_runtime(run))
172
+ else:
173
+ self._focus_chat_input()
174
+
175
+ async def action_clear_history(self) -> None:
176
+ """Clear run history."""
177
+ history_panel = self.query_one("#history-panel", RunHistoryPanel)
178
+ history_panel.clear_runs()
179
+ await self.action_new_run()
180
+
181
+ def action_copy(self) -> None:
182
+ """Copy content of currently focused RichLog to clipboard and notify."""
183
+ focused = self.app.focused
184
+ if isinstance(focused, RichLog):
185
+ clipboard_text = "\n".join(line.text for line in focused.lines)
186
+ pyperclip.copy(clipboard_text)
187
+ self.app.notify("Copied to clipboard!", timeout=1.5)
188
+ else:
189
+ self.app.notify("Nothing to copy here.", timeout=1.5, severity="warning")
190
+
191
+ async def _execute_runtime(self, run: ExecutionRun):
192
+ """Execute the script using UiPath runtime."""
193
+ try:
194
+ execution_input: Optional[dict[str, Any]] = {}
195
+ execution_options: UiPathExecuteOptions = UiPathExecuteOptions()
196
+ if run.status == "suspended":
197
+ execution_input = run.resume_data
198
+ execution_options.resume = True
199
+ self._add_info_log(run, f"Resuming execution: {run.entrypoint}")
200
+ else:
201
+ execution_input = run.input_data
202
+ self._add_info_log(run, f"Starting execution: {run.entrypoint}")
203
+
204
+ run.status = "running"
205
+ run.start_time = datetime.now()
206
+
207
+ runtime = self.runtime_factory.new_runtime(entrypoint=run.entrypoint)
208
+ execution_runtime = UiPathExecutionRuntime(
209
+ delegate=runtime, trace_manager=self.trace_manager, execution_id=run.id
210
+ )
211
+ result = await execution_runtime.execute(execution_input, execution_options)
212
+
213
+ if result is not None:
214
+ if (
215
+ result.status == UiPathRuntimeStatus.SUSPENDED.value
216
+ and result.resume
217
+ ):
218
+ run.status = "suspended"
219
+ else:
220
+ if result.output is None:
221
+ run.output_data = {}
222
+ elif isinstance(result.output, BaseModel):
223
+ run.output_data = result.output.model_dump()
224
+ else:
225
+ run.output_data = result.output
226
+ run.status = "completed"
227
+ if run.output_data:
228
+ self._add_info_log(run, f"Execution result: {run.output_data}")
229
+
230
+ self._add_info_log(run, "✅ Execution completed successfully")
231
+ run.end_time = datetime.now()
232
+
233
+ except UiPathRuntimeError as e:
234
+ self._add_error_log(run)
235
+ run.status = "failed"
236
+ run.end_time = datetime.now()
237
+ run.error = e.error_info
238
+
239
+ except Exception as e:
240
+ self._add_error_log(run)
241
+ run.status = "failed"
242
+ run.end_time = datetime.now()
243
+ run.error = UiPathErrorContract(
244
+ code="Unknown", title=str(e), detail=traceback.format_exc()
245
+ )
246
+
247
+ self._update_run_in_history(run)
248
+ self._update_run_details(run)
249
+
250
+ def _show_run_details(self, run: ExecutionRun):
251
+ """Show details panel for a specific run."""
252
+ # Hide new run panel, show details panel
253
+ new_panel = self.query_one("#new-run-panel")
254
+ details_panel = self.query_one("#details-panel", RunDetailsPanel)
255
+
256
+ new_panel.add_class("hidden")
257
+ details_panel.remove_class("hidden")
258
+
259
+ # Populate the details panel with run data
260
+ details_panel.update_run(run)
261
+
262
+ def _focus_chat_input(self):
263
+ """Focus the chat input box."""
264
+ details_panel = self.query_one("#details-panel", RunDetailsPanel)
265
+ details_panel.switch_tab("chat-tab")
266
+ chat_input = details_panel.query_one("#chat-input", Input)
267
+ chat_input.focus()
268
+
269
+ def _add_run_in_history(self, run: ExecutionRun):
270
+ """Add run to history panel."""
271
+ history_panel = self.query_one("#history-panel", RunHistoryPanel)
272
+ history_panel.add_run(run)
273
+
274
+ def _update_run_in_history(self, run: ExecutionRun):
275
+ """Update run display in history panel."""
276
+ history_panel = self.query_one("#history-panel", RunHistoryPanel)
277
+ history_panel.update_run(run)
278
+
279
+ def _update_run_details(self, run: ExecutionRun):
280
+ """Update the displayed run information."""
281
+ details_panel = self.query_one("#details-panel", RunDetailsPanel)
282
+ details_panel.update_run_details(run)
283
+
284
+ def _handle_trace_message(self, trace_msg: TraceMessage):
285
+ """Handle trace message from exporter."""
286
+ run = self.runs[trace_msg.run_id]
287
+ for i, existing_trace in enumerate(run.traces):
288
+ if existing_trace.span_id == trace_msg.span_id:
289
+ run.traces[i] = trace_msg
290
+ break
291
+ else:
292
+ run.traces.append(trace_msg)
293
+
294
+ details_panel = self.query_one("#details-panel", RunDetailsPanel)
295
+ details_panel.add_trace(trace_msg)
296
+
297
+ def _handle_log_message(self, log_msg: LogMessage):
298
+ """Handle log message from exporter."""
299
+ self.runs[log_msg.run_id].logs.append(log_msg)
300
+ details_panel = self.query_one("#details-panel", RunDetailsPanel)
301
+ details_panel.add_log(log_msg)
302
+
303
+ def _add_info_log(self, run: ExecutionRun, message: str):
304
+ """Add info log to run."""
305
+ timestamp = datetime.now()
306
+ log_msg = LogMessage(run.id, "INFO", message, timestamp)
307
+ self._handle_log_message(log_msg)
308
+
309
+ def _add_error_log(self, run: ExecutionRun):
310
+ """Add error log to run."""
311
+ timestamp = datetime.now()
312
+ tb = Traceback(
313
+ show_locals=False,
314
+ max_frames=4,
315
+ )
316
+ log_msg = LogMessage(run.id, "ERROR", tb, timestamp)
317
+ self._handle_log_message(log_msg)
318
+
319
+ def _add_subprocess_log(self, level: str, message: str) -> None:
320
+ """Handle a stderr line coming from subprocesses."""
321
+
322
+ def add_log() -> None:
323
+ details_panel = self.query_one("#details-panel", RunDetailsPanel)
324
+ run = getattr(details_panel, "current_run", None)
325
+ if run:
326
+ log_msg = LogMessage(run.id, level, message, datetime.now())
327
+ self._handle_log_message(log_msg)
328
+
329
+ self.call_from_thread(add_log)
File without changes
@@ -0,0 +1,64 @@
1
+ """Minimal demo script to run UiPathDevTerminal with mock runtimes."""
2
+
3
+ import asyncio
4
+ from typing import Any, Optional
5
+
6
+ from uipath.runtime import (
7
+ UiPathBaseRuntime,
8
+ UiPathExecuteOptions,
9
+ UiPathRuntimeFactory,
10
+ UiPathRuntimeResult,
11
+ UiPathRuntimeStatus,
12
+ )
13
+ from uipath.runtime.schema import UiPathRuntimeSchema
14
+
15
+
16
+ class MockRuntime(UiPathBaseRuntime):
17
+ """A simple mock runtime that echoes its input."""
18
+
19
+ async def get_schema(self) -> UiPathRuntimeSchema:
20
+ return UiPathRuntimeSchema(
21
+ filePath="default",
22
+ uniqueId="mock-runtime",
23
+ type="agent",
24
+ input={
25
+ "type": "object",
26
+ "properties": {"message": {"type": "string"}},
27
+ "required": ["message"],
28
+ },
29
+ output={
30
+ "type": "object",
31
+ "properties": {"result": {"type": "string"}},
32
+ "required": ["result"],
33
+ },
34
+ )
35
+
36
+ async def execute(
37
+ self,
38
+ input: Optional[dict[str, Any]] = None,
39
+ options: Optional[UiPathExecuteOptions] = None,
40
+ ) -> UiPathRuntimeResult:
41
+ payload = input or {}
42
+ # Simulate some async work
43
+ await asyncio.sleep(0.2)
44
+ return UiPathRuntimeResult(
45
+ output={"result": f"Mock runtime got: {payload!r}"},
46
+ status=UiPathRuntimeStatus.SUCCESSFUL,
47
+ )
48
+
49
+ async def cleanup(self) -> None:
50
+ # Nothing to clean up in this mock
51
+ pass
52
+
53
+
54
+ # 2) Mock runtime factory
55
+ class MockRuntimeFactory(UiPathRuntimeFactory[MockRuntime]):
56
+ """Runtime factory compatible with UiPathDevTerminal expectations."""
57
+
58
+ # This is the method the Textual app calls here:
59
+ # runtime = self.runtime_factory.new_runtime(entrypoint=run.entrypoint)
60
+ def new_runtime(self, entrypoint: str) -> MockRuntime:
61
+ return MockRuntime()
62
+
63
+ def discover_runtimes(self) -> list[MockRuntime]:
64
+ return []
@@ -0,0 +1,15 @@
1
+ from uipath.core.tracing import UiPathTraceManager
2
+
3
+ from uipath.dev import UiPathDeveloperConsole
4
+ from uipath.dev._demo.mock_runtime import MockRuntimeFactory
5
+
6
+
7
+ def main():
8
+ trace_manager = UiPathTraceManager()
9
+ factory = MockRuntimeFactory()
10
+ app = UiPathDeveloperConsole(runtime_factory=factory, trace_manager=trace_manager)
11
+ app.run()
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()
@@ -0,0 +1,261 @@
1
+ Screen {
2
+ layout: horizontal;
3
+ }
4
+
5
+ .run-history {
6
+ width: 30%;
7
+ min-width: 25;
8
+ padding-right: 1;
9
+ }
10
+
11
+ .run-list {
12
+ height: 1fr;
13
+ margin-bottom: 1;
14
+ }
15
+
16
+ .run-item {
17
+ padding: 0 1;
18
+ }
19
+
20
+ .run-running {
21
+ border-left: solid #ffaa00;
22
+ color: #ffaa00;
23
+ }
24
+
25
+ .run-suspended {
26
+ border-left: solid #00FFFF;
27
+ color: #e0e0e0;
28
+ }
29
+
30
+ .run-pending {
31
+ border-left: solid grey;
32
+ color: #e0e0e0;
33
+ }
34
+
35
+ .run-completed {
36
+ border-left: solid #00ff88;
37
+ color: #e0e0e0;
38
+ }
39
+
40
+ .run-failed {
41
+ border-left: solid #ff4444;
42
+ color: #ff4444;
43
+ }
44
+
45
+ .new-run-btn {
46
+ width: 100%;
47
+ margin-bottom: 1;
48
+ border: none;
49
+ text-style: bold;
50
+ }
51
+ .main-content {
52
+ width: 70%;
53
+ padding-left: 1;
54
+ }
55
+
56
+ .new-run-title {
57
+ text-style: bold;
58
+ padding: 0 1;
59
+ height: 1;
60
+ margin-bottom: 0;
61
+ }
62
+
63
+ .new-run-panel {
64
+ height: 100%;
65
+ }
66
+
67
+ .field-label {
68
+ text-style: bold;
69
+ }
70
+
71
+ .run-actions {
72
+ dock: bottom;
73
+ height: auto;
74
+ align: left middle;
75
+ }
76
+
77
+ .action-btn {
78
+ margin-right: 2;
79
+ min-width: 8;
80
+ border: none;
81
+ text-style: bold;
82
+ }
83
+
84
+ .details-content {
85
+ height: 1fr;
86
+ }
87
+
88
+ .traces-section,
89
+ .logs-section {
90
+ width: 50%;
91
+ height: 100%;
92
+ }
93
+
94
+ .traces-section {
95
+ width: 50%;
96
+ }
97
+
98
+ .logs-section {
99
+ width: 50%;
100
+ }
101
+
102
+ .detail-log {
103
+ height: 1fr;
104
+ padding: 1;
105
+ padding-top: 0;
106
+ }
107
+
108
+ .status-running {
109
+ background: #ffaa00;
110
+ color: #000000;
111
+ border: solid #ffaa00;
112
+ }
113
+
114
+ .status-success {
115
+ background: #00ff88;
116
+ color: #000000;
117
+ border: solid #00ff88;
118
+ }
119
+
120
+ .status-error {
121
+ background: #ff4444;
122
+ color: #ffffff;
123
+ border: solid #ff4444;
124
+ }
125
+
126
+ .hidden {
127
+ display: none;
128
+ }
129
+
130
+ Footer {
131
+ margin-top:1;
132
+ height: auto;
133
+ dock: bottom;
134
+ }
135
+
136
+ TabbedContent {
137
+ height: 100%;
138
+ }
139
+
140
+ TabPane {
141
+ height: 100%;
142
+ padding: 0;
143
+ }
144
+
145
+ .traces-content {
146
+ height: 100%;
147
+ }
148
+
149
+ .spans-tree-section {
150
+ width: 40%;
151
+ height: 100%;
152
+ padding-right: 1;
153
+ }
154
+
155
+ .span-details-section {
156
+ width: 60%;
157
+ height: 100%;
158
+ padding-left: 1;
159
+ }
160
+
161
+ .spans-tree {
162
+ height: 100%;
163
+ padding: 1;
164
+ padding-top: 0;
165
+ }
166
+
167
+ Label {
168
+ margin: 1 1;
169
+ width: 100%;
170
+ height: 100%;
171
+ border: tall $primary;
172
+ content-align: center middle;
173
+ }
174
+
175
+ ContentSwitcher {
176
+ height: 1fr;
177
+ }
178
+
179
+ SpanDetailsDisplay {
180
+ height: 100%;
181
+ }
182
+
183
+ #span-details-display {
184
+ height: 100%;
185
+ }
186
+
187
+ #span-details {
188
+ height: 100%;
189
+ }
190
+
191
+ .new-run-panel {
192
+ height: 100%;
193
+ }
194
+
195
+ .new-run-title {
196
+ text-style: bold;
197
+ padding: 0 1;
198
+ height: 2;
199
+ content-align: left middle;
200
+ }
201
+
202
+ .field-label {
203
+ text-style: bold;
204
+ margin: 1 0;
205
+ }
206
+
207
+ .script-input {
208
+ height: 3;
209
+ }
210
+
211
+ .json-input {
212
+ margin-top: 1;
213
+ height: auto;
214
+ }
215
+
216
+ .run-actions {
217
+ height: auto;
218
+ padding: 1;
219
+ }
220
+
221
+ .action-btn {
222
+ min-width: 10;
223
+ padding: 0 2;
224
+ text-style: bold;
225
+ border: none;
226
+ }
227
+
228
+ TextArea.invalid {
229
+ border: tall red;
230
+ }
231
+
232
+
233
+ Prompt {
234
+ border: wide $primary-background;
235
+ background: $surface;
236
+ color: $text;
237
+ margin-right: 8;
238
+ margin-left: 1;
239
+ padding: 1 1 0 1;
240
+ }
241
+ Response, Tool {
242
+ border: wide $primary-background;
243
+ background: $surface;
244
+ color: $text;
245
+ margin: 1;
246
+ margin-left: 8;
247
+ padding: 1 1 0 1;
248
+ }
249
+
250
+ #chat-container{
251
+ background: $surface;
252
+ }
253
+
254
+ #chat-input{
255
+ dock: bottom;
256
+ margin: 1;
257
+ }
258
+
259
+ Checkbox{
260
+ margin-top: 1;
261
+ }