inspect-ai 0.3.49__py3-none-any.whl → 0.3.50__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.
- inspect_ai/_cli/info.py +2 -2
- inspect_ai/_cli/log.py +2 -2
- inspect_ai/_cli/score.py +2 -2
- inspect_ai/_display/core/display.py +19 -0
- inspect_ai/_display/core/panel.py +37 -7
- inspect_ai/_display/core/progress.py +29 -2
- inspect_ai/_display/core/results.py +79 -40
- inspect_ai/_display/core/textual.py +21 -0
- inspect_ai/_display/rich/display.py +28 -8
- inspect_ai/_display/textual/app.py +107 -1
- inspect_ai/_display/textual/display.py +1 -1
- inspect_ai/_display/textual/widgets/samples.py +132 -91
- inspect_ai/_display/textual/widgets/task_detail.py +232 -0
- inspect_ai/_display/textual/widgets/tasks.py +74 -6
- inspect_ai/_display/textual/widgets/toggle.py +32 -0
- inspect_ai/_eval/context.py +2 -0
- inspect_ai/_eval/eval.py +4 -3
- inspect_ai/_eval/loader.py +1 -1
- inspect_ai/_eval/run.py +35 -2
- inspect_ai/_eval/task/log.py +13 -11
- inspect_ai/_eval/task/results.py +12 -3
- inspect_ai/_eval/task/run.py +139 -36
- inspect_ai/_eval/task/sandbox.py +2 -1
- inspect_ai/_util/_async.py +30 -1
- inspect_ai/_util/file.py +31 -4
- inspect_ai/_util/html.py +3 -0
- inspect_ai/_util/logger.py +6 -5
- inspect_ai/_util/platform.py +5 -6
- inspect_ai/_util/registry.py +1 -1
- inspect_ai/_view/server.py +9 -9
- inspect_ai/_view/www/App.css +2 -2
- inspect_ai/_view/www/dist/assets/index.css +2 -2
- inspect_ai/_view/www/dist/assets/index.js +352 -294
- inspect_ai/_view/www/log-schema.json +13 -0
- inspect_ai/_view/www/package.json +1 -0
- inspect_ai/_view/www/src/components/MessageBand.mjs +1 -1
- inspect_ai/_view/www/src/components/Tools.mjs +16 -13
- inspect_ai/_view/www/src/samples/SampleDisplay.mjs +1 -3
- inspect_ai/_view/www/src/samples/SampleScoreView.mjs +52 -77
- inspect_ai/_view/www/src/samples/SamplesDescriptor.mjs +38 -13
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.mjs +15 -2
- inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.mjs +4 -2
- inspect_ai/_view/www/src/types/log.d.ts +2 -0
- inspect_ai/_view/www/src/workspace/WorkSpace.mjs +2 -0
- inspect_ai/_view/www/yarn.lock +9 -4
- inspect_ai/approval/__init__.py +1 -1
- inspect_ai/approval/_human/approver.py +35 -0
- inspect_ai/approval/_human/console.py +62 -0
- inspect_ai/approval/_human/manager.py +108 -0
- inspect_ai/approval/_human/panel.py +233 -0
- inspect_ai/approval/_human/util.py +51 -0
- inspect_ai/dataset/_sources/hf.py +2 -2
- inspect_ai/dataset/_sources/util.py +1 -1
- inspect_ai/log/_file.py +106 -36
- inspect_ai/log/_recorders/eval.py +226 -158
- inspect_ai/log/_recorders/file.py +9 -6
- inspect_ai/log/_recorders/json.py +35 -12
- inspect_ai/log/_recorders/recorder.py +15 -15
- inspect_ai/log/_samples.py +52 -0
- inspect_ai/model/_model.py +14 -0
- inspect_ai/model/_model_output.py +4 -0
- inspect_ai/model/_providers/azureai.py +1 -1
- inspect_ai/model/_providers/hf.py +106 -4
- inspect_ai/model/_providers/util/__init__.py +2 -0
- inspect_ai/model/_providers/util/hf_handler.py +200 -0
- inspect_ai/scorer/_common.py +1 -1
- inspect_ai/solver/_plan.py +0 -8
- inspect_ai/solver/_task_state.py +18 -1
- inspect_ai/solver/_use_tools.py +9 -1
- inspect_ai/tool/_tool_def.py +2 -2
- inspect_ai/tool/_tool_info.py +14 -2
- inspect_ai/tool/_tool_params.py +2 -1
- inspect_ai/tool/_tools/_execute.py +1 -1
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +6 -0
- inspect_ai/util/__init__.py +5 -6
- inspect_ai/util/_panel.py +91 -0
- inspect_ai/util/_sandbox/__init__.py +2 -6
- inspect_ai/util/_sandbox/context.py +4 -3
- inspect_ai/util/_sandbox/docker/compose.py +12 -2
- inspect_ai/util/_sandbox/docker/docker.py +19 -9
- inspect_ai/util/_sandbox/docker/util.py +10 -2
- inspect_ai/util/_sandbox/environment.py +47 -41
- inspect_ai/util/_sandbox/local.py +15 -10
- inspect_ai/util/_subprocess.py +43 -3
- {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/METADATA +2 -2
- {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/RECORD +90 -82
- inspect_ai/_view/www/node_modules/flatted/python/flatted.py +0 -149
- inspect_ai/_view/www/node_modules/flatted/python/test.py +0 -63
- inspect_ai/approval/_human.py +0 -123
- {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/LICENSE +0 -0
- {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/WHEEL +0 -0
- {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.49.dist-info → inspect_ai-0.3.50.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,29 @@
|
|
1
1
|
import asyncio
|
2
2
|
import contextlib
|
3
3
|
from asyncio import CancelledError
|
4
|
-
from typing import Any, AsyncIterator, Coroutine, Generic, Iterator
|
4
|
+
from typing import Any, AsyncIterator, ClassVar, Coroutine, Generic, Iterator, cast
|
5
5
|
|
6
6
|
import rich
|
7
7
|
from rich.console import Console
|
8
8
|
from rich.text import Text
|
9
9
|
from textual.app import App, ComposeResult
|
10
|
+
from textual.binding import Binding, BindingType
|
11
|
+
from textual.css.query import NoMatches
|
10
12
|
from textual.events import Print
|
11
13
|
from textual.widgets import TabbedContent, TabPane
|
14
|
+
from textual.widgets.tabbed_content import ContentTabs
|
12
15
|
from textual.worker import Worker, WorkerState
|
13
16
|
from typing_extensions import override
|
14
17
|
|
18
|
+
from inspect_ai._display.core.textual import textual_enable_mouse_support
|
19
|
+
from inspect_ai._util.html import as_html_id
|
15
20
|
from inspect_ai.log._samples import active_samples
|
16
21
|
from inspect_ai.log._transcript import InputEvent, transcript
|
17
22
|
|
23
|
+
from ...util._panel import InputPanel
|
18
24
|
from ..core.config import task_config
|
19
25
|
from ..core.display import (
|
26
|
+
TP,
|
20
27
|
TR,
|
21
28
|
TaskDisplay,
|
22
29
|
TaskProfile,
|
@@ -50,6 +57,17 @@ class TaskScreenResult(Generic[TR]):
|
|
50
57
|
class TaskScreenApp(App[TR]):
|
51
58
|
CSS_PATH = "app.tcss"
|
52
59
|
|
60
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
61
|
+
Binding(
|
62
|
+
"ctrl+c",
|
63
|
+
"quit",
|
64
|
+
"Interrupt",
|
65
|
+
tooltip="Interrupt the app and return to the command prompt.",
|
66
|
+
show=False,
|
67
|
+
priority=True,
|
68
|
+
)
|
69
|
+
]
|
70
|
+
|
53
71
|
def __init__(self) -> None:
|
54
72
|
# call super
|
55
73
|
super().__init__()
|
@@ -70,6 +88,12 @@ class TaskScreenApp(App[TR]):
|
|
70
88
|
# enable rich hooks
|
71
89
|
rich_initialise()
|
72
90
|
|
91
|
+
def _watch_app_focus(self, focus: bool) -> None:
|
92
|
+
super()._watch_app_focus(focus)
|
93
|
+
|
94
|
+
if focus and self.app._driver:
|
95
|
+
textual_enable_mouse_support(self.app._driver)
|
96
|
+
|
73
97
|
def run_app(self, main: Coroutine[Any, Any, TR]) -> TaskScreenResult[TR]:
|
74
98
|
# create the worker
|
75
99
|
self._worker = self.run_worker(main, start=False, exit_on_error=False)
|
@@ -217,6 +241,8 @@ class TaskScreenApp(App[TR]):
|
|
217
241
|
self.update_tasks()
|
218
242
|
self.update_samples()
|
219
243
|
self.update_footer()
|
244
|
+
for input_panel in self.query(f".{InputPanel.DEFAULT_CLASSES}"):
|
245
|
+
cast(InputPanel, input_panel).update()
|
220
246
|
|
221
247
|
# update the header title
|
222
248
|
def update_title(self) -> None:
|
@@ -288,6 +314,12 @@ class TaskScreenApp(App[TR]):
|
|
288
314
|
|
289
315
|
self.watch(tabs, "active", set_active_tab)
|
290
316
|
|
317
|
+
# activate the tasks tab
|
318
|
+
def activate_tasks_tab(self) -> None:
|
319
|
+
tasks = self.query_one(TasksView)
|
320
|
+
tasks.tasks.focus() # force the tab to switch by focusing a child
|
321
|
+
self.query_one(ContentTabs).focus() # focus the tab control
|
322
|
+
|
291
323
|
# capture output and route to console view and our buffer
|
292
324
|
def on_print(self, event: Print) -> None:
|
293
325
|
# remove trailing newline
|
@@ -308,10 +340,66 @@ class TaskScreenApp(App[TR]):
|
|
308
340
|
self._worker.cancel()
|
309
341
|
self.update_title()
|
310
342
|
|
343
|
+
# dynamic input panels
|
344
|
+
async def add_input_panel(self, title: str, panel: InputPanel) -> None:
|
345
|
+
tabs = self.query_one(TabbedContent)
|
346
|
+
await tabs.add_pane(TabPane(title, panel, id=as_input_panel_id(title)))
|
347
|
+
|
348
|
+
def get_input_panel(self, title: str) -> InputPanel | None:
|
349
|
+
try:
|
350
|
+
tab_pane = self.query_one(f"#{as_input_panel_id(title)}")
|
351
|
+
if len(tab_pane.children) > 0:
|
352
|
+
return cast(InputPanel, tab_pane.children[0])
|
353
|
+
else:
|
354
|
+
return None
|
355
|
+
except NoMatches:
|
356
|
+
return None
|
357
|
+
|
358
|
+
async def remove_input_panel(self, title: str) -> None:
|
359
|
+
tabs = self.query_one(TabbedContent)
|
360
|
+
await tabs.remove_pane(as_html_id(as_input_panel_id(title), title))
|
361
|
+
|
362
|
+
class InputPanelHost(InputPanel.Host):
|
363
|
+
def __init__(self, app: "TaskScreenApp[TR]", tab_id: str) -> None:
|
364
|
+
self.app = app
|
365
|
+
self.tab_id = tab_id
|
366
|
+
|
367
|
+
def set_title(self, title: str) -> None:
|
368
|
+
tabs = self.app.query_one(TabbedContent)
|
369
|
+
tab = tabs.get_tab(self.tab_id)
|
370
|
+
tab.label = Text.from_markup(title)
|
371
|
+
|
372
|
+
def activate(self) -> None:
|
373
|
+
# show the tab
|
374
|
+
tabs = self.app.query_one(TabbedContent)
|
375
|
+
tabs.show_tab(self.tab_id)
|
376
|
+
|
377
|
+
# focus the first focuable child (this seems to be necessary
|
378
|
+
# to get textual to reliably make the switch). after that, focus
|
379
|
+
# the tabs control so the user can switch back w/ the keyboard
|
380
|
+
tab_pane = self.app.query_one(f"#{self.tab_id}")
|
381
|
+
panel = cast(InputPanel, tab_pane.children[0])
|
382
|
+
for child in panel.children:
|
383
|
+
if child.focusable:
|
384
|
+
child.focus()
|
385
|
+
self.app.query_one(ContentTabs).focus()
|
386
|
+
break
|
387
|
+
|
388
|
+
def deactivate(self) -> None:
|
389
|
+
tabs = self.app.query_one(TabbedContent)
|
390
|
+
if tabs.active == self.tab_id:
|
391
|
+
self.app.activate_tasks_tab()
|
392
|
+
|
393
|
+
def close(self) -> None:
|
394
|
+
tabs = self.app.query_one(TabbedContent)
|
395
|
+
tabs.remove_pane(self.tab_id)
|
396
|
+
self.app.activate_tasks_tab()
|
397
|
+
|
311
398
|
|
312
399
|
class TextualTaskScreen(TaskScreen, Generic[TR]):
|
313
400
|
def __init__(self, app: TaskScreenApp[TR]) -> None:
|
314
401
|
self.app = app
|
402
|
+
self.lock = asyncio.Lock()
|
315
403
|
|
316
404
|
def __exit__(self, *excinfo: Any) -> None:
|
317
405
|
pass
|
@@ -361,3 +449,21 @@ class TextualTaskScreen(TaskScreen, Generic[TR]):
|
|
361
449
|
# reset width
|
362
450
|
if old_width:
|
363
451
|
console.width = old_width
|
452
|
+
|
453
|
+
@override
|
454
|
+
async def input_panel(self, title: str, panel: type[TP]) -> TP:
|
455
|
+
async with self.lock:
|
456
|
+
panel_widget = self.app.get_input_panel(title)
|
457
|
+
if panel_widget is None:
|
458
|
+
panel_widget = panel(
|
459
|
+
title,
|
460
|
+
TaskScreenApp[TR].InputPanelHost(
|
461
|
+
self.app, as_input_panel_id(title)
|
462
|
+
),
|
463
|
+
)
|
464
|
+
await self.app.add_input_panel(title, panel_widget)
|
465
|
+
return cast(TP, panel_widget)
|
466
|
+
|
467
|
+
|
468
|
+
def as_input_panel_id(title: str) -> str:
|
469
|
+
return as_html_id("id-input-panel", title)
|
@@ -53,7 +53,7 @@ class TextualDisplay(Display):
|
|
53
53
|
@override
|
54
54
|
@contextlib.contextmanager
|
55
55
|
def suspend_task_app(self) -> Iterator[None]:
|
56
|
-
if getattr(self, "app", None):
|
56
|
+
if getattr(self, "app", None) and self.app.is_running:
|
57
57
|
with self.app.suspend_app():
|
58
58
|
yield
|
59
59
|
else:
|
@@ -1,5 +1,7 @@
|
|
1
|
+
import time
|
1
2
|
from typing import cast
|
2
3
|
|
4
|
+
from rich.console import RenderableType
|
3
5
|
from rich.table import Table
|
4
6
|
from rich.text import Text
|
5
7
|
from textual.app import ComposeResult
|
@@ -9,6 +11,7 @@ from textual.containers import (
|
|
9
11
|
Vertical,
|
10
12
|
VerticalGroup,
|
11
13
|
)
|
14
|
+
from textual.reactive import reactive
|
12
15
|
from textual.widget import Widget
|
13
16
|
from textual.widgets import (
|
14
17
|
Button,
|
@@ -21,11 +24,6 @@ from textual.widgets.option_list import Option, Separator
|
|
21
24
|
|
22
25
|
from inspect_ai._util.registry import registry_unqualified_name
|
23
26
|
from inspect_ai.log._samples import ActiveSample
|
24
|
-
from inspect_ai.util._sandbox import (
|
25
|
-
SandboxConnection,
|
26
|
-
SandboxConnectionContainer,
|
27
|
-
SandboxConnectionLocal,
|
28
|
-
)
|
29
27
|
|
30
28
|
from ...core.progress import progress_time
|
31
29
|
from .clock import Clock
|
@@ -49,6 +47,7 @@ class SamplesView(Widget):
|
|
49
47
|
def __init__(self) -> None:
|
50
48
|
super().__init__()
|
51
49
|
self.samples: list[ActiveSample] = []
|
50
|
+
self.last_updated = time.perf_counter()
|
52
51
|
|
53
52
|
def compose(self) -> ComposeResult:
|
54
53
|
yield SamplesList()
|
@@ -65,7 +64,12 @@ class SamplesView(Widget):
|
|
65
64
|
await self.query_one(TranscriptView).notify_active(active)
|
66
65
|
|
67
66
|
def set_samples(self, samples: list[ActiveSample]) -> None:
|
68
|
-
|
67
|
+
# throttle to no more than 1 second per 100 samples
|
68
|
+
throttle = round(max(len(samples) / 100, 1))
|
69
|
+
current = time.perf_counter()
|
70
|
+
if (current - self.last_updated) > throttle:
|
71
|
+
self.query_one(SamplesList).set_samples(samples)
|
72
|
+
self.last_updated = current
|
69
73
|
|
70
74
|
async def set_highlighted_sample(self, highlighted: int | None) -> None:
|
71
75
|
sample_info = self.query_one(SampleInfo)
|
@@ -198,7 +202,7 @@ class SampleInfo(Horizontal):
|
|
198
202
|
}
|
199
203
|
SampleInfo Collapsible Contents {
|
200
204
|
padding: 1 0 1 2;
|
201
|
-
|
205
|
+
height: auto;
|
202
206
|
overflow-x: auto;
|
203
207
|
}
|
204
208
|
SampleInfo Static {
|
@@ -211,52 +215,89 @@ class SampleInfo(Horizontal):
|
|
211
215
|
def __init__(self) -> None:
|
212
216
|
super().__init__()
|
213
217
|
self._sample: ActiveSample | None = None
|
214
|
-
self._show_sandboxes = False
|
215
218
|
|
216
219
|
def compose(self) -> ComposeResult:
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
else:
|
221
|
-
yield Static()
|
220
|
+
with Collapsible(title=""):
|
221
|
+
yield SampleLimits()
|
222
|
+
yield SandboxesView()
|
222
223
|
|
223
224
|
async def sync_sample(self, sample: ActiveSample | None) -> None:
|
224
|
-
|
225
|
-
|
226
|
-
|
225
|
+
if sample is None:
|
226
|
+
self.display = False
|
227
|
+
self._sample = None
|
228
|
+
else:
|
229
|
+
# update sample limits
|
230
|
+
limits = self.query_one(SampleLimits)
|
231
|
+
await limits.sync_sample(sample)
|
227
232
|
|
228
|
-
|
229
|
-
|
233
|
+
# bail if we've already processed this sample
|
234
|
+
if self._sample == sample:
|
235
|
+
return
|
230
236
|
|
231
|
-
|
232
|
-
|
233
|
-
if show_sandboxes != self._show_sandboxes:
|
234
|
-
await self.recompose()
|
235
|
-
self._show_sandboxes = show_sandboxes
|
237
|
+
# set sample
|
238
|
+
self._sample = sample
|
236
239
|
|
237
|
-
|
240
|
+
# update UI
|
238
241
|
self.display = True
|
239
242
|
title = f"{registry_unqualified_name(sample.task)} (id: {sample.sample.id}, epoch {sample.epoch}): {sample.model}"
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
243
|
+
self.query_one(Collapsible).title = title
|
244
|
+
sandboxes = self.query_one(SandboxesView)
|
245
|
+
await sandboxes.sync_sample(sample)
|
246
|
+
|
247
|
+
|
248
|
+
class SampleLimits(Widget):
|
249
|
+
DEFAULT_CSS = """
|
250
|
+
SampleLimits {
|
251
|
+
padding: 0 0 0 0;
|
252
|
+
color: $secondary;
|
253
|
+
background: transparent;
|
254
|
+
height: auto;
|
255
|
+
}
|
256
|
+
SampleLimits Static {
|
257
|
+
background: transparent;
|
258
|
+
color: $secondary;
|
259
|
+
}
|
260
|
+
"""
|
261
|
+
|
262
|
+
messages = reactive(0)
|
263
|
+
message_limit = reactive(0)
|
264
|
+
tokens = reactive(0)
|
265
|
+
token_limit = reactive(0)
|
266
|
+
started = reactive(0)
|
267
|
+
time_limit = reactive(0)
|
268
|
+
|
269
|
+
def __init__(self) -> None:
|
270
|
+
super().__init__()
|
271
|
+
|
272
|
+
def render(self) -> RenderableType:
|
273
|
+
limits = f"[bold]messages[/bold]: {self.messages}"
|
274
|
+
if self.message_limit:
|
275
|
+
limits = f"{limits} (limit {self.message_limit})"
|
276
|
+
limits = f"{limits}, [bold]tokens[/bold]: {self.tokens:,}"
|
277
|
+
if self.token_limit:
|
278
|
+
limits = f"{limits} ({self.token_limit:,})"
|
279
|
+
return limits
|
280
|
+
|
281
|
+
async def sync_sample(self, sample: ActiveSample) -> None:
|
282
|
+
self.messages = sample.total_messages
|
283
|
+
self.message_limit = sample.message_limit or 0
|
284
|
+
self.tokens = sample.total_tokens
|
285
|
+
self.token_limit = sample.token_limit or 0
|
248
286
|
|
249
287
|
|
250
288
|
class SandboxesView(Vertical):
|
251
289
|
DEFAULT_CSS = """
|
252
290
|
SandboxesView {
|
253
|
-
padding:
|
291
|
+
padding: 1 0 1 0;
|
254
292
|
background: transparent;
|
255
293
|
height: auto;
|
256
294
|
}
|
257
295
|
SandboxesView Static {
|
258
296
|
background: transparent;
|
259
297
|
}
|
298
|
+
.clipboard-message {
|
299
|
+
margin-top: 1;
|
300
|
+
}
|
260
301
|
"""
|
261
302
|
|
262
303
|
def __init__(self) -> None:
|
@@ -264,61 +305,57 @@ class SandboxesView(Vertical):
|
|
264
305
|
|
265
306
|
def compose(self) -> ComposeResult:
|
266
307
|
yield Static(id="sandboxes-caption", markup=True)
|
267
|
-
yield Vertical(id="sandboxes")
|
268
|
-
yield Static(
|
269
|
-
"[italic]Hold down Alt (or Option) to select text for copying[/italic]",
|
270
|
-
id="sandboxes-footer",
|
271
|
-
markup=True,
|
272
|
-
)
|
273
|
-
|
274
|
-
async def sync_sandboxes(self, sandboxes: dict[str, SandboxConnection]) -> None:
|
275
|
-
def sandbox_connection_type() -> str:
|
276
|
-
connection = list(sandboxes.values())[0]
|
277
|
-
if isinstance(connection, SandboxConnectionLocal):
|
278
|
-
return "directories"
|
279
|
-
elif isinstance(connection, SandboxConnectionContainer):
|
280
|
-
return "containers"
|
281
|
-
else:
|
282
|
-
return "hosts"
|
308
|
+
yield Vertical(id="sandboxes-list")
|
283
309
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
target = sandbox.container
|
289
|
-
else:
|
290
|
-
target = sandbox.destination
|
291
|
-
return target.strip()
|
292
|
-
|
293
|
-
caption = cast(Static, self.query_one("#sandboxes-caption"))
|
294
|
-
caption.update(f"[bold]sandbox {sandbox_connection_type()}:[/bold]")
|
295
|
-
|
296
|
-
sandboxes_widget = self.query_one("#sandboxes")
|
297
|
-
sandboxes_widget.styles.margin = (
|
298
|
-
(0, 0, 1, 0) if len(sandboxes) > 1 else (0, 0, 0, 0)
|
299
|
-
)
|
300
|
-
await sandboxes_widget.remove_children()
|
301
|
-
await sandboxes_widget.mount_all(
|
302
|
-
[
|
303
|
-
Static(sandbox_connection_target(sandbox))
|
304
|
-
for sandbox in sandboxes.values()
|
305
|
-
]
|
310
|
+
async def sync_sample(self, sample: ActiveSample) -> None:
|
311
|
+
sandboxes = sample.sandboxes
|
312
|
+
show_sandboxes = (
|
313
|
+
len([sandbox for sandbox in sandboxes.values() if sandbox.container]) > 0
|
306
314
|
)
|
307
315
|
|
316
|
+
if show_sandboxes:
|
317
|
+
self.display = True
|
318
|
+
sandboxes_caption = cast(Static, self.query_one("#sandboxes-caption"))
|
319
|
+
sandboxes_caption.update("[bold]sandbox containers:[/bold]")
|
320
|
+
|
321
|
+
sandboxes_list = self.query_one("#sandboxes-list")
|
322
|
+
await sandboxes_list.remove_children()
|
323
|
+
await sandboxes_list.mount_all(
|
324
|
+
[
|
325
|
+
Static(sandbox.container)
|
326
|
+
for sandbox in sandboxes.values()
|
327
|
+
if sandbox.container
|
328
|
+
]
|
329
|
+
)
|
330
|
+
sandboxes_list.mount(
|
331
|
+
Static(
|
332
|
+
"[italic]Hold down Alt (or Option) to select text for copying[/italic]",
|
333
|
+
classes="clipboard-message",
|
334
|
+
markup=True,
|
335
|
+
)
|
336
|
+
)
|
337
|
+
else:
|
338
|
+
self.display = False
|
339
|
+
|
308
340
|
|
309
341
|
class SampleToolbar(Horizontal):
|
310
|
-
|
311
|
-
|
342
|
+
CANCEL_SCORE_OUTPUT = "cancel_score_output"
|
343
|
+
CANCEL_RAISE_ERROR = "cancel_raise_error"
|
344
|
+
PENDING_STATUS = "pending_status"
|
345
|
+
PENDING_CAPTION = "pending_caption"
|
346
|
+
|
347
|
+
DEFAULT_CSS = f"""
|
348
|
+
SampleToolbar Button {{
|
312
349
|
margin-bottom: 1;
|
313
350
|
margin-right: 2;
|
314
351
|
min-width: 20;
|
315
|
-
}
|
316
|
-
SampleToolbar #
|
352
|
+
}}
|
353
|
+
SampleToolbar #{CANCEL_SCORE_OUTPUT} {{
|
317
354
|
color: $primary-darken-3;
|
318
|
-
}
|
319
|
-
SampleToolbar #
|
355
|
+
}}
|
356
|
+
SampleToolbar #{CANCEL_RAISE_ERROR} {{
|
320
357
|
color: $warning-darken-3;
|
321
|
-
}
|
358
|
+
}}
|
322
359
|
"""
|
323
360
|
|
324
361
|
def __init__(self) -> None:
|
@@ -326,30 +363,30 @@ class SampleToolbar(Horizontal):
|
|
326
363
|
self.sample: ActiveSample | None = None
|
327
364
|
|
328
365
|
def compose(self) -> ComposeResult:
|
329
|
-
with VerticalGroup(id=
|
330
|
-
yield Static("Executing...", id=
|
366
|
+
with VerticalGroup(id=self.PENDING_STATUS):
|
367
|
+
yield Static("Executing...", id=self.PENDING_CAPTION)
|
331
368
|
yield HorizontalGroup(EventLoadingIndicator(), Clock())
|
332
369
|
yield Button(
|
333
370
|
Text("Cancel (Score)"),
|
334
|
-
id=
|
371
|
+
id=self.CANCEL_SCORE_OUTPUT,
|
335
372
|
tooltip="Cancel the sample and score whatever output has been generated so far.",
|
336
373
|
)
|
337
374
|
yield Button(
|
338
375
|
Text("Cancel (Error)"),
|
339
|
-
id=
|
376
|
+
id=self.CANCEL_RAISE_ERROR,
|
340
377
|
tooltip="Cancel the sample and raise an error (task will exit unless fail_on_error is set)",
|
341
378
|
)
|
342
379
|
|
343
380
|
def on_mount(self) -> None:
|
344
|
-
self.query_one("#
|
345
|
-
self.query_one("#
|
346
|
-
self.query_one("#
|
381
|
+
self.query_one("#" + self.PENDING_STATUS).visible = False
|
382
|
+
self.query_one("#" + self.CANCEL_SCORE_OUTPUT).display = False
|
383
|
+
self.query_one("#" + self.CANCEL_RAISE_ERROR).display = False
|
347
384
|
|
348
385
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
349
386
|
if self.sample:
|
350
|
-
if event.button.id ==
|
387
|
+
if event.button.id == self.CANCEL_SCORE_OUTPUT:
|
351
388
|
self.sample.interrupt("score")
|
352
|
-
elif event.button.id ==
|
389
|
+
elif event.button.id == self.CANCEL_RAISE_ERROR:
|
353
390
|
self.sample.interrupt("error")
|
354
391
|
|
355
392
|
async def sync_sample(self, sample: ActiveSample | None) -> None:
|
@@ -358,10 +395,12 @@ class SampleToolbar(Horizontal):
|
|
358
395
|
# track the sample
|
359
396
|
self.sample = sample
|
360
397
|
|
361
|
-
pending_status = self.query_one("#
|
398
|
+
pending_status = self.query_one("#" + self.PENDING_STATUS)
|
362
399
|
clock = self.query_one(Clock)
|
363
|
-
cancel_score_output = cast(
|
364
|
-
|
400
|
+
cancel_score_output = cast(
|
401
|
+
Button, self.query_one("#" + self.CANCEL_SCORE_OUTPUT)
|
402
|
+
)
|
403
|
+
cancel_with_error = cast(Button, self.query_one("#" + self.CANCEL_RAISE_ERROR))
|
365
404
|
if sample and not sample.completed:
|
366
405
|
# update visibility and button status
|
367
406
|
self.display = True
|
@@ -376,7 +415,9 @@ class SampleToolbar(Horizontal):
|
|
376
415
|
)
|
377
416
|
if last_event and last_event.pending:
|
378
417
|
pending_status.visible = True
|
379
|
-
pending_caption = cast(
|
418
|
+
pending_caption = cast(
|
419
|
+
Static, self.query_one("#" + self.PENDING_CAPTION)
|
420
|
+
)
|
380
421
|
pending_caption_text = (
|
381
422
|
"Generating..."
|
382
423
|
if isinstance(last_event, ModelEvent)
|