sciworld 0.1.0__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.
- sciworld/__init__.py +32 -0
- sciworld/__main__.py +5 -0
- sciworld/_api.py +242 -0
- sciworld/cache.py +42 -0
- sciworld/cli.py +1398 -0
- sciworld/config.py +97 -0
- sciworld/containers/agent.Dockerfile +59 -0
- sciworld/containers/agent.def +52 -0
- sciworld/evaluate_cmd.py +403 -0
- sciworld/format_stream.py +526 -0
- sciworld/github.py +315 -0
- sciworld/info_cmd.py +234 -0
- sciworld/launcher.py +1022 -0
- sciworld/milestones.py +34 -0
- sciworld/normscore.py +150 -0
- sciworld/package.py +198 -0
- sciworld/prompts/continue.md +3 -0
- sciworld/prompts/environment.md +7 -0
- sciworld/prompts/improve.md +60 -0
- sciworld/prompts/scratch.md +60 -0
- sciworld/prompts/system_prompt.md +1 -0
- sciworld/prompts/test.md +7 -0
- sciworld/research_cmd.py +842 -0
- sciworld/select_cmd.py +231 -0
- sciworld/session.py +47 -0
- sciworld/style.py +719 -0
- sciworld/widgets.py +720 -0
- sciworld-0.1.0.dist-info/METADATA +156 -0
- sciworld-0.1.0.dist-info/RECORD +33 -0
- sciworld-0.1.0.dist-info/WHEEL +5 -0
- sciworld-0.1.0.dist-info/entry_points.txt +3 -0
- sciworld-0.1.0.dist-info/licenses/LICENSE +21 -0
- sciworld-0.1.0.dist-info/top_level.txt +1 -0
sciworld/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""sciworld — research environments for paper reproduction.
|
|
2
|
+
|
|
3
|
+
This package exposes the same commands the ``sw`` CLI provides as Python
|
|
4
|
+
functions. Typical usage from a script or notebook:
|
|
5
|
+
|
|
6
|
+
import sciworld
|
|
7
|
+
|
|
8
|
+
sciworld.research(id="2505.19252", agent="claude-code", mode="scratch",
|
|
9
|
+
loop=True, max_loops=5)
|
|
10
|
+
sciworld.evaluate(id="2505.19252", run="claude-code-scratch")
|
|
11
|
+
|
|
12
|
+
Prompt kwargs accept either a literal string or a ``pathlib.Path`` — the
|
|
13
|
+
file is read at call time.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from sciworld._api import (
|
|
17
|
+
research,
|
|
18
|
+
evaluate,
|
|
19
|
+
select,
|
|
20
|
+
info,
|
|
21
|
+
init,
|
|
22
|
+
config,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"research",
|
|
27
|
+
"evaluate",
|
|
28
|
+
"select",
|
|
29
|
+
"info",
|
|
30
|
+
"init",
|
|
31
|
+
"config",
|
|
32
|
+
]
|
sciworld/__main__.py
ADDED
sciworld/_api.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Python API mirror of the ``sw`` CLI.
|
|
2
|
+
|
|
3
|
+
Every top-level CLI command has a sibling Python function here. The
|
|
4
|
+
function signature reuses the CLI's kwargs; interactive prompts are
|
|
5
|
+
replaced with ``TypeError`` on missing required args so calling the
|
|
6
|
+
API from a notebook or script never blocks on stdin.
|
|
7
|
+
|
|
8
|
+
Typical usage:
|
|
9
|
+
|
|
10
|
+
import sciworld
|
|
11
|
+
result = sciworld.research(
|
|
12
|
+
id="2505.19252", agent="claude-code", mode="scratch",
|
|
13
|
+
loop=True, max_loops=5,
|
|
14
|
+
system_prompt=Path("~/my_prompts/strict.md"),
|
|
15
|
+
)
|
|
16
|
+
# result = {"arxiv_id": ..., "run_name": ..., "env_dir": Path(...),
|
|
17
|
+
# "launch_script": Path(...), "source_dir": Path(...)}
|
|
18
|
+
|
|
19
|
+
sciworld.evaluate(id="2505.19252", run=result["run_name"])
|
|
20
|
+
|
|
21
|
+
Prompt kwargs (``system_prompt``, ``mode_prompt``, ``continue_prompt``,
|
|
22
|
+
``test_prompt``, ``compute``) accept either a literal string or a
|
|
23
|
+
``pathlib.Path``. Paths are read at call time — the resolved content
|
|
24
|
+
is used for that call; later file edits do not affect the running
|
|
25
|
+
session (for live per-iteration binding, see the `live_prompts`
|
|
26
|
+
roadmap item).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from types import SimpleNamespace
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from sciworld import config as _config
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── Helpers ────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_prompt(value: Any) -> str | None:
|
|
42
|
+
"""Normalize a prompt kwarg: Path → file content; str → as-is; None → None."""
|
|
43
|
+
if value is None:
|
|
44
|
+
return None
|
|
45
|
+
if isinstance(value, Path):
|
|
46
|
+
return value.expanduser().read_text()
|
|
47
|
+
return str(value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _require(**kwargs) -> None:
|
|
51
|
+
"""Raise TypeError for any kwarg that's None (required but missing)."""
|
|
52
|
+
missing = [k for k, v in kwargs.items() if v is None]
|
|
53
|
+
if missing:
|
|
54
|
+
raise TypeError(
|
|
55
|
+
f"Missing required argument(s): {', '.join(missing)}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _run_with_overrides(callable_, args_ns, overrides: dict) -> int:
|
|
60
|
+
"""Run a cmd_* function with prompt overrides active for this call."""
|
|
61
|
+
if not overrides:
|
|
62
|
+
return callable_(args_ns)
|
|
63
|
+
token = _config._instruction_overrides.set(overrides)
|
|
64
|
+
try:
|
|
65
|
+
return callable_(args_ns)
|
|
66
|
+
finally:
|
|
67
|
+
_config._instruction_overrides.reset(token)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── research ──────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def research(
|
|
74
|
+
*,
|
|
75
|
+
id: str,
|
|
76
|
+
agent: str,
|
|
77
|
+
mode: str,
|
|
78
|
+
tag: str | None = None,
|
|
79
|
+
path: str | Path | None = None,
|
|
80
|
+
loop: bool = False,
|
|
81
|
+
max_loops: int = 10,
|
|
82
|
+
setup_only: bool = False,
|
|
83
|
+
refresh: bool = False,
|
|
84
|
+
org: str | None = None,
|
|
85
|
+
system_prompt: str | Path | None = None,
|
|
86
|
+
mode_prompt: str | Path | None = None,
|
|
87
|
+
continue_prompt: str | Path | None = None,
|
|
88
|
+
test_prompt: str | Path | None = None,
|
|
89
|
+
compute: str | Path | None = None,
|
|
90
|
+
) -> dict:
|
|
91
|
+
"""Build (and optionally launch) a research environment.
|
|
92
|
+
|
|
93
|
+
Mirrors ``sw research``. Returns a dict with ``arxiv_id``, ``run_name``,
|
|
94
|
+
``env_dir``, ``source_dir``, ``launch_script`` — all the paths you need
|
|
95
|
+
to inspect the workspace or tail the agent log.
|
|
96
|
+
|
|
97
|
+
Raises ``TypeError`` if any of the three required kwargs (``id``,
|
|
98
|
+
``agent``, ``mode``) is missing, and ``RuntimeError`` if the command
|
|
99
|
+
itself exits with a non-zero return code.
|
|
100
|
+
"""
|
|
101
|
+
_require(id=id, agent=agent, mode=mode)
|
|
102
|
+
|
|
103
|
+
from sciworld.research_cmd import cmd_research # lazy import
|
|
104
|
+
|
|
105
|
+
args_ns = SimpleNamespace(
|
|
106
|
+
paper_id=id,
|
|
107
|
+
agent=agent,
|
|
108
|
+
mode=mode,
|
|
109
|
+
tag=tag,
|
|
110
|
+
dest_path=str(path) if path else None,
|
|
111
|
+
loop=loop,
|
|
112
|
+
max_loops=max_loops,
|
|
113
|
+
setup_only=setup_only,
|
|
114
|
+
refresh=refresh,
|
|
115
|
+
org=org,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
overrides: dict[str, str] = {}
|
|
119
|
+
if system_prompt is not None:
|
|
120
|
+
overrides["system_prompt"] = _resolve_prompt(system_prompt)
|
|
121
|
+
if mode_prompt is not None:
|
|
122
|
+
overrides[f"research_{mode}"] = _resolve_prompt(mode_prompt)
|
|
123
|
+
if continue_prompt is not None:
|
|
124
|
+
overrides["continue"] = _resolve_prompt(continue_prompt)
|
|
125
|
+
if test_prompt is not None:
|
|
126
|
+
overrides["test"] = _resolve_prompt(test_prompt)
|
|
127
|
+
if compute is not None:
|
|
128
|
+
overrides["compute"] = _resolve_prompt(compute)
|
|
129
|
+
|
|
130
|
+
rc = _run_with_overrides(cmd_research, args_ns, overrides)
|
|
131
|
+
if rc != 0:
|
|
132
|
+
raise RuntimeError(f"sciworld.research exited with code {rc}")
|
|
133
|
+
|
|
134
|
+
result = getattr(args_ns, "_sw_result", None)
|
|
135
|
+
if result is None:
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
"sciworld.research completed but produced no result metadata"
|
|
138
|
+
)
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── evaluate ──────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def evaluate(*, id: str, run: str | None = None) -> int:
|
|
146
|
+
"""Compare a run's scores to the paper's scores.
|
|
147
|
+
|
|
148
|
+
Mirrors ``sw evaluate``. Returns the command's exit code (0 = success).
|
|
149
|
+
Raises ``TypeError`` if ``id`` is missing.
|
|
150
|
+
"""
|
|
151
|
+
_require(id=id)
|
|
152
|
+
from sciworld.evaluate_cmd import cmd_evaluate # lazy import
|
|
153
|
+
|
|
154
|
+
args_ns = SimpleNamespace(arxiv_id=id, run=run)
|
|
155
|
+
return cmd_evaluate(args_ns)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ── select ────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def select(
|
|
162
|
+
*,
|
|
163
|
+
id: str | None = None,
|
|
164
|
+
path: str | Path | None = None,
|
|
165
|
+
refresh: bool = False,
|
|
166
|
+
) -> int:
|
|
167
|
+
"""Select a paper and clone its repo.
|
|
168
|
+
|
|
169
|
+
Mirrors ``sw select``. When ``id`` is given, fetches that paper
|
|
170
|
+
directly; when missing, raises ``TypeError`` (no interactive fallback
|
|
171
|
+
in the Python API).
|
|
172
|
+
"""
|
|
173
|
+
_require(id=id)
|
|
174
|
+
from sciworld.select_cmd import cmd_select # lazy import
|
|
175
|
+
|
|
176
|
+
args_ns = SimpleNamespace(
|
|
177
|
+
paper_id=id,
|
|
178
|
+
dest_path=str(path) if path else None,
|
|
179
|
+
refresh=refresh,
|
|
180
|
+
)
|
|
181
|
+
return cmd_select(args_ns)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── info ──────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def info(*, id: str, full: bool = False) -> int:
|
|
188
|
+
"""Show a paper's metadata (overview, problem, experiments, …).
|
|
189
|
+
|
|
190
|
+
Mirrors ``sw info``. Raises ``TypeError`` if ``id`` is missing.
|
|
191
|
+
"""
|
|
192
|
+
_require(id=id)
|
|
193
|
+
from sciworld.info_cmd import cmd_info # lazy import
|
|
194
|
+
|
|
195
|
+
args_ns = SimpleNamespace(arxiv_id=id, full=full)
|
|
196
|
+
return cmd_info(args_ns)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── config ────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def config(section: str | None = None, **overrides) -> dict:
|
|
203
|
+
"""View or update configuration.
|
|
204
|
+
|
|
205
|
+
``sciworld.config()`` → returns the current config as a dict.
|
|
206
|
+
``sciworld.config(section="...")`` → runs the interactive editor for
|
|
207
|
+
that section; raises ``ValueError`` if the section is unknown.
|
|
208
|
+
"""
|
|
209
|
+
if section is None and not overrides:
|
|
210
|
+
return dict(_config.load_config())
|
|
211
|
+
|
|
212
|
+
if section is not None:
|
|
213
|
+
from sciworld.cli import _CONFIG_SECTIONS, cmd_config
|
|
214
|
+
if section not in _CONFIG_SECTIONS:
|
|
215
|
+
valid = ", ".join(_CONFIG_SECTIONS)
|
|
216
|
+
raise ValueError(f"Unknown config section {section!r}. Valid: {valid}")
|
|
217
|
+
args_ns = SimpleNamespace(section=section)
|
|
218
|
+
cmd_config(args_ns)
|
|
219
|
+
return dict(_config.load_config())
|
|
220
|
+
|
|
221
|
+
# If we got here, someone passed raw kwargs with no section. Write them
|
|
222
|
+
# straight into config.json. (Low-level escape hatch.)
|
|
223
|
+
cfg = dict(_config.load_config())
|
|
224
|
+
cfg.update(overrides)
|
|
225
|
+
_config.save_config(cfg)
|
|
226
|
+
return cfg
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── init ──────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def init(*, force: bool = False) -> int:
|
|
233
|
+
"""Run the first-time setup wizard (builds the agent SIF, etc.).
|
|
234
|
+
|
|
235
|
+
Mirrors ``sw init``. The underlying command is interactive; calling
|
|
236
|
+
it from a script typically only makes sense with the setup already
|
|
237
|
+
partially complete. ``force=True`` rebuilds the agent container.
|
|
238
|
+
"""
|
|
239
|
+
from sciworld.cli import cmd_init # lazy import
|
|
240
|
+
|
|
241
|
+
args_ns = SimpleNamespace(force=force)
|
|
242
|
+
return cmd_init(args_ns)
|
sciworld/cache.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""TTL-based cache for GitHub repo data."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from sciworld.config import config_dir
|
|
9
|
+
|
|
10
|
+
CACHE_DIR = config_dir() / "cache"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cache_path(org: str) -> Path:
|
|
14
|
+
return CACHE_DIR / f"{org}_repos.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_cached(org: str, ttl: int = 600) -> list[dict] | None:
|
|
18
|
+
"""Load cached repo data. Returns None if stale or missing."""
|
|
19
|
+
path = cache_path(org)
|
|
20
|
+
if not path.exists():
|
|
21
|
+
return None
|
|
22
|
+
try:
|
|
23
|
+
data = json.loads(path.read_text())
|
|
24
|
+
if time.time() - data.get("timestamp", 0) > ttl:
|
|
25
|
+
return None
|
|
26
|
+
return data.get("repos", [])
|
|
27
|
+
except (json.JSONDecodeError, KeyError):
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def save_cached(org: str, repos: list[dict]) -> None:
|
|
32
|
+
"""Write repos to cache with current timestamp."""
|
|
33
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
data = {"timestamp": time.time(), "repos": repos}
|
|
35
|
+
cache_path(org).write_text(json.dumps(data))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def invalidate(org: str) -> None:
|
|
39
|
+
"""Delete cache for an org."""
|
|
40
|
+
path = cache_path(org)
|
|
41
|
+
if path.exists():
|
|
42
|
+
path.unlink()
|