From 51551b1d480684d75236fb4d6707c9b7f9996736 Mon Sep 17 00:00:00 2001 From: aisi-inspect <166920645+aisi-inspect@users.noreply.github.com> Date: Sun, 16 Jun 2024 15:37:52 +0000 Subject: [PATCH] release v0.3.15 --- src/inspect_ai/_view/www/App.css | 22 ++++ src/inspect_ai/_view/www/App.mjs | 17 +-- .../_view/www/libs/prism/prism-dark.css | 108 +++++++++++++++++- .../_view/www/src/components/ChatView.mjs | 2 +- .../_view/www/src/samples/SampleDisplay.mjs | 16 +-- .../_view/www/src/samples/SampleList.mjs | 3 +- .../_tool/environment/docker/cleanup.py | 7 +- .../_tool/environment/docker/compose.py | 8 +- .../solver/_tool/environment/docker/config.py | 60 +++++----- .../solver/_tool/environment/docker/docker.py | 76 ++++++------ .../solver/_tool/environment/docker/util.py | 4 - tools/vscode/tools/ts-to-mjs/yarn.lock | 16 +-- 12 files changed, 228 insertions(+), 111 deletions(-) diff --git a/src/inspect_ai/_view/www/App.css b/src/inspect_ai/_view/www/App.css index 6d5a2dd12..701347fc7 100644 --- a/src/inspect_ai/_view/www/App.css +++ b/src/inspect_ai/_view/www/App.css @@ -441,6 +441,20 @@ pre[class*=language-] { margin-bottom: 0.75em; } + +.sample-answer .markdown-content h1, +.sample-answer .markdown-content h2, +.sample-answer .markdown-content h3, +.sample-answer .markdown-content h4, +.sample-answer .markdown-content h5, +.sample-answer .markdown-content h6 { + font-size: 1em; + font-weight: 400; + margin-top: 0em; + margin-bottom: 0em; +} + + .accordion-item:not(.no-highlight) .collapse.show.highlight-when-expanded, .accordion-item:not(.no-highlight) .collapsing.highlight-when-expanded, .accordion-item:not(.no-highlight) .accordion-button[aria-expanded="true"].highlight-when-expanded { @@ -604,4 +618,12 @@ table.table.table-sm td { .expandable-panel pre { overflow: unset; +} + +.tool-output { + background-color: #f8f8f8; +} + +.vscode-dark .tool-output { + background-color: #333333; } \ No newline at end of file diff --git a/src/inspect_ai/_view/www/App.mjs b/src/inspect_ai/_view/www/App.mjs index 52282f44a..a7e2ddc0e 100644 --- a/src/inspect_ai/_view/www/App.mjs +++ b/src/inspect_ai/_view/www/App.mjs @@ -1,5 +1,5 @@ import { html } from "htm/preact"; -import { useCallback, useState, useEffect, useRef } from "preact/hooks"; +import { useCallback, useState, useEffect, useMemo, useRef } from "preact/hooks"; // Registration component import "./src/Register.mjs"; @@ -267,13 +267,15 @@ export function App() { /> `; - const progress = () => { + const progress = useMemo(() => { if (status.loading) { return html`<${ProgressBar}/>`; + } else { + return undefined; } - } + }, [status]); - const workspace = () => { + const workspace = useMemo(() => { if (status.error) { return html`<${ErrorPanel} title="An error occurred while loading this task." @@ -289,13 +291,14 @@ export function App() { offcanvas=${offcanvas} />` } - } + }, [logs, currentLog, selected, fullScreen, offcanvas, status]); + return html` <${AppErrorBoundary}>
${appEnvelope} - ${progress()} - ${workspace()} + ${progress} + ${workspace}
`; diff --git a/src/inspect_ai/_view/www/libs/prism/prism-dark.css b/src/inspect_ai/_view/www/libs/prism/prism-dark.css index 74b7e0701..1e5b64ea7 100644 --- a/src/inspect_ai/_view/www/libs/prism/prism-dark.css +++ b/src/inspect_ai/_view/www/libs/prism/prism-dark.css @@ -1,3 +1,109 @@ /* PrismJS 1.29.0 https://prismjs.com/download.html#themes=prism-dark&languages=markup+css+clike+javascript+bash+python */ -code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;text-shadow:0 -.1em .2em #000;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}:not(pre)>code[class*=language-],pre[class*=language-]{background:#4c3f33}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border:.3em solid #7a6651;border-radius:.5em;box-shadow:1px 1px .5em #000 inset}:not(pre)>code[class*=language-]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid #7a6651;box-shadow:1px 1px .3em -.1em #000 inset;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#997f66}.token.punctuation{opacity:.7}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.number,.token.property,.token.symbol,.token.tag{color:#d1939e}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#bce051}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f4b73d}.token.atrule,.token.attr-value,.token.keyword{color:#d1939e}.token.important,.token.regex{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red} +/* DO NOT SIMPLY REPLACE - NOTE the '.vscode-dark' preview on selectors + which is used to target this at vscode dark mode. Be sure to replace this + when updating */ +.vscode-dark code[class*="language-"], +.vscode-dark pre[class*="language-"] { + color: #fff; + background: 0 0; + text-shadow: 0 -0.1em 0.2em #000; + font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} +@media print { + .vscode-dark code[class*="language-"], + .vscode-dark pre[class*="language-"] { + text-shadow: none; + } +} +.vscode-dark :not(pre) > code[class*="language-"], +.vscode-dark pre[class*="language-"] { + background: #4c3f33; +} +.vscode-dark pre[class*="language-"] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + border: 0.3em solid #7a6651; + border-radius: 0.5em; + box-shadow: 1px 1px 0.5em #000 inset; +} +.vscode-dark :not(pre) > code[class*="language-"] { + padding: 0.15em 0.2em 0.05em; + border-radius: 0.3em; + border: 0.13em solid #7a6651; + box-shadow: 1px 1px 0.3em -0.1em #000 inset; + white-space: normal; +} +.vscode-dark .token.cdata, +.vscode-dark .token.comment, +.vscode-dark .token.doctype, +.vscode-dark .token.prolog { + color: #997f66; +} +.vscode-dark .token.punctuation { + opacity: 0.7; +} +.vscode-dark .token.namespace { + opacity: 0.7; +} +.vscode-dark .token.boolean, +.vscode-dark .token.constant, +.vscode-dark .token.number, +.vscode-dark .token.property, +.vscode-dark .token.symbol, +.vscode-dark .token.tag { + color: #d1939e; +} +.vscode-dark .token.attr-name, +.vscode-dark .token.builtin, +.vscode-dark .token.char, +.vscode-dark .token.inserted, +.vscode-dark .token.selector, +.vscode-dark .token.string { + color: #bce051; +} +.vscode-dark .language-css .token.string, +.vscode-dark .style .token.string, +.vscode-dark .token.entity, +.vscode-dark .token.operator, +.vscode-dark .token.url, +.vscode-dark .token.variable { + color: #f4b73d; +} +.vscode-dark .token.atrule, +.vscode-dark .token.attr-value, +.vscode-dark .token.keyword { + color: #d1939e; +} +.vscode-dark .token.important, +.vscode-dark .token.regex { + color: #e90; +} +.vscode-dark .token.bold, +.vscode-dark .token.important { + font-weight: 700; +} +.vscode-dark .token.italic { + font-style: italic; +} +.vscode-dark .token.entity { + cursor: help; +} +.vscode-dark .token.deleted { + color: red; +} diff --git a/src/inspect_ai/_view/www/src/components/ChatView.mjs b/src/inspect_ai/_view/www/src/components/ChatView.mjs index 0553e3f84..e2fce5db6 100644 --- a/src/inspect_ai/_view/www/src/components/ChatView.mjs +++ b/src/inspect_ai/_view/www/src/components/ChatView.mjs @@ -201,9 +201,9 @@ export const ToolInput = ({ type, contents }) => { }, [toolInputRef.current, type, contents]); return html`
 {
   return sampleMetadatas;
 };
 
-const inputString = (input) => {
-  if (typeof input === "string") {
-    return input;
-  } else {
-    return input.map((inp) => {
-      if (typeof inp === "string") {
-        return inp;
-      } else {
-        return inp.content;
-      }
-    });
-  }
-};
-
 const SampleSummary = ({ id, sample, sampleDescriptor }) => {
   const input =
     sampleDescriptor?.messageShape.input > 0
diff --git a/src/inspect_ai/_view/www/src/samples/SampleList.mjs b/src/inspect_ai/_view/www/src/samples/SampleList.mjs
index 739b72b99..53724f280 100644
--- a/src/inspect_ai/_view/www/src/samples/SampleList.mjs
+++ b/src/inspect_ai/_view/www/src/samples/SampleList.mjs
@@ -11,6 +11,7 @@ import {
 } from "../utils/Format.mjs";
 import { EmptyPanel } from "../components/EmptyPanel.mjs";
 import { VirtualList } from "../components/VirtualList.mjs";
+import { inputString } from "../utils/Format.mjs"
 
 const kSampleHeight = 82;
 const kSeparatorHeight = 20;
@@ -240,7 +241,7 @@ const SampleRow = ({
           ...cellStyle,
         }}
       >
-        ${sample.input}
+        ${inputString(sample.input)}
       
       
None: # bring down services await compose_down(project=project, quiet=quiet) - # remove temp dir - project.temp_dir.cleanup() - # remove the project from the list of running projects running_projects().remove(project) @@ -53,6 +51,9 @@ async def project_cleanup_shutdown() -> None: f"Error cleaning up compose containers: {exception_message(result)}" ) + # cleanup auto config + auto_config_cleanup() + def running_projects() -> list[ComposeProject]: return _running_projects.get() diff --git a/src/inspect_ai/solver/_tool/environment/docker/compose.py b/src/inspect_ai/solver/_tool/environment/docker/compose.py index 23ccd0131..0744616d4 100644 --- a/src/inspect_ai/solver/_tool/environment/docker/compose.py +++ b/src/inspect_ai/solver/_tool/environment/docker/compose.py @@ -6,6 +6,7 @@ from inspect_ai.util._context.subprocess import ExecResult, subprocess +from .config import auto_config from .util import ComposeProject, tools_log logger = getLogger(__name__) @@ -129,7 +130,7 @@ async def compose_exec( async def compose_services(project: ComposeProject) -> dict[str, ComposeService]: result = await compose_command(["config"], project=project) if not result.success: - raise RuntimeError("Error reading docker config: {result.stderr}") + raise RuntimeError(f"Error reading docker config: {result.stderr}") return cast(dict[str, ComposeService], yaml.safe_load(result.stdout)["services"]) @@ -193,8 +194,9 @@ async def compose_command( compose_command = compose_command + ["--project-name", project.name] # add config file if specified - if project.config: - compose_command = compose_command + ["-f", project.config] + config = project.config if project.config else await auto_config() + if config: + compose_command = compose_command + ["-f", config] # build final command compose_command = compose_command + command diff --git a/src/inspect_ai/solver/_tool/environment/docker/config.py b/src/inspect_ai/solver/_tool/environment/docker/config.py index d1ca8958a..fe0c28e60 100644 --- a/src/inspect_ai/solver/_tool/environment/docker/config.py +++ b/src/inspect_ai/solver/_tool/environment/docker/config.py @@ -1,5 +1,4 @@ import os -import tempfile from logging import getLogger from pathlib import Path @@ -8,18 +7,28 @@ logger = getLogger(__name__) -async def auto_config(temp_dir: str) -> str | None: +async def auto_config() -> str | None: # compose file provides all the config we need if has_compose_file(): return None + # temporary auto-compose + if has_auto_compose_file(): + return AUTO_COMPOSE_YAML + # dockerfile just needs a compose.yaml synthesized elif has_dockerfile(): - return await dockerfile_compose(Path(), temp_dir) + return await auto_compose_file(COMPOSE_DOCKERFILE_YAML) # otherwise provide a generic python container else: - return await generic_container_compose(temp_dir) + return await auto_compose_file(COMPOSE_GENERIC_YAML) + + +def auto_config_cleanup() -> None: + # if we have an auto-generated .compose.yaml then clean it up + if has_auto_compose_file(): + Path(AUTO_COMPOSE_YAML).unlink(True) def has_compose_file() -> bool: @@ -39,37 +48,32 @@ def has_dockerfile() -> bool: return os.path.isfile("Dockerfile") -# Our default compose.yaml -COMPOSE_GENERIC_YAML = """ +def has_auto_compose_file() -> bool: + return os.path.isfile(AUTO_COMPOSE_YAML) + + +AUTO_COMPOSE_YAML = ".compose.yaml" + +COMPOSE_COMMENT = """# inspect auto-generated docker compose file +# (will be removed when task is complete)""" + +COMPOSE_GENERIC_YAML = f"""{COMPOSE_COMMENT} services: default: image: "python:3.12-bookworm" - command: tail -f /dev/null + command: "tail -f /dev/null" """ - -async def generic_container_compose(directory: str) -> str: - return await default_compose_file(directory, COMPOSE_GENERIC_YAML) - - -async def dockerfile_compose(context: Path, directory: str) -> str: - # Template for a DockerFile - compose_dockerfile_yaml = f""" +COMPOSE_DOCKERFILE_YAML = f"""{COMPOSE_COMMENT} services: default: build: - context: {context.resolve().as_posix()} - command: tail -f /dev/null - """ - - return await default_compose_file(directory, compose_dockerfile_yaml) + context: "." + command: "tail -f /dev/null" +""" -# Provide the path to a default compose file -async def default_compose_file(directory: str, contents: str) -> str: - with tempfile.NamedTemporaryFile( - dir=directory, suffix=".yaml", delete=False - ) as compose_file: - async with aiofiles.open(compose_file.name, "w", encoding="utf-8") as f: - await f.write(contents) - return compose_file.name +async def auto_compose_file(contents: str) -> str: + async with aiofiles.open(AUTO_COMPOSE_YAML, "w", encoding="utf-8") as f: + await f.write(contents) + return AUTO_COMPOSE_YAML diff --git a/src/inspect_ai/solver/_tool/environment/docker/docker.py b/src/inspect_ai/solver/_tool/environment/docker/docker.py index 820653746..14c97e462 100644 --- a/src/inspect_ai/solver/_tool/environment/docker/docker.py +++ b/src/inspect_ai/solver/_tool/environment/docker/docker.py @@ -26,7 +26,6 @@ compose_services, compose_up, ) -from .config import auto_config from .util import ComposeProject, task_project_name, tools_log logger = getLogger(__name__) @@ -39,41 +38,41 @@ async def task_init(cls, task_name: str, config: str | None) -> None: # intialize project cleanup project_cleanup_startup() - # create project - temp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True) - project = ComposeProject( - name=task_project_name(task_name), - config=config if config else await auto_config(temp_dir.name), - env=dict(), - working_dir="/", - temp_dir=temp_dir, - ) - - # build containers which are out of date - await compose_build(project) - - # cleanup images created during build - await compose_cleanup_images(project) + try: + # create project + project = ComposeProject( + name=task_project_name(task_name), + config=config, + env=dict(), + working_dir="/", + ) - # pull any remote images - services = await compose_services(project) - for name, service in services.items(): - if ( - service.get("build", None) is None - and service.get("x-local", None) is None - ): - pull_result = await compose_pull(name, project) - if not pull_result.success: - image = service.get("image", "(unknown)") - logger.error( - f"Failed to pull docker image '{image}' from remote registry. If this is a locally built image add 'x-local: true' to the the service definition to prevent this error." - ) - - # cleanup temp_dir - temp_dir.cleanup() - - # provide some space above task display - print("") + # build containers which are out of date + await compose_build(project) + + # cleanup images created during build + await compose_cleanup_images(project) + + # pull any remote images + services = await compose_services(project) + for name, service in services.items(): + if ( + service.get("build", None) is None + and service.get("x-local", None) is None + ): + pull_result = await compose_pull(name, project) + if not pull_result.success: + image = service.get("image", "(unknown)") + logger.error( + f"Failed to pull docker image '{image}' from remote registry. If this is a locally built image add 'x-local: true' to the the service definition to prevent this error." + ) + + # provide some space above task display + print("") + + except BaseException as ex: + await project_cleanup_shutdown() + raise ex @classmethod async def task_cleanup(cls, task_name: str, config: str | None) -> None: @@ -92,13 +91,8 @@ async def sample_init( env[f"SAMPLE_METADATA_{key.replace(' ', '_').upper()}"] = str(value) # create project - temp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True) project = ComposeProject( - name=task_project_name(task_name), - config=config if config else await auto_config(temp_dir.name), - env=env, - working_dir="/", - temp_dir=temp_dir, + name=task_project_name(task_name), config=config, env=env, working_dir="/" ) # enumerate the services that will be created diff --git a/src/inspect_ai/solver/_tool/environment/docker/util.py b/src/inspect_ai/solver/_tool/environment/docker/util.py index d45e4bab1..46fe48b85 100644 --- a/src/inspect_ai/solver/_tool/environment/docker/util.py +++ b/src/inspect_ai/solver/_tool/environment/docker/util.py @@ -1,4 +1,3 @@ -import tempfile from dataclasses import dataclass from logging import getLogger @@ -15,7 +14,6 @@ class ComposeProject: config: str | None env: dict[str, str] working_dir: str - temp_dir: tempfile.TemporaryDirectory[str] def __init__( self, @@ -23,13 +21,11 @@ def __init__( config: str | None, env: dict[str, str], working_dir: str, - temp_dir: tempfile.TemporaryDirectory[str], ) -> None: self.name = name self.config = config self.env = env self.working_dir = working_dir - self.temp_dir = temp_dir def __eq__(self, other: object) -> bool: if not isinstance(other, ComposeProject): diff --git a/tools/vscode/tools/ts-to-mjs/yarn.lock b/tools/vscode/tools/ts-to-mjs/yarn.lock index c28ca1399..505aa198c 100644 --- a/tools/vscode/tools/ts-to-mjs/yarn.lock +++ b/tools/vscode/tools/ts-to-mjs/yarn.lock @@ -1166,11 +1166,11 @@ __metadata: linkType: hard "braces@npm:^3.0.2": - version: 3.0.2 - resolution: "braces@npm:3.0.2" + version: 3.0.3 + resolution: "braces@npm:3.0.3" dependencies: - fill-range: ^7.0.1 - checksum: e2a8e769a863f3d4ee887b5fe21f63193a891c68b612ddb4b68d82d1b5f3ff9073af066c343e9867a393fe4c2555dcb33e89b937195feb9c1613d259edfcd459 + fill-range: ^7.1.1 + checksum: b95aa0b3bd909f6cd1720ffcf031aeaf46154dd88b4da01f9a1d3f7ea866a79eba76a6d01cbc3c422b2ee5cdc39a4f02491058d5df0d7bf6e6a162a832df1f69 languageName: node linkType: hard @@ -1633,12 +1633,12 @@ __metadata: languageName: node linkType: hard -"fill-range@npm:^7.0.1": - version: 7.0.1 - resolution: "fill-range@npm:7.0.1" +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" dependencies: to-regex-range: ^5.0.1 - checksum: cc283f4e65b504259e64fd969bcf4def4eb08d85565e906b7d36516e87819db52029a76b6363d0f02d0d532f0033c9603b9e2d943d56ee3b0d4f7ad3328ff917 + checksum: b4abfbca3839a3d55e4ae5ec62e131e2e356bf4859ce8480c64c4876100f4df292a63e5bb1618e1d7460282ca2b305653064f01654474aa35c68000980f17798 languageName: node linkType: hard