File size: 8,541 Bytes
824b65d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43e3e30
 
 
 
 
 
 
824b65d
 
 
43e3e30
 
 
 
 
824b65d
43e3e30
824b65d
 
 
 
 
 
 
 
43e3e30
824b65d
 
 
 
 
 
 
 
43e3e30
 
 
 
 
 
824b65d
43e3e30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824b65d
 
 
 
 
 
43e3e30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824b65d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
"""Shared job store for the papers-reproducibility demo.

Both the Gradio Space (app.py) and the local agent runner (local_runner.py)
read and write the same Hugging Face Dataset repo, since they run on
different machines. The repo holds:

    state.json              a JSON list of job records (the "database")
    reports/<job_id>.html   self-contained HTML report for a finished job
    traces/<job_id>.jsonl   append-only trace events for a job

This is a demo-grade store: every write downloads state.json, mutates it in
Python, and re-uploads the whole file. There is no locking, so concurrent
writers can race — acceptable for one admin and light request traffic, but
worth knowing if this grows into something bigger.
"""

from __future__ import annotations

import json
import os
import uuid
from datetime import datetime, timezone

from huggingface_hub import HfApi
from huggingface_hub.utils import EntryNotFoundError, HfHubHTTPError

try:
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    pass

STATE_FILENAME = "state.json"
STATUSES = ("pending", "approved", "rejected", "running", "completed", "failed")


def _repo_id() -> str:
    repo_id = os.environ.get("HF_STORE_REPO")
    if not repo_id:
        raise RuntimeError(
            "HF_STORE_REPO is not set. Point it at the shared dataset repo, "
            "e.g. 'your-username/papers-repro-store'."
        )
    return repo_id


def _token() -> str | None:
    return os.environ.get("HF_TOKEN")


def _api() -> HfApi:
    return HfApi(token=_token())


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _download_state() -> list[dict]:
    from huggingface_hub import hf_hub_download

    try:
        path = hf_hub_download(
            repo_id=_repo_id(),
            repo_type="dataset",
            filename=STATE_FILENAME,
            token=_token(),
            force_download=True,
        )
    except (EntryNotFoundError, HfHubHTTPError):
        return []
    with open(path, "r") as f:
        return json.load(f)


def _upload_state(jobs: list[dict]) -> None:
    _api().upload_file(
        path_or_fileobj=json.dumps(jobs, indent=2).encode("utf-8"),
        path_in_repo=STATE_FILENAME,
        repo_id=_repo_id(),
        repo_type="dataset",
        commit_message="Update job state",
    )


def list_jobs() -> list[dict]:
    """All jobs, newest request first."""
    jobs = _download_state()
    return sorted(jobs, key=lambda j: j.get("requested_at", ""), reverse=True)


def get_job(job_id: str) -> dict | None:
    for job in _download_state():
        if job["id"] == job_id:
            return job
    return None


def existing_arxiv_ids() -> set[str]:
    """arxiv ids already tracked (any status) — lets the daily scan skip papers it has seen before."""
    return {job["arxiv_id"] for job in _download_state() if job.get("arxiv_id")}


def _base_job(
    *,
    title: str,
    paper_url: str,
    code_url: str,
    data_url: str,
    mode: str,
    notes: str,
    requested_by: str,
    source: str,
) -> dict:
    return {
        "id": str(uuid.uuid4()),
        "title": title.strip(),
        "paper_url": paper_url.strip(),
        "code_url": code_url.strip(),
        "data_url": data_url.strip(),
        "mode": mode if mode in ("author", "replicator") else "replicator",
        "notes": notes.strip(),
        "requested_by": requested_by.strip(),
        "source": source,
        "status": "pending",
        "requested_at": _now(),
        "approved_at": None,
        "started_at": None,
        "finished_at": None,
        "error": None,
        "report_path": None,
        "trace_path": None,
        # daily-scan-only fields (None for manual requests)
        "arxiv_id": None,
        "upvotes": None,
        "repro_score": None,
        "repro_summary": None,
        "decided_by": None,
    }


def create_request(
    title: str,
    paper_url: str,
    code_url: str,
    data_url: str = "",
    mode: str = "replicator",
    notes: str = "",
    requested_by: str = "",
) -> dict:
    job = _base_job(
        title=title,
        paper_url=paper_url,
        code_url=code_url,
        data_url=data_url,
        mode=mode,
        notes=notes,
        requested_by=requested_by,
        source="manual",
    )
    jobs = _download_state()
    jobs.append(job)
    _upload_state(jobs)
    return job


