inspect-ai 0.3.80__py3-none-any.whl → 0.3.82__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/eval.py +35 -2
- inspect_ai/_cli/util.py +44 -1
- inspect_ai/_display/core/config.py +1 -1
- inspect_ai/_display/core/display.py +13 -4
- inspect_ai/_display/core/results.py +1 -1
- inspect_ai/_display/textual/widgets/task_detail.py +5 -4
- inspect_ai/_eval/eval.py +38 -1
- inspect_ai/_eval/evalset.py +5 -0
- inspect_ai/_eval/run.py +5 -2
- inspect_ai/_eval/task/log.py +53 -6
- inspect_ai/_eval/task/run.py +51 -10
- inspect_ai/_util/constants.py +2 -0
- inspect_ai/_util/file.py +17 -1
- inspect_ai/_util/json.py +36 -1
- inspect_ai/_view/server.py +113 -1
- inspect_ai/_view/www/App.css +1 -1
- inspect_ai/_view/www/dist/assets/index.css +518 -296
- inspect_ai/_view/www/dist/assets/index.js +38803 -36307
- inspect_ai/_view/www/eslint.config.mjs +1 -1
- inspect_ai/_view/www/log-schema.json +13 -0
- inspect_ai/_view/www/node_modules/flatted/python/flatted.py +149 -0
- inspect_ai/_view/www/package.json +8 -2
- inspect_ai/_view/www/src/App.tsx +151 -855
- inspect_ai/_view/www/src/api/api-browser.ts +176 -5
- inspect_ai/_view/www/src/api/api-vscode.ts +75 -1
- inspect_ai/_view/www/src/api/client-api.ts +66 -10
- inspect_ai/_view/www/src/api/jsonrpc.ts +2 -0
- inspect_ai/_view/www/src/api/types.ts +107 -2
- inspect_ai/_view/www/src/appearance/icons.ts +1 -0
- inspect_ai/_view/www/src/components/AsciinemaPlayer.tsx +3 -3
- inspect_ai/_view/www/src/components/DownloadPanel.tsx +2 -2
- inspect_ai/_view/www/src/components/ExpandablePanel.tsx +56 -61
- inspect_ai/_view/www/src/components/FindBand.tsx +17 -9
- inspect_ai/_view/www/src/components/HumanBaselineView.tsx +1 -1
- inspect_ai/_view/www/src/components/JsonPanel.tsx +14 -24
- inspect_ai/_view/www/src/components/LargeModal.tsx +2 -35
- inspect_ai/_view/www/src/components/LightboxCarousel.tsx +27 -11
- inspect_ai/_view/www/src/components/LiveVirtualList.module.css +11 -0
- inspect_ai/_view/www/src/components/LiveVirtualList.tsx +177 -0
- inspect_ai/_view/www/src/components/MarkdownDiv.tsx +3 -3
- inspect_ai/_view/www/src/components/MessageBand.tsx +14 -9
- inspect_ai/_view/www/src/components/MorePopOver.tsx +3 -3
- inspect_ai/_view/www/src/components/NavPills.tsx +20 -8
- inspect_ai/_view/www/src/components/NoContentsPanel.module.css +12 -0
- inspect_ai/_view/www/src/components/NoContentsPanel.tsx +20 -0
- inspect_ai/_view/www/src/components/ProgressBar.module.css +5 -4
- inspect_ai/_view/www/src/components/ProgressBar.tsx +3 -2
- inspect_ai/_view/www/src/components/PulsingDots.module.css +81 -0
- inspect_ai/_view/www/src/components/PulsingDots.tsx +45 -0
- inspect_ai/_view/www/src/components/TabSet.tsx +4 -37
- inspect_ai/_view/www/src/components/ToolButton.tsx +3 -4
- inspect_ai/_view/www/src/index.tsx +26 -94
- inspect_ai/_view/www/src/logfile/remoteLogFile.ts +9 -1
- inspect_ai/_view/www/src/logfile/remoteZipFile.ts +30 -4
- inspect_ai/_view/www/src/metadata/RenderedContent.tsx +4 -6
- inspect_ai/_view/www/src/plan/ScorerDetailView.tsx +1 -1
- inspect_ai/_view/www/src/samples/InlineSampleDisplay.module.css +9 -1
- inspect_ai/_view/www/src/samples/InlineSampleDisplay.tsx +67 -28
- inspect_ai/_view/www/src/samples/SampleDialog.tsx +51 -22
- inspect_ai/_view/www/src/samples/SampleDisplay.module.css +4 -0
- inspect_ai/_view/www/src/samples/SampleDisplay.tsx +144 -90
- inspect_ai/_view/www/src/samples/SampleSummaryView.module.css +4 -0
- inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +82 -35
- inspect_ai/_view/www/src/samples/SamplesTools.tsx +23 -30
- inspect_ai/_view/www/src/samples/chat/ChatMessage.tsx +2 -1
- inspect_ai/_view/www/src/samples/chat/ChatMessageRenderer.tsx +1 -1
- inspect_ai/_view/www/src/samples/chat/ChatViewVirtualList.tsx +45 -53
- inspect_ai/_view/www/src/samples/chat/MessageContent.tsx +4 -1
- inspect_ai/_view/www/src/samples/chat/MessageContents.tsx +3 -0
- inspect_ai/_view/www/src/samples/chat/messages.ts +34 -0
- inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.module.css +3 -0
- inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +10 -1
- inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +22 -46
- inspect_ai/_view/www/src/samples/descriptor/samplesDescriptor.tsx +25 -17
- inspect_ai/_view/www/src/samples/descriptor/score/ObjectScoreDescriptor.tsx +2 -1
- inspect_ai/_view/www/src/samples/descriptor/types.ts +6 -5
- inspect_ai/_view/www/src/samples/list/SampleFooter.module.css +21 -3
- inspect_ai/_view/www/src/samples/list/SampleFooter.tsx +20 -1
- inspect_ai/_view/www/src/samples/list/SampleList.tsx +105 -85
- inspect_ai/_view/www/src/samples/list/SampleRow.module.css +6 -0
- inspect_ai/_view/www/src/samples/list/SampleRow.tsx +27 -14
- inspect_ai/_view/www/src/samples/sample-tools/SelectScorer.tsx +29 -18
- inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +28 -28
- inspect_ai/_view/www/src/samples/sample-tools/sample-filter/SampleFilter.tsx +19 -9
- inspect_ai/_view/www/src/samples/sampleDataAdapter.ts +33 -0
- inspect_ai/_view/www/src/samples/sampleLimit.ts +2 -2
- inspect_ai/_view/www/src/samples/scores/SampleScoreView.tsx +7 -9
- inspect_ai/_view/www/src/samples/scores/SampleScores.tsx +7 -11
- inspect_ai/_view/www/src/samples/transcript/ErrorEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/InfoEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/InputEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +4 -0
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.tsx +10 -24
- inspect_ai/_view/www/src/samples/transcript/SampleInitEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/SampleLimitEventView.tsx +4 -22
- inspect_ai/_view/www/src/samples/transcript/SandboxEventView.tsx +15 -24
- inspect_ai/_view/www/src/samples/transcript/ScoreEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/StepEventView.tsx +6 -28
- inspect_ai/_view/www/src/samples/transcript/SubtaskEventView.tsx +24 -34
- inspect_ai/_view/www/src/samples/transcript/ToolEventView.module.css +4 -0
- inspect_ai/_view/www/src/samples/transcript/ToolEventView.tsx +8 -13
- inspect_ai/_view/www/src/samples/transcript/TranscriptView.tsx +197 -338
- inspect_ai/_view/www/src/samples/transcript/TranscriptVirtualListComponent.module.css +16 -0
- inspect_ai/_view/www/src/samples/transcript/TranscriptVirtualListComponent.tsx +44 -0
- inspect_ai/_view/www/src/samples/transcript/event/EventNav.tsx +7 -4
- inspect_ai/_view/www/src/samples/transcript/event/EventPanel.tsx +52 -58
- inspect_ai/_view/www/src/samples/transcript/event/EventProgressPanel.module.css +23 -0
- inspect_ai/_view/www/src/samples/transcript/event/EventProgressPanel.tsx +27 -0
- inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.tsx +30 -1
- inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +102 -72
- inspect_ai/_view/www/src/scoring/utils.ts +87 -0
- inspect_ai/_view/www/src/state/appSlice.ts +244 -0
- inspect_ai/_view/www/src/state/hooks.ts +397 -0
- inspect_ai/_view/www/src/state/logPolling.ts +196 -0
- inspect_ai/_view/www/src/state/logSlice.ts +214 -0
- inspect_ai/_view/www/src/state/logsPolling.ts +118 -0
- inspect_ai/_view/www/src/state/logsSlice.ts +181 -0
- inspect_ai/_view/www/src/state/samplePolling.ts +311 -0
- inspect_ai/_view/www/src/state/sampleSlice.ts +127 -0
- inspect_ai/_view/www/src/state/sampleUtils.ts +21 -0
- inspect_ai/_view/www/src/state/scrolling.ts +206 -0
- inspect_ai/_view/www/src/state/store.ts +168 -0
- inspect_ai/_view/www/src/state/store_filter.ts +84 -0
- inspect_ai/_view/www/src/state/utils.ts +23 -0
- inspect_ai/_view/www/src/storage/index.ts +26 -0
- inspect_ai/_view/www/src/types/log.d.ts +2 -0
- inspect_ai/_view/www/src/types.ts +94 -32
- inspect_ai/_view/www/src/utils/attachments.ts +58 -23
- inspect_ai/_view/www/src/utils/logger.ts +52 -0
- inspect_ai/_view/www/src/utils/polling.ts +100 -0
- inspect_ai/_view/www/src/utils/react.ts +30 -0
- inspect_ai/_view/www/src/utils/vscode.ts +1 -1
- inspect_ai/_view/www/src/workspace/WorkSpace.tsx +181 -216
- inspect_ai/_view/www/src/workspace/WorkSpaceView.tsx +11 -53
- inspect_ai/_view/www/src/workspace/navbar/Navbar.tsx +8 -18
- inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.module.css +1 -0
- inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +40 -22
- inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.module.css +0 -1
- inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.tsx +98 -39
- inspect_ai/_view/www/src/workspace/navbar/RunningStatusPanel.module.css +32 -0
- inspect_ai/_view/www/src/workspace/navbar/RunningStatusPanel.tsx +32 -0
- inspect_ai/_view/www/src/workspace/navbar/SecondaryBar.tsx +11 -13
- inspect_ai/_view/www/src/workspace/navbar/StatusPanel.tsx +6 -2
- inspect_ai/_view/www/src/workspace/sidebar/LogDirectoryTitleView.tsx +4 -4
- inspect_ai/_view/www/src/workspace/sidebar/Sidebar.tsx +28 -13
- inspect_ai/_view/www/src/workspace/tabs/InfoTab.tsx +5 -10
- inspect_ai/_view/www/src/workspace/tabs/JsonTab.tsx +4 -4
- inspect_ai/_view/www/src/workspace/tabs/RunningNoSamples.module.css +22 -0
- inspect_ai/_view/www/src/workspace/tabs/RunningNoSamples.tsx +19 -0
- inspect_ai/_view/www/src/workspace/tabs/SamplesTab.tsx +110 -115
- inspect_ai/_view/www/src/workspace/tabs/grouping.ts +37 -5
- inspect_ai/_view/www/src/workspace/tabs/types.ts +4 -0
- inspect_ai/_view/www/src/workspace/types.ts +4 -3
- inspect_ai/_view/www/src/workspace/utils.ts +4 -4
- inspect_ai/_view/www/vite.config.js +6 -0
- inspect_ai/_view/www/yarn.lock +370 -354
- inspect_ai/log/_condense.py +26 -0
- inspect_ai/log/_log.py +6 -3
- inspect_ai/log/_recorders/buffer/__init__.py +14 -0
- inspect_ai/log/_recorders/buffer/buffer.py +30 -0
- inspect_ai/log/_recorders/buffer/database.py +685 -0
- inspect_ai/log/_recorders/buffer/filestore.py +259 -0
- inspect_ai/log/_recorders/buffer/types.py +84 -0
- inspect_ai/log/_recorders/eval.py +2 -11
- inspect_ai/log/_recorders/types.py +30 -0
- inspect_ai/log/_transcript.py +27 -1
- inspect_ai/model/_call_tools.py +1 -0
- inspect_ai/model/_generate_config.py +2 -2
- inspect_ai/model/_model.py +1 -0
- inspect_ai/tool/_tool_support_helpers.py +4 -4
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +3 -1
- inspect_ai/util/_subtask.py +1 -0
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/METADATA +2 -2
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/RECORD +178 -138
- inspect_ai/_view/www/src/samples/transcript/SampleTranscript.tsx +0 -22
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/WHEEL +0 -0
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/licenses/LICENSE +0 -0
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/top_level.txt +0 -0
inspect_ai/_eval/task/run.py
CHANGED
@@ -19,7 +19,7 @@ from inspect_ai._display import (
|
|
19
19
|
TaskSuccess,
|
20
20
|
display,
|
21
21
|
)
|
22
|
-
from inspect_ai._display.core.display import
|
22
|
+
from inspect_ai._display.core.display import TaskDisplayMetric
|
23
23
|
from inspect_ai._util._async import tg_collect
|
24
24
|
from inspect_ai._util.constants import (
|
25
25
|
DEFAULT_EPOCHS,
|
@@ -29,6 +29,7 @@ from inspect_ai._util.constants import (
|
|
29
29
|
from inspect_ai._util.datetime import iso_now
|
30
30
|
from inspect_ai._util.error import exception_message
|
31
31
|
from inspect_ai._util.hooks import send_telemetry
|
32
|
+
from inspect_ai._util.json import to_json_str_safe
|
32
33
|
from inspect_ai._util.registry import (
|
33
34
|
is_registry_object,
|
34
35
|
registry_log_name,
|
@@ -51,13 +52,17 @@ from inspect_ai.log import (
|
|
51
52
|
from inspect_ai.log._condense import condense_sample
|
52
53
|
from inspect_ai.log._file import eval_log_json_str
|
53
54
|
from inspect_ai.log._log import EvalSampleLimit, EvalSampleReductions, eval_error
|
54
|
-
from inspect_ai.log.
|
55
|
+
from inspect_ai.log._recorders.types import SampleSummary
|
56
|
+
from inspect_ai.log._samples import (
|
57
|
+
active_sample,
|
58
|
+
)
|
55
59
|
from inspect_ai.log._transcript import (
|
56
60
|
ErrorEvent,
|
57
61
|
SampleInitEvent,
|
58
62
|
SampleLimitEvent,
|
59
63
|
ScoreEvent,
|
60
64
|
StepEvent,
|
65
|
+
Transcript,
|
61
66
|
transcript,
|
62
67
|
)
|
63
68
|
from inspect_ai.model import (
|
@@ -264,8 +269,13 @@ async def task_run(options: TaskRunOptions) -> EvalLog:
|
|
264
269
|
|
265
270
|
# track when samples complete and update progress as we go
|
266
271
|
progress_results: list[dict[str, SampleScore]] = []
|
272
|
+
|
273
|
+
def update_metrics(metrics: list[TaskDisplayMetric]) -> None:
|
274
|
+
td.update_metrics(metrics)
|
275
|
+
logger.update_metrics(metrics)
|
276
|
+
|
267
277
|
update_metrics_display = update_metrics_display_fn(
|
268
|
-
|
278
|
+
update_metrics,
|
269
279
|
display_metrics=profile.eval_config.score_display is not False,
|
270
280
|
)
|
271
281
|
|
@@ -423,7 +433,7 @@ async def task_run(options: TaskRunOptions) -> EvalLog:
|
|
423
433
|
|
424
434
|
|
425
435
|
def update_metrics_display_fn(
|
426
|
-
|
436
|
+
update_fn: Callable[[list[TaskDisplayMetric]], None],
|
427
437
|
initial_interval: float = 0,
|
428
438
|
min_interval: float = 0.9,
|
429
439
|
display_metrics: bool = True,
|
@@ -463,7 +473,7 @@ def update_metrics_display_fn(
|
|
463
473
|
)
|
464
474
|
|
465
475
|
# Name, reducer, value
|
466
|
-
task_metrics = []
|
476
|
+
task_metrics: list[TaskDisplayMetric] = []
|
467
477
|
if len(results.scores) > 0:
|
468
478
|
for score in results.scores:
|
469
479
|
for key, metric in score.metrics.items():
|
@@ -475,7 +485,7 @@ def update_metrics_display_fn(
|
|
475
485
|
reducer=score.reducer,
|
476
486
|
)
|
477
487
|
)
|
478
|
-
|
488
|
+
update_fn(task_metrics)
|
479
489
|
|
480
490
|
# determine how long to wait before recomputing metrics
|
481
491
|
time_end = time.perf_counter()
|
@@ -516,7 +526,7 @@ async def task_run_sample(
|
|
516
526
|
|
517
527
|
# log if requested
|
518
528
|
if logger:
|
519
|
-
await logger.
|
529
|
+
await logger.complete_sample(previous_sample, flush=False)
|
520
530
|
|
521
531
|
# return score
|
522
532
|
sample_scores = (
|
@@ -539,10 +549,19 @@ async def task_run_sample(
|
|
539
549
|
semaphore if semaphore else contextlib.nullcontext()
|
540
550
|
)
|
541
551
|
|
552
|
+
# validate that we have sample_id (mostly for the typechecker)
|
553
|
+
sample_id = sample.id
|
554
|
+
if sample_id is None:
|
555
|
+
raise ValueError("sample must have id to run")
|
556
|
+
|
542
557
|
# initialise subtask and scoring context
|
543
558
|
init_sample_model_usage()
|
544
559
|
set_sample_state(state)
|
545
|
-
sample_transcript = init_subtask(SAMPLE_SUBTASK, state.store)
|
560
|
+
sample_transcript: Transcript = init_subtask(SAMPLE_SUBTASK, state.store)
|
561
|
+
if logger:
|
562
|
+
sample_transcript._subscribe(
|
563
|
+
lambda event: logger.log_sample_event(sample_id, state.epoch, event)
|
564
|
+
)
|
546
565
|
if scorers:
|
547
566
|
init_scoring_context(scorers, Target(sample.target))
|
548
567
|
|
@@ -626,6 +645,28 @@ async def task_run_sample(
|
|
626
645
|
# mark started
|
627
646
|
active.started = datetime.now().timestamp()
|
628
647
|
|
648
|
+
if logger is not None:
|
649
|
+
await logger.start_sample(
|
650
|
+
SampleSummary(
|
651
|
+
id=sample_id,
|
652
|
+
epoch=state.epoch,
|
653
|
+
input=sample.input,
|
654
|
+
target=sample.target,
|
655
|
+
)
|
656
|
+
)
|
657
|
+
|
658
|
+
# sample init event (remove file bodies as they have content or absolute paths)
|
659
|
+
event_sample = sample.model_copy(
|
660
|
+
update=dict(files={k: "" for k in sample.files.keys()})
|
661
|
+
if sample.files
|
662
|
+
else None
|
663
|
+
)
|
664
|
+
transcript()._event(
|
665
|
+
SampleInitEvent(
|
666
|
+
sample=event_sample, state=state_jsonable(state)
|
667
|
+
)
|
668
|
+
)
|
669
|
+
|
629
670
|
# set progress for plan then run it
|
630
671
|
state = await plan(state, generate)
|
631
672
|
|
@@ -824,7 +865,7 @@ async def log_sample(
|
|
824
865
|
id = sample.id
|
825
866
|
if id is None:
|
826
867
|
raise ValueError(
|
827
|
-
f"Samples without IDs cannot be logged: {sample
|
868
|
+
f"Samples without IDs cannot be logged: {to_json_str_safe(sample)}"
|
828
869
|
)
|
829
870
|
|
830
871
|
# construct sample for logging
|
@@ -866,7 +907,7 @@ async def log_sample(
|
|
866
907
|
limit=limit,
|
867
908
|
)
|
868
909
|
|
869
|
-
await logger.
|
910
|
+
await logger.complete_sample(condense_sample(eval_sample, log_images), flush=True)
|
870
911
|
|
871
912
|
|
872
913
|
async def resolve_dataset(
|
inspect_ai/_util/constants.py
CHANGED
@@ -25,8 +25,10 @@ ALL_LOG_LEVELS = [
|
|
25
25
|
]
|
26
26
|
DEFAULT_LOG_LEVEL = "warning"
|
27
27
|
DEFAULT_LOG_LEVEL_TRANSCRIPT = "info"
|
28
|
+
DEFAULT_LOG_SHARED = 10
|
28
29
|
ALL_LOG_FORMATS = ["eval", "json"]
|
29
30
|
DEFAULT_LOG_FORMAT: Literal["eval", "json"] = "eval"
|
31
|
+
JSON_LOG_FORMAT = "json"
|
30
32
|
EVAL_LOG_FORMAT = "eval"
|
31
33
|
DEFAULT_DISPLAY = "full"
|
32
34
|
LOG_SCHEMA_VERSION = 2
|
inspect_ai/_util/file.py
CHANGED
@@ -13,7 +13,7 @@ from urllib.parse import urlparse
|
|
13
13
|
import fsspec # type: ignore # type: ignore
|
14
14
|
from fsspec.core import split_protocol # type: ignore # type: ignore
|
15
15
|
from fsspec.implementations.local import make_path_posix # type: ignore
|
16
|
-
from pydantic import BaseModel
|
16
|
+
from pydantic import BaseModel, Field
|
17
17
|
from s3fs import S3FileSystem # type: ignore
|
18
18
|
from shortuuid import uuid
|
19
19
|
|
@@ -158,6 +158,9 @@ class FileInfo(BaseModel):
|
|
158
158
|
mtime: float | None
|
159
159
|
"""File modification time (None if the file is a directory on S3)."""
|
160
160
|
|
161
|
+
etag: str | None = Field(default=None)
|
162
|
+
"""Etag (provided by some remote filesystems)"""
|
163
|
+
|
161
164
|
|
162
165
|
class FileSystem:
|
163
166
|
def __init__(self, fs: Any) -> None:
|
@@ -178,6 +181,9 @@ class FileSystem:
|
|
178
181
|
) -> None:
|
179
182
|
self.fs.rm(path, recursive=recursive, maxdepth=maxdepth)
|
180
183
|
|
184
|
+
def mv(self, lpath: str, rpath: str) -> None:
|
185
|
+
self.fs.mv(lpath, rpath)
|
186
|
+
|
181
187
|
def mkdir(self, path: str, exist_ok: bool = False) -> None:
|
182
188
|
if self.is_s3():
|
183
189
|
# try to avoid calling create_bucket on s3 filesystems (as that requires distinct
|
@@ -199,6 +205,9 @@ class FileSystem:
|
|
199
205
|
def info(self, path: str, **kwargs: dict[str, Any]) -> FileInfo:
|
200
206
|
return self._file_info(self.fs.info(path, **kwargs))
|
201
207
|
|
208
|
+
def path_as_uri(self, path: str) -> str:
|
209
|
+
return str(self.fs.unstrip_protocol(path))
|
210
|
+
|
202
211
|
def ls(
|
203
212
|
self, path: str, recursive: bool = False, **kwargs: dict[str, Any]
|
204
213
|
) -> list[FileInfo]:
|
@@ -267,11 +276,18 @@ class FileSystem:
|
|
267
276
|
else:
|
268
277
|
file["mtime"] = None
|
269
278
|
|
279
|
+
# S3 filesystems provided an ETag
|
280
|
+
if "ETag" in file.keys():
|
281
|
+
etag: str | None = file["ETag"].strip('"')
|
282
|
+
else:
|
283
|
+
etag = None
|
284
|
+
|
270
285
|
return FileInfo(
|
271
286
|
name=file["name"],
|
272
287
|
type=file["type"],
|
273
288
|
size=file["size"],
|
274
289
|
mtime=file["mtime"],
|
290
|
+
etag=etag,
|
275
291
|
)
|
276
292
|
|
277
293
|
|
inspect_ai/_util/json.py
CHANGED
@@ -6,7 +6,9 @@ from typing import (
|
|
6
6
|
|
7
7
|
import jsonpatch
|
8
8
|
from pydantic import BaseModel, Field, JsonValue
|
9
|
-
from pydantic_core import to_jsonable_python
|
9
|
+
from pydantic_core import to_json, to_jsonable_python
|
10
|
+
|
11
|
+
from inspect_ai.util._json import JSONType
|
10
12
|
|
11
13
|
|
12
14
|
def jsonable_python(x: Any) -> Any:
|
@@ -23,6 +25,39 @@ def jsonable_dict(x: Any) -> dict[str, JsonValue]:
|
|
23
25
|
)
|
24
26
|
|
25
27
|
|
28
|
+
def to_json_safe(x: Any) -> bytes:
|
29
|
+
return to_json(value=x, indent=2, exclude_none=True, fallback=lambda _x: None)
|
30
|
+
|
31
|
+
|
32
|
+
def to_json_str_safe(x: Any) -> str:
|
33
|
+
return to_json_safe(x).decode()
|
34
|
+
|
35
|
+
|
36
|
+
def python_type_to_json_type(python_type: str | None) -> JSONType:
|
37
|
+
match python_type:
|
38
|
+
case "str":
|
39
|
+
return "string"
|
40
|
+
case "int":
|
41
|
+
return "integer"
|
42
|
+
case "float":
|
43
|
+
return "number"
|
44
|
+
case "bool":
|
45
|
+
return "boolean"
|
46
|
+
case "list":
|
47
|
+
return "array"
|
48
|
+
case "dict":
|
49
|
+
return "object"
|
50
|
+
case "None":
|
51
|
+
return "null"
|
52
|
+
# treat 'unknown' as string as anything can be converted to string
|
53
|
+
case None:
|
54
|
+
return "string"
|
55
|
+
case _:
|
56
|
+
raise ValueError(
|
57
|
+
f"Unsupported type: {python_type} for Python to JSON conversion."
|
58
|
+
)
|
59
|
+
|
60
|
+
|
26
61
|
class JsonChange(BaseModel):
|
27
62
|
"""Describes a change to data using JSON Patch format."""
|
28
63
|
|
inspect_ai/_view/server.py
CHANGED
@@ -5,7 +5,7 @@ import os
|
|
5
5
|
import urllib.parse
|
6
6
|
from logging import LogRecord, getLogger
|
7
7
|
from pathlib import Path
|
8
|
-
from typing import Any, AsyncIterator, Awaitable, Callable, Literal, cast
|
8
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, Literal, TypeVar, cast
|
9
9
|
|
10
10
|
import fsspec # type: ignore
|
11
11
|
from aiohttp import web
|
@@ -25,6 +25,7 @@ from inspect_ai.log._file import (
|
|
25
25
|
read_eval_log_async,
|
26
26
|
read_eval_log_headers_async,
|
27
27
|
)
|
28
|
+
from inspect_ai.log._recorders.buffer.buffer import sample_buffer
|
28
29
|
|
29
30
|
from .notify import view_last_eval_time
|
30
31
|
|
@@ -131,6 +132,60 @@ def view_server(
|
|
131
132
|
)
|
132
133
|
return web.json_response(actions)
|
133
134
|
|
135
|
+
@routes.get("/api/pending-samples")
|
136
|
+
async def api_pending_samples(request: web.Request) -> web.Response:
|
137
|
+
# log file requested
|
138
|
+
file = query_param_required("log", request, str)
|
139
|
+
|
140
|
+
file = urllib.parse.unquote(file)
|
141
|
+
validate_log_file_request(file)
|
142
|
+
|
143
|
+
# see if there is an etag
|
144
|
+
client_etag = request.headers.get("If-None-Match")
|
145
|
+
|
146
|
+
# get samples and respond
|
147
|
+
buffer = sample_buffer(file)
|
148
|
+
samples = buffer.get_samples(client_etag)
|
149
|
+
if samples == "NotModified":
|
150
|
+
return web.Response(status=304)
|
151
|
+
elif samples is None:
|
152
|
+
return web.Response(status=404)
|
153
|
+
else:
|
154
|
+
return web.Response(
|
155
|
+
body=samples.model_dump_json(), headers={"ETag": samples.etag}
|
156
|
+
)
|
157
|
+
|
158
|
+
@routes.get("/api/pending-sample-data")
|
159
|
+
async def api_sample_events(request: web.Request) -> web.Response:
|
160
|
+
# log file requested
|
161
|
+
file = query_param_required("log", request, str)
|
162
|
+
|
163
|
+
file = urllib.parse.unquote(file)
|
164
|
+
validate_log_file_request(file)
|
165
|
+
|
166
|
+
# sample id information
|
167
|
+
id = query_param_required("id", request, str)
|
168
|
+
epoch = query_param_required("epoch", request, int)
|
169
|
+
|
170
|
+
# get sync info
|
171
|
+
after_event_id = query_param_optional("last-event-id", request, int)
|
172
|
+
after_attachment_id = query_param_optional("after-attachment-id", request, int)
|
173
|
+
|
174
|
+
# get samples and responsd
|
175
|
+
buffer = sample_buffer(file)
|
176
|
+
sample_data = buffer.get_sample_data(
|
177
|
+
id=id,
|
178
|
+
epoch=epoch,
|
179
|
+
after_event_id=after_event_id,
|
180
|
+
after_attachment_id=after_attachment_id,
|
181
|
+
)
|
182
|
+
|
183
|
+
# respond
|
184
|
+
if sample_data is None:
|
185
|
+
return web.Response(status=404)
|
186
|
+
else:
|
187
|
+
return web.Response(body=sample_data.model_dump_json())
|
188
|
+
|
134
189
|
# optional auth middleware
|
135
190
|
@web.middleware
|
136
191
|
async def authorize(
|
@@ -430,3 +485,60 @@ async def async_fileystem(
|
|
430
485
|
else:
|
431
486
|
options.update({"asynchronous": True, "loop": asyncio.get_event_loop()})
|
432
487
|
yield fsspec.filesystem(protocol, **options)
|
488
|
+
|
489
|
+
|
490
|
+
T = TypeVar("T") # Define type variable
|
491
|
+
|
492
|
+
|
493
|
+
def query_param_required(
|
494
|
+
key: str, request: web.Request, converter: Callable[[str], T]
|
495
|
+
) -> T:
|
496
|
+
"""
|
497
|
+
Generic parameter validation function.
|
498
|
+
|
499
|
+
Args:
|
500
|
+
key: Parameter key to look up
|
501
|
+
request: aiohttp Request object
|
502
|
+
converter: Function to convert the string parameter to type T
|
503
|
+
|
504
|
+
Returns:
|
505
|
+
Converted parameter value of type T
|
506
|
+
|
507
|
+
Raises:
|
508
|
+
HTTPBadRequest: If parameter is missing or invalid
|
509
|
+
"""
|
510
|
+
value = request.query.get(key)
|
511
|
+
if value is None:
|
512
|
+
raise web.HTTPBadRequest(text=f"Missing parameter {key}")
|
513
|
+
|
514
|
+
try:
|
515
|
+
return converter(value)
|
516
|
+
except ValueError:
|
517
|
+
raise web.HTTPBadRequest(text=f"Invalid value {value} for {key}")
|
518
|
+
|
519
|
+
|
520
|
+
def query_param_optional(
|
521
|
+
key: str, request: web.Request, converter: Callable[[str], T]
|
522
|
+
) -> T | None:
|
523
|
+
"""
|
524
|
+
Generic parameter validation function.
|
525
|
+
|
526
|
+
Args:
|
527
|
+
key: Parameter key to look up
|
528
|
+
request: aiohttp Request object
|
529
|
+
converter: Function to convert the string parameter to type T
|
530
|
+
|
531
|
+
Returns:
|
532
|
+
Converted parameter value of type T
|
533
|
+
|
534
|
+
Raises:
|
535
|
+
HTTPBadRequest: If parameter is missing or invalid
|
536
|
+
"""
|
537
|
+
value = request.query.get(key)
|
538
|
+
if value is None:
|
539
|
+
return None
|
540
|
+
|
541
|
+
try:
|
542
|
+
return converter(value)
|
543
|
+
except ValueError:
|
544
|
+
raise web.HTTPBadRequest(text=f"Invalid value {value} for {key}")
|