Spaces:
Running
v0.4: surface ALL task files, not just an allowlist
Browse filesThe previous version hardcoded six file IDs in HarborTask / _read_task_file /
_build_file_tree (task.toml, instruction.md, solution/patch.diff,
solution/solve.sh, tests/test.sh, environment/Dockerfile). Anything else in a
task dir (e.g. tests/grader.py, environment/pull_bucket.py, helper modules)
was downloaded by snapshot_download but never shown in the UI.
Generalized:
- HarborTask gains a 'files: dict[str, str]' field β every readable text
file under the task dir, keyed by POSIX-style relative path.
- parse.py: new _discover_task_files() walks task_dir.rglob('*'), skips
binaries (UTF-8 decode failures), hidden noise (.DS_Store, .git,
__pycache__, .cache, node_modules, .venv, *_cache), and oversized files
(>512 KiB). Convenience fields (instruction_md, oracle_patch, β¦) kept
populated from the same dict for backwards compat with anything that
reaches in by name.
- app.py: _read_task_file becomes a dict lookup with the two special cases
preserved (task.toml uses task_toml_raw; instruction.md falls back to
the inline 'task.instruction' field).
- app.py: _build_file_tree walks task.files, groups by top-level folder,
orders Overview β top-level files β folders alphabetically β children
alphabetically. Tasks with extra files (e.g. data-agent's grader.py +
pull_bucket.py) now show every artifact faithfully.
- app.py: _EXTENSION_LANGUAGE extended for .py, .md, .json, .yaml/.yml,
.ini/.cfg, .conf, .html/.css/.js/.ts, .txt/.csv/.tsv so non-shell files
render with the right Prism token.
Backwards compatible: existing datasets (e.g. AdithyaSK/click-r2e-v082post1)
keep working β same six files surface, just now via the new path.
- app.py +60 -39
- viewer/parse.py +56 -5
|
@@ -51,6 +51,22 @@ _EXTENSION_LANGUAGE: dict[str, str] = {
|
|
| 51 |
".patch": "python",
|
| 52 |
".sh": "shell",
|
| 53 |
".bash": "shell",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
|
|
@@ -63,69 +79,74 @@ def _file_language(filename: str) -> str:
|
|
| 63 |
|
| 64 |
|
| 65 |
def _read_task_file(task: HarborTask, file_id: str) -> str | None:
|
| 66 |
-
"""Fetch the content for a file_id; None when the file isn't present.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
if file_id == "task.toml":
|
| 68 |
return task.task_toml_raw or None
|
| 69 |
if file_id == "instruction.md":
|
| 70 |
-
return task.
|
| 71 |
-
|
| 72 |
-
return task.oracle_patch
|
| 73 |
-
if file_id == "solution/solve.sh":
|
| 74 |
-
return task.solve_sh
|
| 75 |
-
if file_id == "tests/test.sh":
|
| 76 |
-
return task.test_sh
|
| 77 |
-
if file_id == "environment/Dockerfile":
|
| 78 |
-
return task.dockerfile
|
| 79 |
-
return None
|
| 80 |
|
| 81 |
|
| 82 |
def _build_file_tree(task: HarborTask) -> tuple[list[tuple[str, str]], dict[str, str]]:
|
| 83 |
"""Render a file-explorer-style choice list for a task.
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
Returns:
|
| 86 |
choices: list of (label, value) tuples for `gr.Radio`. Labels use unicode
|
| 87 |
tree glyphs (π, ββ, ββ) so the radio reads like a file tree.
|
| 88 |
folder_redirects: maps each folder pseudo-id to its first present child
|
| 89 |
file_id so clicking a folder header opens its first file.
|
| 90 |
-
|
| 91 |
-
Order is fixed: Overview first, then top-level files, then folders with
|
| 92 |
-
their children. Folders only appear when they have β₯1 present child.
|
| 93 |
"""
|
| 94 |
choices: list[tuple[str, str]] = [("β Overview", _OVERVIEW)]
|
| 95 |
redirects: dict[str, str] = {}
|
| 96 |
|
| 97 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
if (task.task_toml_raw or "").strip():
|
| 99 |
choices.append(("π task.toml", "task.toml"))
|
| 100 |
-
if (task.
|
| 101 |
choices.append(("π instruction.md", "instruction.md"))
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
| 111 |
folder_id = f"{_FOLDER_PREFIX}{folder}"
|
| 112 |
choices.append((f"π {folder}/", folder_id))
|
| 113 |
-
redirects[folder_id] =
|
| 114 |
-
for i,
|
| 115 |
-
|
|
|
|
|
|
|
| 116 |
choices.append((f" {glyph} {basename}", full_id))
|
| 117 |
|
| 118 |
-
_maybe_folder("solution", [
|
| 119 |
-
("patch.diff", "solution/patch.diff"),
|
| 120 |
-
("solve.sh", "solution/solve.sh"),
|
| 121 |
-
])
|
| 122 |
-
_maybe_folder("tests", [
|
| 123 |
-
("test.sh", "tests/test.sh"),
|
| 124 |
-
])
|
| 125 |
-
_maybe_folder("environment", [
|
| 126 |
-
("Dockerfile", "environment/Dockerfile"),
|
| 127 |
-
])
|
| 128 |
-
|
| 129 |
return choices, redirects
|
| 130 |
|
| 131 |
|
|
|
|
| 51 |
".patch": "python",
|
| 52 |
".sh": "shell",
|
| 53 |
".bash": "shell",
|
| 54 |
+
".py": "python",
|
| 55 |
+
".json": "json",
|
| 56 |
+
".yaml": "yaml",
|
| 57 |
+
".yml": "yaml",
|
| 58 |
+
".md": "markdown",
|
| 59 |
+
".markdown": "markdown",
|
| 60 |
+
".txt": "shell",
|
| 61 |
+
".csv": "shell",
|
| 62 |
+
".tsv": "shell",
|
| 63 |
+
".ini": "yaml",
|
| 64 |
+
".cfg": "yaml",
|
| 65 |
+
".conf": "shell",
|
| 66 |
+
".html": "html",
|
| 67 |
+
".css": "css",
|
| 68 |
+
".js": "javascript",
|
| 69 |
+
".ts": "typescript",
|
| 70 |
}
|
| 71 |
|
| 72 |
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def _read_task_file(task: HarborTask, file_id: str) -> str | None:
|
| 82 |
+
"""Fetch the content for a file_id; None when the file isn't present.
|
| 83 |
+
|
| 84 |
+
task.toml and instruction.md have special handling (task.toml uses the
|
| 85 |
+
pre-captured raw text; instruction.md falls back to the inline `task.instruction`
|
| 86 |
+
field from task.toml when no instruction.md file is on disk). Everything else
|
| 87 |
+
is a direct lookup against the dict populated by walking the task dir.
|
| 88 |
+
"""
|
| 89 |
if file_id == "task.toml":
|
| 90 |
return task.task_toml_raw or None
|
| 91 |
if file_id == "instruction.md":
|
| 92 |
+
return task.files.get("instruction.md") or task.instruction_inline
|
| 93 |
+
return task.files.get(file_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
|
| 96 |
def _build_file_tree(task: HarborTask) -> tuple[list[tuple[str, str]], dict[str, str]]:
|
| 97 |
"""Render a file-explorer-style choice list for a task.
|
| 98 |
|
| 99 |
+
Walks every file discovered under the task dir (via `task.files`) and groups
|
| 100 |
+
them by their first path segment. Order: Overview β top-level files
|
| 101 |
+
(task.toml, instruction.md, anything else) β folders alphabetically with
|
| 102 |
+
their children alphabetically. No hardcoded allowlist β what's on disk is
|
| 103 |
+
what gets shown.
|
| 104 |
+
|
| 105 |
Returns:
|
| 106 |
choices: list of (label, value) tuples for `gr.Radio`. Labels use unicode
|
| 107 |
tree glyphs (π, ββ, ββ) so the radio reads like a file tree.
|
| 108 |
folder_redirects: maps each folder pseudo-id to its first present child
|
| 109 |
file_id so clicking a folder header opens its first file.
|
|
|
|
|
|
|
|
|
|
| 110 |
"""
|
| 111 |
choices: list[tuple[str, str]] = [("β Overview", _OVERVIEW)]
|
| 112 |
redirects: dict[str, str] = {}
|
| 113 |
|
| 114 |
+
# Bucket every discovered file by top-level dir ("" = at task root)
|
| 115 |
+
top_level: list[str] = []
|
| 116 |
+
by_folder: dict[str, list[str]] = {}
|
| 117 |
+
for path in sorted(task.files):
|
| 118 |
+
if "/" in path:
|
| 119 |
+
folder = path.split("/", 1)[0]
|
| 120 |
+
by_folder.setdefault(folder, []).append(path)
|
| 121 |
+
else:
|
| 122 |
+
top_level.append(path)
|
| 123 |
+
|
| 124 |
+
# Top-level files: task.toml first, then instruction.md (with inline
|
| 125 |
+
# fallback), then anything else alphabetically. Order is presentational.
|
| 126 |
if (task.task_toml_raw or "").strip():
|
| 127 |
choices.append(("π task.toml", "task.toml"))
|
| 128 |
+
if (task.files.get("instruction.md") or task.instruction_inline):
|
| 129 |
choices.append(("π instruction.md", "instruction.md"))
|
| 130 |
+
for path in sorted(top_level):
|
| 131 |
+
if path in ("task.toml", "instruction.md"):
|
| 132 |
+
continue # already added
|
| 133 |
+
choices.append((f"π {path}", path))
|
| 134 |
+
|
| 135 |
+
# Folders alphabetically (environment / solution / tests / ...) with
|
| 136 |
+
# children alphabetical within each.
|
| 137 |
+
for folder in sorted(by_folder):
|
| 138 |
+
children = sorted(by_folder[folder])
|
| 139 |
+
if not children:
|
| 140 |
+
continue
|
| 141 |
folder_id = f"{_FOLDER_PREFIX}{folder}"
|
| 142 |
choices.append((f"π {folder}/", folder_id))
|
| 143 |
+
redirects[folder_id] = children[0] # folder header β first child
|
| 144 |
+
for i, full_id in enumerate(children):
|
| 145 |
+
# Show the path *inside* the folder (handles nested subdirs too)
|
| 146 |
+
basename = full_id[len(folder) + 1:] # strip "<folder>/"
|
| 147 |
+
glyph = "ββ" if i == len(children) - 1 else "ββ"
|
| 148 |
choices.append((f" {glyph} {basename}", full_id))
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
return choices, redirects
|
| 151 |
|
| 152 |
|
|
@@ -61,6 +61,14 @@ class HarborTask:
|
|
| 61 |
dockerfile: str | None = None
|
| 62 |
task_toml_raw: str = ""
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
def _read_text(path: Path) -> str | None:
|
| 66 |
"""Read a text file, return None if it doesn't exist."""
|
|
@@ -73,6 +81,42 @@ def _read_text(path: Path) -> str | None:
|
|
| 73 |
return None
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
def _discover_task_roots(dataset_root: Path) -> list[Path]:
|
| 77 |
"""Find every directory under `dataset_root` that contains a `task.toml`.
|
| 78 |
|
|
@@ -136,6 +180,10 @@ def load_task(dataset_root: Path, task_id: str) -> HarborTask:
|
|
| 136 |
if repo2env is not None and not isinstance(repo2env, dict):
|
| 137 |
repo2env = None
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
return HarborTask(
|
| 140 |
id=task_id,
|
| 141 |
root=task_dir,
|
|
@@ -150,10 +198,13 @@ def load_task(dataset_root: Path, task_id: str) -> HarborTask:
|
|
| 150 |
agent_timeout_sec=agent_block.get("timeout_sec"),
|
| 151 |
verifier_timeout_sec=verifier_block.get("timeout_sec"),
|
| 152 |
repo2env=repo2env,
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
| 158 |
task_toml_raw=raw,
|
|
|
|
| 159 |
)
|
|
|
|
| 61 |
dockerfile: str | None = None
|
| 62 |
task_toml_raw: str = ""
|
| 63 |
|
| 64 |
+
# Generic discovery: every readable text file under the task dir,
|
| 65 |
+
# keyed by path relative to the task root. e.g. "tests/grader.py",
|
| 66 |
+
# "environment/pull_bucket.py", "solution/patch.diff". Populated by
|
| 67 |
+
# load_task() walking the directory tree β the file viewer surfaces
|
| 68 |
+
# everything in here so the dataset is shown faithfully, not via
|
| 69 |
+
# a hardcoded allowlist.
|
| 70 |
+
files: dict[str, str] = field(default_factory=dict)
|
| 71 |
+
|
| 72 |
|
| 73 |
def _read_text(path: Path) -> str | None:
|
| 74 |
"""Read a text file, return None if it doesn't exist."""
|
|
|
|
| 81 |
return None
|
| 82 |
|
| 83 |
|
| 84 |
+
# Files we never surface in the file viewer (binaries, caches, secrets).
|
| 85 |
+
_SKIP_DIRS: set[str] = {".git", "__pycache__", ".cache", "node_modules", ".venv", ".pytest_cache", ".mypy_cache"}
|
| 86 |
+
_SKIP_NAME_PREFIXES: tuple[str, ...] = (".DS_Store",)
|
| 87 |
+
# Hard cap on file size β anything bigger we treat as non-displayable.
|
| 88 |
+
_MAX_FILE_BYTES = 512 * 1024 # 512 KiB; viewer's code panel chokes well below this
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _discover_task_files(task_dir: Path) -> dict[str, str]:
|
| 92 |
+
"""Walk `task_dir` recursively and return every readable text file as
|
| 93 |
+
{relative_path: content}. Skips binaries, hidden noise, and oversized files.
|
| 94 |
+
Paths use forward slashes (POSIX-style) for stable file_ids across OSes.
|
| 95 |
+
"""
|
| 96 |
+
out: dict[str, str] = {}
|
| 97 |
+
for path in sorted(task_dir.rglob("*")):
|
| 98 |
+
if not path.is_file():
|
| 99 |
+
continue
|
| 100 |
+
# Skip files inside excluded directories (any level)
|
| 101 |
+
if any(part in _SKIP_DIRS for part in path.relative_to(task_dir).parts):
|
| 102 |
+
continue
|
| 103 |
+
if path.name.startswith(_SKIP_NAME_PREFIXES):
|
| 104 |
+
continue
|
| 105 |
+
try:
|
| 106 |
+
size = path.stat().st_size
|
| 107 |
+
except OSError:
|
| 108 |
+
continue
|
| 109 |
+
if size > _MAX_FILE_BYTES:
|
| 110 |
+
continue
|
| 111 |
+
try:
|
| 112 |
+
content = path.read_text(encoding="utf-8")
|
| 113 |
+
except (UnicodeDecodeError, OSError):
|
| 114 |
+
continue
|
| 115 |
+
rel = path.relative_to(task_dir).as_posix()
|
| 116 |
+
out[rel] = content
|
| 117 |
+
return out
|
| 118 |
+
|
| 119 |
+
|
| 120 |
def _discover_task_roots(dataset_root: Path) -> list[Path]:
|
| 121 |
"""Find every directory under `dataset_root` that contains a `task.toml`.
|
| 122 |
|
|
|
|
| 180 |
if repo2env is not None and not isinstance(repo2env, dict):
|
| 181 |
repo2env = None
|
| 182 |
|
| 183 |
+
# Walk the whole task dir so any present file (grader.py, pull_bucket.py,
|
| 184 |
+
# helper modules, multiple test scripts, ...) ends up in the viewer.
|
| 185 |
+
files = _discover_task_files(task_dir)
|
| 186 |
+
|
| 187 |
return HarborTask(
|
| 188 |
id=task_id,
|
| 189 |
root=task_dir,
|
|
|
|
| 198 |
agent_timeout_sec=agent_block.get("timeout_sec"),
|
| 199 |
verifier_timeout_sec=verifier_block.get("timeout_sec"),
|
| 200 |
repo2env=repo2env,
|
| 201 |
+
# Convenience fields β kept populated for backwards compat with callers
|
| 202 |
+
# that reach in by name. Source of truth for the viewer is `files`.
|
| 203 |
+
instruction_md=files.get("instruction.md"),
|
| 204 |
+
oracle_patch=files.get("solution/patch.diff"),
|
| 205 |
+
solve_sh=files.get("solution/solve.sh"),
|
| 206 |
+
test_sh=files.get("tests/test.sh"),
|
| 207 |
+
dockerfile=files.get("environment/Dockerfile"),
|
| 208 |
task_toml_raw=raw,
|
| 209 |
+
files=files,
|
| 210 |
)
|