def create_candidate(
    *,
    title: str,
    paper_url: str,
    code_url: str,
    data_url: str = "",
    arxiv_id: str = "",
    upvotes: int = 0,
    repro_score: float = 0.0,
    repro_summary: str = "",
    mode: str = "replicator",
    notes: str = "",
) -> dict:
    """Create a job sourced from the daily-papers scan, awaiting Slack accept/reject.

    Lands as an ordinary "pending" job so the existing Admin tab can approve
    or reject it too, in case Slack is unreachable or the message is missed.
    """
    job = _base_job(
        title=title,
        paper_url=paper_url,
        code_url=code_url,
        data_url=data_url,
        mode=mode,
        notes=notes,
        requested_by="daily-scan",
        source="daily_scan",
    )
    job["arxiv_id"] = arxiv_id
    job["upvotes"] = upvotes
    job["repro_score"] = repro_score
    job["repro_summary"] = repro_summary.strip()
    jobs = _download_state()
    jobs.append(job)
    _upload_state(jobs)
    return job


def set_status(job_id: str, status: str, **fields) -> dict:
    if status not in STATUSES:
        raise ValueError(f"Unknown status {status!r}")
    jobs = _download_state()
    updated = None
    for job in jobs:
        if job["id"] == job_id:
            job["status"] = status
            job.update(fields)
            updated = job
            break
    if updated is None:
        raise KeyError(f"No job with id {job_id!r}")
    _upload_state(jobs)
    return updated


def append_trace_events(job_id: str, events: list[dict]) -> str:
    """Append events to traces/<job_id>.jsonl and return the repo path."""
    if not events:
        return f"traces/{job_id}.jsonl"

    from huggingface_hub import hf_hub_download

    path_in_repo = f"traces/{job_id}.jsonl"
    try:
        local_path = hf_hub_download(
            repo_id=_repo_id(),
            repo_type="dataset",
            filename=path_in_repo,
            token=_token(),
            force_download=True,
        )
        with open(local_path, "r") as f:
            existing = f.read()
    except (EntryNotFoundError, HfHubHTTPError):
        existing = ""

    new_lines = "\n".join(json.dumps(e) for e in events)
    content = existing + (new_lines + "\n" if not existing or existing.endswith("\n") else "\n" + new_lines + "\n")

    _api().upload_file(
        path_or_fileobj=content.encode("utf-8"),
        path_in_repo=path_in_repo,
        repo_id=_repo_id(),
        repo_type="dataset",
        commit_message=f"Append {len(events)} trace event(s) for {job_id}",
    )
    return path_in_repo


def read_trace(job_id: str) -> list[dict]:
    from huggingface_hub import hf_hub_download

    try:
        local_path = hf_hub_download(
            repo_id=_repo_id(),
            repo_type="dataset",
            filename=f"traces/{job_id}.jsonl",
            token=_token(),
            force_download=True,
        )
    except (EntryNotFoundError, HfHubHTTPError):
        return []
    events = []
    with open(local_path, "r") as f:
        for line in f:
            line = line.strip()
            if line:
                events.append(json.loads(line))
    return events


def upload_report(job_id: str, html_path: str) -> str:
    path_in_repo = f"reports/{job_id}.html"
    _api().upload_file(
        path_or_fileobj=html_path,
        path_in_repo=path_in_repo,
        repo_id=_repo_id(),
        repo_type="dataset",
        commit_message=f"Upload report for {job_id}",
    )
    return path_in_repo


def read_report(job_id: str) -> str | None:
    from huggingface_hub import hf_hub_download

    try:
        local_path = hf_hub_download(
            repo_id=_repo_id(),
            repo_type="dataset",
            filename=f"reports/{job_id}.html",
            token=_token(),
            force_download=True,
        )
    except (EntryNotFoundError, HfHubHTTPError):
        return None
    with open(local_path, "r") as f:
        return f.read()


def ensure_repo_exists() -> None:
    """Create the dataset repo (private) if it doesn't exist yet."""
    _api().create_repo(repo_id=_repo_id(), repo_type="dataset", private=True, exist_ok=True)