uipath 2.1.27__py3-none-any.whl → 2.1.28__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/_cli/__init__.py +2 -0
- uipath/_cli/_dev/_terminal/__init__.py +227 -0
- uipath/_cli/_dev/_terminal/_components/_details.py +421 -0
- uipath/_cli/_dev/_terminal/_components/_history.py +57 -0
- uipath/_cli/_dev/_terminal/_components/_new.py +133 -0
- uipath/_cli/_dev/_terminal/_models/_execution.py +43 -0
- uipath/_cli/_dev/_terminal/_models/_messages.py +65 -0
- uipath/_cli/_dev/_terminal/_styles/terminal.tcss +361 -0
- uipath/_cli/_dev/_terminal/_traces/_exporter.py +119 -0
- uipath/_cli/_dev/_terminal/_traces/_logger.py +32 -0
- uipath/_cli/_runtime/_contracts.py +94 -4
- uipath/_cli/_runtime/_logging.py +20 -13
- uipath/_cli/_runtime/_runtime.py +2 -14
- uipath/_cli/cli_dev.py +44 -0
- uipath/_cli/cli_run.py +33 -28
- uipath/_cli/middlewares.py +1 -0
- uipath/telemetry/_track.py +2 -2
- {uipath-2.1.27.dist-info → uipath-2.1.28.dist-info}/METADATA +3 -1
- {uipath-2.1.27.dist-info → uipath-2.1.28.dist-info}/RECORD +22 -12
- {uipath-2.1.27.dist-info → uipath-2.1.28.dist-info}/WHEEL +0 -0
- {uipath-2.1.27.dist-info → uipath-2.1.28.dist-info}/entry_points.txt +0 -0
- {uipath-2.1.27.dist-info → uipath-2.1.28.dist-info}/licenses/LICENSE +0 -0
uipath/_cli/__init__.py
CHANGED
@@ -5,6 +5,7 @@ import click
|
|
5
5
|
|
6
6
|
from .cli_auth import auth as auth
|
7
7
|
from .cli_deploy import deploy as deploy # type: ignore
|
8
|
+
from .cli_dev import dev as dev
|
8
9
|
from .cli_eval import eval as eval # type: ignore
|
9
10
|
from .cli_init import init as init # type: ignore
|
10
11
|
from .cli_invoke import invoke as invoke # type: ignore
|
@@ -69,3 +70,4 @@ cli.add_command(invoke)
|
|
69
70
|
cli.add_command(push)
|
70
71
|
cli.add_command(pull)
|
71
72
|
cli.add_command(eval)
|
73
|
+
cli.add_command(dev)
|
@@ -0,0 +1,227 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
from datetime import datetime
|
4
|
+
from os import environ as env
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Any, Dict
|
7
|
+
from uuid import uuid4
|
8
|
+
|
9
|
+
from textual.app import App, ComposeResult
|
10
|
+
from textual.binding import Binding
|
11
|
+
from textual.containers import Container, Horizontal
|
12
|
+
from textual.widgets import Button, ListView
|
13
|
+
|
14
|
+
from ..._runtime._contracts import (
|
15
|
+
UiPathRuntimeContext,
|
16
|
+
UiPathRuntimeFactory,
|
17
|
+
)
|
18
|
+
from ._components._details import RunDetailsPanel
|
19
|
+
from ._components._history import RunHistoryPanel
|
20
|
+
from ._components._new import NewRunPanel
|
21
|
+
from ._models._execution import ExecutionRun
|
22
|
+
from ._models._messages import LogMessage, TraceMessage
|
23
|
+
from ._traces._exporter import RunContextExporter
|
24
|
+
from ._traces._logger import RunContextLogHandler
|
25
|
+
|
26
|
+
|
27
|
+
class UiPathDevTerminal(App[Any]):
|
28
|
+
"""UiPath development terminal interface."""
|
29
|
+
|
30
|
+
CSS_PATH = Path(__file__).parent / "_styles" / "terminal.tcss"
|
31
|
+
|
32
|
+
BINDINGS = [
|
33
|
+
Binding("q", "quit", "Quit"),
|
34
|
+
Binding("n", "new_run", "New Run"),
|
35
|
+
Binding("r", "execute_run", "Execute"),
|
36
|
+
Binding("c", "clear_history", "Clear History"),
|
37
|
+
Binding("escape", "cancel", "Cancel"),
|
38
|
+
]
|
39
|
+
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
runtime_factory: UiPathRuntimeFactory[Any, Any],
|
43
|
+
**kwargs,
|
44
|
+
):
|
45
|
+
super().__init__(**kwargs)
|
46
|
+
|
47
|
+
self.initial_entrypoint: str = "main.py"
|
48
|
+
self.initial_input: str = '{\n "message": "Hello World"\n}'
|
49
|
+
self.runs: Dict[str, ExecutionRun] = {}
|
50
|
+
self.runtime_factory = runtime_factory
|
51
|
+
self.runtime_factory.add_span_exporter(
|
52
|
+
RunContextExporter(
|
53
|
+
on_trace=self._handle_trace_message,
|
54
|
+
on_log=self._handle_log_message,
|
55
|
+
),
|
56
|
+
batch=False,
|
57
|
+
)
|
58
|
+
|
59
|
+
def compose(self) -> ComposeResult:
|
60
|
+
with Horizontal():
|
61
|
+
# Left sidebar - run history
|
62
|
+
with Container(classes="run-history"):
|
63
|
+
yield RunHistoryPanel(id="history-panel")
|
64
|
+
|
65
|
+
# Main content area
|
66
|
+
with Container(classes="main-content"):
|
67
|
+
# New run panel (initially visible)
|
68
|
+
yield NewRunPanel(
|
69
|
+
id="new-run-panel",
|
70
|
+
classes="new-run-panel",
|
71
|
+
)
|
72
|
+
|
73
|
+
# Run details panel (initially hidden)
|
74
|
+
yield RunDetailsPanel(id="details-panel", classes="hidden")
|
75
|
+
|
76
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
77
|
+
"""Handle button press events."""
|
78
|
+
if event.button.id == "new-run-btn":
|
79
|
+
await self.action_new_run()
|
80
|
+
elif event.button.id == "execute-btn":
|
81
|
+
await self.action_execute_run()
|
82
|
+
elif event.button.id == "cancel-btn":
|
83
|
+
await self.action_cancel()
|
84
|
+
|
85
|
+
async def on_list_view_selected(self, event: ListView.Selected) -> None:
|
86
|
+
"""Handle run selection from history."""
|
87
|
+
if event.list_view.id == "run-list" and event.item:
|
88
|
+
run_id = getattr(event.item, "run_id", None)
|
89
|
+
if run_id:
|
90
|
+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
|
91
|
+
run = history_panel.get_run_by_id(run_id)
|
92
|
+
if run:
|
93
|
+
self._show_run_details(run)
|
94
|
+
|
95
|
+
async def action_new_run(self) -> None:
|
96
|
+
"""Show new run panel."""
|
97
|
+
new_panel = self.query_one("#new-run-panel")
|
98
|
+
details_panel = self.query_one("#details-panel")
|
99
|
+
|
100
|
+
new_panel.remove_class("hidden")
|
101
|
+
details_panel.add_class("hidden")
|
102
|
+
|
103
|
+
async def action_cancel(self) -> None:
|
104
|
+
"""Cancel and return to new run view."""
|
105
|
+
await self.action_new_run()
|
106
|
+
|
107
|
+
async def action_execute_run(self) -> None:
|
108
|
+
"""Execute a new run with UiPath runtime."""
|
109
|
+
new_run_panel = self.query_one("#new-run-panel", NewRunPanel)
|
110
|
+
entrypoint, input_data = new_run_panel.get_input_values()
|
111
|
+
|
112
|
+
if not entrypoint:
|
113
|
+
return
|
114
|
+
|
115
|
+
try:
|
116
|
+
json.loads(input_data)
|
117
|
+
except json.JSONDecodeError:
|
118
|
+
return
|
119
|
+
|
120
|
+
run = ExecutionRun(entrypoint, input_data)
|
121
|
+
self.runs[run.id] = run
|
122
|
+
|
123
|
+
self._add_run_in_history(run)
|
124
|
+
|
125
|
+
self._show_run_details(run)
|
126
|
+
|
127
|
+
asyncio.create_task(self._execute_runtime(run))
|
128
|
+
|
129
|
+
async def action_clear_history(self) -> None:
|
130
|
+
"""Clear run history."""
|
131
|
+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
|
132
|
+
history_panel.clear_runs()
|
133
|
+
await self.action_new_run()
|
134
|
+
|
135
|
+
async def _execute_runtime(self, run: ExecutionRun):
|
136
|
+
"""Execute the script using UiPath runtime."""
|
137
|
+
try:
|
138
|
+
context: UiPathRuntimeContext = self.runtime_factory.new_context(
|
139
|
+
entrypoint=run.entrypoint,
|
140
|
+
input=run.input_data,
|
141
|
+
trace_id=str(uuid4()),
|
142
|
+
execution_id=run.id,
|
143
|
+
logs_min_level=env.get("LOG_LEVEL", "INFO"),
|
144
|
+
log_handler=RunContextLogHandler(
|
145
|
+
run_id=run.id, on_log=self._handle_log_message
|
146
|
+
),
|
147
|
+
)
|
148
|
+
|
149
|
+
self._add_info_log(run, f"Starting execution: {run.entrypoint}")
|
150
|
+
|
151
|
+
result = await self.runtime_factory.execute_in_root_span(context)
|
152
|
+
|
153
|
+
if result is not None:
|
154
|
+
run.output_data = json.dumps(result.output)
|
155
|
+
if run.output_data:
|
156
|
+
self._add_info_log(run, f"Execution result: {run.output_data}")
|
157
|
+
|
158
|
+
self._add_info_log(run, "✅ Execution completed successfully")
|
159
|
+
run.status = "completed"
|
160
|
+
run.end_time = datetime.now()
|
161
|
+
|
162
|
+
except Exception as e:
|
163
|
+
error_msg = f"Execution failed: {str(e)}"
|
164
|
+
self._add_error_log(run, error_msg)
|
165
|
+
run.status = "failed"
|
166
|
+
run.end_time = datetime.now()
|
167
|
+
|
168
|
+
self._update_run_in_history(run)
|
169
|
+
self._update_run_details(run)
|
170
|
+
|
171
|
+
def _show_run_details(self, run: ExecutionRun):
|
172
|
+
"""Show details panel for a specific run."""
|
173
|
+
# Hide new run panel, show details panel
|
174
|
+
new_panel = self.query_one("#new-run-panel")
|
175
|
+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
|
176
|
+
|
177
|
+
new_panel.add_class("hidden")
|
178
|
+
details_panel.remove_class("hidden")
|
179
|
+
|
180
|
+
# Populate the details panel with run data
|
181
|
+
details_panel.update_run(run)
|
182
|
+
|
183
|
+
def _add_run_in_history(self, run: ExecutionRun):
|
184
|
+
"""Add run to history panel."""
|
185
|
+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
|
186
|
+
history_panel.add_run(run)
|
187
|
+
|
188
|
+
def _update_run_in_history(self, run: ExecutionRun):
|
189
|
+
"""Update run display in history panel."""
|
190
|
+
history_panel = self.query_one("#history-panel", RunHistoryPanel)
|
191
|
+
history_panel.update_run(run)
|
192
|
+
|
193
|
+
def _update_run_details(self, run: ExecutionRun):
|
194
|
+
"""Update the displayed run information."""
|
195
|
+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
|
196
|
+
details_panel.update_run_details(run)
|
197
|
+
|
198
|
+
def _handle_trace_message(self, trace_msg: TraceMessage):
|
199
|
+
"""Handle trace message from exporter."""
|
200
|
+
run = self.runs[trace_msg.run_id]
|
201
|
+
for i, existing_trace in enumerate(run.traces):
|
202
|
+
if existing_trace.span_id == trace_msg.span_id:
|
203
|
+
run.traces[i] = trace_msg
|
204
|
+
break
|
205
|
+
else:
|
206
|
+
run.traces.append(trace_msg)
|
207
|
+
|
208
|
+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
|
209
|
+
details_panel.add_trace(trace_msg)
|
210
|
+
|
211
|
+
def _handle_log_message(self, log_msg: LogMessage):
|
212
|
+
"""Handle log message from exporter."""
|
213
|
+
self.runs[log_msg.run_id].logs.append(log_msg)
|
214
|
+
details_panel = self.query_one("#details-panel", RunDetailsPanel)
|
215
|
+
details_panel.add_log(log_msg)
|
216
|
+
|
217
|
+
def _add_info_log(self, run: ExecutionRun, message: str):
|
218
|
+
"""Add info log to run."""
|
219
|
+
timestamp = datetime.now()
|
220
|
+
log_msg = LogMessage(run.id, "INFO", message, timestamp)
|
221
|
+
self._handle_log_message(log_msg)
|
222
|
+
|
223
|
+
def _add_error_log(self, run: ExecutionRun, message: str):
|
224
|
+
"""Add error log to run."""
|
225
|
+
timestamp = datetime.now()
|
226
|
+
log_msg = LogMessage(run.id, "ERROR", message, timestamp)
|
227
|
+
self._handle_log_message(log_msg)
|
@@ -0,0 +1,421 @@
|
|
1
|
+
from typing import Dict, List, Optional
|
2
|
+
|
3
|
+
from textual.app import ComposeResult
|
4
|
+
from textual.containers import Container, Horizontal, Vertical
|
5
|
+
from textual.reactive import reactive
|
6
|
+
from textual.widgets import RichLog, TabbedContent, TabPane, Tree
|
7
|
+
from textual.widgets.tree import TreeNode
|
8
|
+
|
9
|
+
from .._models._execution import ExecutionRun
|
10
|
+
from .._models._messages import LogMessage, TraceMessage
|
11
|
+
|
12
|
+
|
13
|
+
class SpanDetailsDisplay(Container):
|
14
|
+
"""Widget to display details of a selected span."""
|
15
|
+
|
16
|
+
def compose(self) -> ComposeResult:
|
17
|
+
yield RichLog(
|
18
|
+
id="span-details",
|
19
|
+
max_lines=1000,
|
20
|
+
highlight=True,
|
21
|
+
markup=True,
|
22
|
+
classes="detail-log",
|
23
|
+
)
|
24
|
+
|
25
|
+
def show_span_details(self, trace_msg: TraceMessage):
|
26
|
+
"""Display detailed information about a trace span."""
|
27
|
+
details_log = self.query_one("#span-details", RichLog)
|
28
|
+
details_log.clear()
|
29
|
+
|
30
|
+
# Format span details
|
31
|
+
details_log.write(f"[bold cyan]Span: {trace_msg.span_name}[/bold cyan]")
|
32
|
+
details_log.write(f"[dim]Trace ID: {trace_msg.trace_id}[/dim]")
|
33
|
+
details_log.write(f"[dim]Span ID: {trace_msg.span_id}[/dim]")
|
34
|
+
details_log.write(f"[dim]Run ID: {trace_msg.run_id}[/dim]")
|
35
|
+
|
36
|
+
if trace_msg.parent_span_id:
|
37
|
+
details_log.write(f"[dim]Parent Span: {trace_msg.parent_span_id}[/dim]")
|
38
|
+
|
39
|
+
details_log.write("") # Empty line
|
40
|
+
|
41
|
+
# Status with color
|
42
|
+
color_map = {
|
43
|
+
"started": "blue",
|
44
|
+
"running": "yellow",
|
45
|
+
"completed": "green",
|
46
|
+
"failed": "red",
|
47
|
+
"error": "red",
|
48
|
+
}
|
49
|
+
color = color_map.get(trace_msg.status.lower(), "white")
|
50
|
+
details_log.write(f"Status: [{color}]{trace_msg.status.upper()}[/{color}]")
|
51
|
+
|
52
|
+
# Timestamps
|
53
|
+
details_log.write(
|
54
|
+
f"Started: [dim]{trace_msg.timestamp.strftime('%H:%M:%S.%f')[:-3]}[/dim]"
|
55
|
+
)
|
56
|
+
|
57
|
+
if trace_msg.duration_ms is not None:
|
58
|
+
details_log.write(
|
59
|
+
f"Duration: [yellow]{trace_msg.duration_ms:.2f}ms[/yellow]"
|
60
|
+
)
|
61
|
+
|
62
|
+
# Additional attributes if available
|
63
|
+
if trace_msg.attributes:
|
64
|
+
details_log.write("")
|
65
|
+
details_log.write("[bold]Attributes:[/bold]")
|
66
|
+
for key, value in trace_msg.attributes.items():
|
67
|
+
details_log.write(f" {key}: {value}")
|
68
|
+
|
69
|
+
|
70
|
+
class RunDetailsPanel(Container):
|
71
|
+
"""Panel showing traces and logs for selected run with tabbed interface."""
|
72
|
+
|
73
|
+
current_run: reactive[Optional[ExecutionRun]] = reactive(None)
|
74
|
+
|
75
|
+
def __init__(self, **kwargs):
|
76
|
+
super().__init__(**kwargs)
|
77
|
+
self.span_tree_nodes = {} # Map span_id to tree nodes
|
78
|
+
self.current_run = None # Store reference to current run
|
79
|
+
|
80
|
+
def compose(self) -> ComposeResult:
|
81
|
+
with TabbedContent():
|
82
|
+
# Run details tab
|
83
|
+
with TabPane("Details", id="run-tab"):
|
84
|
+
yield RichLog(
|
85
|
+
id="run-details-log",
|
86
|
+
max_lines=1000,
|
87
|
+
highlight=True,
|
88
|
+
markup=True,
|
89
|
+
classes="detail-log",
|
90
|
+
)
|
91
|
+
|
92
|
+
# Traces tab
|
93
|
+
with TabPane("Traces", id="traces-tab"):
|
94
|
+
with Horizontal(classes="traces-content"):
|
95
|
+
# Left side - Span tree
|
96
|
+
with Vertical(
|
97
|
+
classes="spans-tree-section", id="spans-tree-container"
|
98
|
+
):
|
99
|
+
yield Tree("Trace", id="spans-tree", classes="spans-tree")
|
100
|
+
|
101
|
+
# Right side - Span details
|
102
|
+
with Vertical(classes="span-details-section"):
|
103
|
+
yield SpanDetailsDisplay(id="span-details-display")
|
104
|
+
|
105
|
+
# Logs tab
|
106
|
+
with TabPane("Logs", id="logs-tab"):
|
107
|
+
yield RichLog(
|
108
|
+
id="logs-log",
|
109
|
+
max_lines=1000,
|
110
|
+
highlight=True,
|
111
|
+
markup=True,
|
112
|
+
classes="detail-log",
|
113
|
+
)
|
114
|
+
|
115
|
+
def watch_current_run(
|
116
|
+
self, old_value: Optional[ExecutionRun], new_value: Optional[ExecutionRun]
|
117
|
+
):
|
118
|
+
"""Watch for changes to the current run."""
|
119
|
+
if new_value is not None:
|
120
|
+
if old_value != new_value:
|
121
|
+
self.current_run = new_value
|
122
|
+
self.show_run(new_value)
|
123
|
+
|
124
|
+
def update_run(self, run: ExecutionRun):
|
125
|
+
"""Update the displayed run information."""
|
126
|
+
self.current_run = run
|
127
|
+
|
128
|
+
def show_run(self, run: ExecutionRun):
|
129
|
+
"""Display traces and logs for a specific run."""
|
130
|
+
# Populate run details tab
|
131
|
+
self._show_run_details(run)
|
132
|
+
|
133
|
+
# Populate logs - convert string logs to display format
|
134
|
+
logs_log = self.query_one("#logs-log", RichLog)
|
135
|
+
logs_log.clear()
|
136
|
+
for log in run.logs:
|
137
|
+
self.add_log(log)
|
138
|
+
|
139
|
+
# Clear and rebuild traces tree using TraceMessage objects
|
140
|
+
self._rebuild_spans_tree()
|
141
|
+
|
142
|
+
def _show_run_details(self, run: ExecutionRun):
|
143
|
+
"""Display detailed information about the run in the Details tab."""
|
144
|
+
run_details_log = self.query_one("#run-details-log", RichLog)
|
145
|
+
run_details_log.clear()
|
146
|
+
|
147
|
+
# Run header
|
148
|
+
run_details_log.write(f"[bold cyan]Run ID: {run.id}[/bold cyan]")
|
149
|
+
run_details_log.write("")
|
150
|
+
|
151
|
+
# Run status with color
|
152
|
+
status_color_map = {
|
153
|
+
"started": "blue",
|
154
|
+
"running": "yellow",
|
155
|
+
"completed": "green",
|
156
|
+
"failed": "red",
|
157
|
+
"error": "red",
|
158
|
+
}
|
159
|
+
status = getattr(run, "status", "unknown")
|
160
|
+
color = status_color_map.get(status.lower(), "white")
|
161
|
+
run_details_log.write(
|
162
|
+
f"[bold]Status:[/bold] [{color}]{status.upper()}[/{color}]"
|
163
|
+
)
|
164
|
+
|
165
|
+
# Timestamps
|
166
|
+
if hasattr(run, "start_time") and run.start_time:
|
167
|
+
run_details_log.write(
|
168
|
+
f"[bold]Started:[/bold] [dim]{run.start_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}[/dim]"
|
169
|
+
)
|
170
|
+
|
171
|
+
if hasattr(run, "end_time") and run.end_time:
|
172
|
+
run_details_log.write(
|
173
|
+
f"[bold]Ended:[/bold] [dim]{run.end_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}[/dim]"
|
174
|
+
)
|
175
|
+
|
176
|
+
# Duration
|
177
|
+
if hasattr(run, "duration_ms") and run.duration_ms is not None:
|
178
|
+
run_details_log.write(
|
179
|
+
f"[bold]Duration:[/bold] [yellow]{run.duration_ms:.2f}ms[/yellow]"
|
180
|
+
)
|
181
|
+
elif (
|
182
|
+
hasattr(run, "start_time")
|
183
|
+
and hasattr(run, "end_time")
|
184
|
+
and run.start_time
|
185
|
+
and run.end_time
|
186
|
+
):
|
187
|
+
duration = (run.end_time - run.start_time).total_seconds() * 1000
|
188
|
+
run_details_log.write(
|
189
|
+
f"[bold]Duration:[/bold] [yellow]{duration:.2f}ms[/yellow]"
|
190
|
+
)
|
191
|
+
|
192
|
+
run_details_log.write("")
|
193
|
+
|
194
|
+
# Input section
|
195
|
+
if hasattr(run, "input_data") and run.input_data is not None:
|
196
|
+
run_details_log.write("[bold green]INPUT:[/bold green]")
|
197
|
+
run_details_log.write("[dim]" + "=" * 50 + "[/dim]")
|
198
|
+
|
199
|
+
# Handle different input types
|
200
|
+
if isinstance(run.input_data, str):
|
201
|
+
run_details_log.write(run.input_data)
|
202
|
+
elif isinstance(run.input_data, dict):
|
203
|
+
import json
|
204
|
+
|
205
|
+
run_details_log.write(json.dumps(run.input_data, indent=2))
|
206
|
+
else:
|
207
|
+
run_details_log.write(str(run.input_data))
|
208
|
+
|
209
|
+
run_details_log.write("")
|
210
|
+
|
211
|
+
# Output section
|
212
|
+
if hasattr(run, "output_data") and run.output_data is not None:
|
213
|
+
run_details_log.write("[bold magenta]OUTPUT:[/bold magenta]")
|
214
|
+
run_details_log.write("[dim]" + "=" * 50 + "[/dim]")
|
215
|
+
|
216
|
+
# Handle different output types
|
217
|
+
if isinstance(run.output_data, str):
|
218
|
+
run_details_log.write(run.output_data)
|
219
|
+
elif isinstance(run.output_data, dict):
|
220
|
+
import json
|
221
|
+
|
222
|
+
run_details_log.write(json.dumps(run.output_data, indent=2))
|
223
|
+
else:
|
224
|
+
run_details_log.write(str(run.output_data))
|
225
|
+
|
226
|
+
run_details_log.write("")
|
227
|
+
|
228
|
+
# Error section (if applicable)
|
229
|
+
if hasattr(run, "error") and run.error:
|
230
|
+
run_details_log.write("[bold red]ERROR:[/bold red]")
|
231
|
+
run_details_log.write("[dim]" + "=" * 50 + "[/dim]")
|
232
|
+
run_details_log.write(f"[red]{run.error}[/red]")
|
233
|
+
run_details_log.write("")
|
234
|
+
|
235
|
+
# Additional metadata
|
236
|
+
run_details_log.write("[bold]METADATA:[/bold]")
|
237
|
+
run_details_log.write("[dim]" + "=" * 50 + "[/dim]")
|
238
|
+
|
239
|
+
# Show available attributes
|
240
|
+
for attr in ["id", "status", "start_time", "end_time", "duration_ms"]:
|
241
|
+
if hasattr(run, attr):
|
242
|
+
value = getattr(run, attr)
|
243
|
+
if value is not None:
|
244
|
+
run_details_log.write(f" {attr}: {value}")
|
245
|
+
|
246
|
+
# Show traces count
|
247
|
+
traces_count = len(run.traces) if run.traces else 0
|
248
|
+
run_details_log.write(f" traces_count: {traces_count}")
|
249
|
+
|
250
|
+
# Show logs count
|
251
|
+
logs_count = len(run.logs) if run.logs else 0
|
252
|
+
run_details_log.write(f" logs_count: {logs_count}")
|
253
|
+
|
254
|
+
def _rebuild_spans_tree(self):
|
255
|
+
"""Rebuild the spans tree from current run's traces."""
|
256
|
+
spans_tree = self.query_one("#spans-tree", Tree)
|
257
|
+
spans_tree.clear()
|
258
|
+
# Only clear the node mapping since we're rebuilding the tree structure
|
259
|
+
self.span_tree_nodes.clear()
|
260
|
+
|
261
|
+
if not self.current_run or not self.current_run.traces:
|
262
|
+
return
|
263
|
+
|
264
|
+
# Build spans tree from TraceMessage objects
|
265
|
+
self._build_spans_tree(self.current_run.traces)
|
266
|
+
|
267
|
+
# Expand the root "Trace" node
|
268
|
+
spans_tree.root.expand()
|
269
|
+
|
270
|
+
def _build_spans_tree(self, trace_messages: list[TraceMessage]):
|
271
|
+
"""Build the spans tree from trace messages."""
|
272
|
+
spans_tree = self.query_one("#spans-tree", Tree)
|
273
|
+
root = spans_tree.root
|
274
|
+
|
275
|
+
# Group spans by parent relationship
|
276
|
+
root_spans = []
|
277
|
+
child_spans: Dict[str, List[TraceMessage]] = {}
|
278
|
+
|
279
|
+
for trace_msg in trace_messages:
|
280
|
+
if not trace_msg.parent_span_id:
|
281
|
+
root_spans.append(trace_msg)
|
282
|
+
else:
|
283
|
+
if trace_msg.parent_span_id not in child_spans:
|
284
|
+
child_spans[trace_msg.parent_span_id] = []
|
285
|
+
child_spans[trace_msg.parent_span_id].append(trace_msg)
|
286
|
+
|
287
|
+
# Build tree recursively
|
288
|
+
for root_span in sorted(root_spans, key=lambda x: x.timestamp):
|
289
|
+
if root_span.span_id in child_spans:
|
290
|
+
for child in sorted(
|
291
|
+
child_spans[root_span.span_id], key=lambda x: x.timestamp
|
292
|
+
):
|
293
|
+
self._add_span_node(root, child, child_spans)
|
294
|
+
|
295
|
+
def _add_span_node(
|
296
|
+
self,
|
297
|
+
parent_node: TreeNode[str],
|
298
|
+
trace_msg: TraceMessage,
|
299
|
+
child_spans: Dict[str, List[TraceMessage]],
|
300
|
+
):
|
301
|
+
"""Recursively add span nodes to the tree."""
|
302
|
+
# Create display label for the span
|
303
|
+
color_map = {
|
304
|
+
"started": "🔵",
|
305
|
+
"running": "🟡",
|
306
|
+
"completed": "🟢",
|
307
|
+
"failed": "🔴",
|
308
|
+
"error": "🔴",
|
309
|
+
}
|
310
|
+
status_icon = color_map.get(trace_msg.status.lower(), "⚪")
|
311
|
+
|
312
|
+
duration_str = (
|
313
|
+
f" ({trace_msg.duration_ms:.1f}ms)" if trace_msg.duration_ms else ""
|
314
|
+
)
|
315
|
+
label = f"{status_icon} {trace_msg.span_name}{duration_str}"
|
316
|
+
|
317
|
+
# Add node to tree
|
318
|
+
node = parent_node.add(label)
|
319
|
+
node.data = trace_msg.span_id # Store span_id for reference
|
320
|
+
self.span_tree_nodes[trace_msg.span_id] = node
|
321
|
+
|
322
|
+
node.expand()
|
323
|
+
|
324
|
+
# Add child spans (sorted by timestamp)
|
325
|
+
if trace_msg.span_id in child_spans:
|
326
|
+
sorted_children = sorted(
|
327
|
+
child_spans[trace_msg.span_id], key=lambda x: x.timestamp
|
328
|
+
)
|
329
|
+
for child_span in sorted_children:
|
330
|
+
self._add_span_node(node, child_span, child_spans)
|
331
|
+
|
332
|
+
def on_tree_node_selected(self, event: Tree.NodeSelected[str]) -> None:
|
333
|
+
"""Handle span selection in the tree."""
|
334
|
+
# Check if this is our spans tree
|
335
|
+
spans_tree = self.query_one("#spans-tree", Tree)
|
336
|
+
if event.control != spans_tree:
|
337
|
+
return
|
338
|
+
|
339
|
+
# Get the selected span data
|
340
|
+
if hasattr(event.node, "data") and event.node.data:
|
341
|
+
span_id = event.node.data
|
342
|
+
# Find the trace in current_run.traces
|
343
|
+
trace_msg = None
|
344
|
+
if self.current_run:
|
345
|
+
for trace in self.current_run.traces:
|
346
|
+
if trace.span_id == span_id:
|
347
|
+
trace_msg = trace
|
348
|
+
break
|
349
|
+
|
350
|
+
if trace_msg:
|
351
|
+
# Show span details
|
352
|
+
span_details_display = self.query_one(
|
353
|
+
"#span-details-display", SpanDetailsDisplay
|
354
|
+
)
|
355
|
+
span_details_display.show_span_details(trace_msg)
|
356
|
+
|
357
|
+
def update_run_details(self, run: ExecutionRun):
|
358
|
+
if not self.current_run or run.id != self.current_run.id:
|
359
|
+
return
|
360
|
+
|
361
|
+
self._show_run_details(run)
|
362
|
+
|
363
|
+
def add_trace(self, trace_msg: TraceMessage):
|
364
|
+
"""Add trace to current run if it matches."""
|
365
|
+
if not self.current_run or trace_msg.run_id != self.current_run.id:
|
366
|
+
return
|
367
|
+
|
368
|
+
# Rebuild the tree to include new trace
|
369
|
+
self._rebuild_spans_tree()
|
370
|
+
|
371
|
+
def add_log(self, log_msg: LogMessage):
|
372
|
+
"""Add log to current run if it matches."""
|
373
|
+
if not self.current_run or log_msg.run_id != self.current_run.id:
|
374
|
+
return
|
375
|
+
|
376
|
+
color_map = {
|
377
|
+
"DEBUG": "dim cyan",
|
378
|
+
"INFO": "blue",
|
379
|
+
"WARN": "yellow",
|
380
|
+
"WARNING": "yellow",
|
381
|
+
"ERROR": "red",
|
382
|
+
"CRITICAL": "bold red",
|
383
|
+
}
|
384
|
+
|
385
|
+
color = color_map.get(log_msg.level.upper(), "white")
|
386
|
+
timestamp_str = log_msg.timestamp.strftime("%H:%M:%S")
|
387
|
+
level_short = log_msg.level[:4].upper()
|
388
|
+
|
389
|
+
log_text = (
|
390
|
+
f"[dim]{timestamp_str}[/dim] "
|
391
|
+
f"[{color}]{level_short}[/{color}] "
|
392
|
+
f"{log_msg.message}"
|
393
|
+
)
|
394
|
+
|
395
|
+
logs_log = self.query_one("#logs-log", RichLog)
|
396
|
+
logs_log.write(log_text)
|
397
|
+
|
398
|
+
def clear_display(self):
|
399
|
+
"""Clear both traces and logs display."""
|
400
|
+
run_details_log = self.query_one("#run-details-log", RichLog)
|
401
|
+
logs_log = self.query_one("#logs-log", RichLog)
|
402
|
+
spans_tree = self.query_one("#spans-tree", Tree)
|
403
|
+
|
404
|
+
run_details_log.clear()
|
405
|
+
logs_log.clear()
|
406
|
+
spans_tree.clear()
|
407
|
+
|
408
|
+
self.current_run = None
|
409
|
+
self.span_tree_nodes.clear()
|
410
|
+
|
411
|
+
# Clear span details
|
412
|
+
span_details_display = self.query_one(
|
413
|
+
"#span-details-display", SpanDetailsDisplay
|
414
|
+
)
|
415
|
+
span_details_log = span_details_display.query_one("#span-details", RichLog)
|
416
|
+
span_details_log.clear()
|
417
|
+
|
418
|
+
def refresh_display(self):
|
419
|
+
"""Refresh the display with current run data."""
|
420
|
+
if self.current_run:
|
421
|
+
self.show_run(self.current_run)
|