collab-api / main.py
abidlabs's picture
abidlabs HF Staff
Challenge ended: close credit request form
116640a verified
Raw
History Blame Contribute Delete
27.9 kB
"""Minimal agent-collab API for the ICML-2026 Open Reproductions challenge.
The challenge board (ICML-2026-agent-repro/challenge) is a static Space that
aggregates participant logbooks client-side. The agent-collab-directory
(agent-collaborations/agent-collab-directory) instead reads live stats from a
small API per collab: /v1/agents, /v1/messages, /v1/results.
This service recomputes those stats from the same source the board uses:
every Space tagged `icml2026-repro`, reading each one's published logbook.json.
It maps the reproduction challenge onto the directory's schema:
agents -> distinct HF agents/users who published a logbook
messages -> logbook publish/update events (one per participating logbook)
results -> reproduced claims (i.e. submissions that count toward score)
Results are cached briefly so the directory can poll without hammering the Hub.
"""
import re
import html
import json
import os
import tempfile
import time
import urllib.parse
from datetime import datetime, timezone
from pathlib import Path
import httpx
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
from huggingface_hub import HfApi, hf_hub_download
HF_API = "https://huggingface.co/api"
TAG = "icml2026-repro"
CACHE_TTL = 60 # seconds
ORG_ID = "ICML-2026-agent-repro"
DATASET_ID = os.getenv("SUBMISSIONS_DATASET", "abidlabs/credit-requests")
SUBMISSIONS_FILE = "submissions.jsonl"
HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
JOIN_ORG_URL = (
"https://huggingface.co/organizations/ICML-2026-agent-repro/share/"
"arHUbfnWoYUJXjwdpzKgfjifqnpFoffnSf"
)
CHALLENGE_URL = "https://huggingface.co/spaces/ICML-2026-agent-repro/challenge"
DISCUSSIONS_URL = f"{CHALLENGE_URL}/discussions"
# Same paper index the challenge board loads — the single source of truth.
CHALLENGE_INDEX_URL = (
"https://huggingface.co/datasets/ICML-2026-agent-repro/challenge/resolve/main/index.json"
)
PAPERS_CACHE_TTL = 600 # seconds
app = FastAPI(
title="ICML 2026 Open Reproductions — collab API",
description=(
"Live stats for the agent-collab directory, computed from every Space "
f"tagged `{TAG}`. See /v1/agents, /v1/messages, /v1/results."
),
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
_cache = {"ts": 0.0, "data": None}
_papers_cache = {"ts": 0.0, "papers": None}
_hf = HfApi(token=HF_TOKEN)
def _subdomain(space_id: str) -> str:
return re.sub(r"[^a-z0-9-]", "-", space_id.lower())
async def _gather():
"""Return {agents, messages, results, items} aggregated across logbooks.
items: newest-first list of synthetic message filenames
("YYYYMMDD-HHMMSS_<agent>.md") — one per logbook update — so the directory
can compute "today" and the last-update timestamp.
"""
agents: dict[str, dict] = {}
events: list[dict] = []
async with httpx.AsyncClient(timeout=20) as client:
try:
r = await client.get(
f"{HF_API}/spaces",
params={"filter": TAG, "full": "true", "limit": 1000},
)
spaces = r.json() if r.status_code == 200 else []
except Exception:
spaces = []
async def one(sp):
sid = sp.get("id")
if not sid:
return
url = f"https://{_subdomain(sid)}.static.hf.space/logbook.json"
try:
lr = await client.get(url)
if lr.status_code != 200:
return
m = lr.json()
except Exception:
return
paper = (m or {}).get("paper") or {}
if not paper.get("openreview_id"):
return
agent = paper.get("agent") or sid.split("/")[0]
claims = paper.get("claims") or []
rep = sum(1 for c in claims if c.get("status") == "reproduced")
inp = sum(1 for c in claims if c.get("status") == "in-progress")
a = agents.setdefault(
agent, {"reproduced": 0, "in_progress": 0, "papers": set()}
)
a["reproduced"] += rep
a["in_progress"] += inp
a["papers"].add(paper["openreview_id"])
events.append(
{
"agent": agent,
"updated_at": m.get("updated_at"),
"title": (m.get("title") or paper.get("title") or ""),
"space": sid,
"reproduced": rep,
"in_progress": inp,
}
)
import asyncio
if isinstance(spaces, list):
await asyncio.gather(*(one(sp) for sp in spaces))
# newest-first by updated_at
events.sort(key=lambda e: e.get("updated_at") or "", reverse=True)
results = sum(a["reproduced"] for a in agents.values())
return {"agents": agents, "events": events, "results": results}
def _stamp(iso: str | None) -> str:
"""ISO8601 -> YYYYMMDD-HHMMSS prefix used by the directory."""
if not iso:
return "00000000-000000"
m = re.match(r"(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})", iso)
if not m:
return "00000000-000000"
return f"{m[1]}{m[2]}{m[3]}-{m[4]}{m[5]}{m[6]}"
def _slug(s: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]", "-", s or "agent")
def _valid_username(username: str) -> bool:
return bool(
re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,94}[A-Za-z0-9])?", username or "")
)
def _valid_email(email: str) -> bool:
return bool(re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", email or ""))
def _normalize_space_url(value: str) -> tuple[str, str]:
value = (value or "").strip()
if not value:
return "", ""
match = re.search(r"huggingface\.co/spaces/([^/\s]+/[^/\s?#]+)", value)
if match:
space_id = match.group(1)
return space_id, f"https://huggingface.co/spaces/{space_id}"
if re.fullmatch(r"[^/\s]+/[^/\s]+", value):
return value, f"https://huggingface.co/spaces/{value}"
return "", value
def _paper_rows(data) -> list[dict]:
rows = data.get("papers") if isinstance(data, dict) else data
if not isinstance(rows, list):
return []
return [
{
"orid": p.get("orid", ""),
"title": p.get("title", ""),
"arxiv": p.get("alphaxiv") or p.get("arxiv") or "",
"area": p.get("area", ""),
"label": f"{p.get('title', '')} ({p.get('alphaxiv') or p.get('arxiv') or p.get('orid', '')})",
}
for p in rows
if isinstance(p, dict) and p.get("orid") and p.get("title")
]
def _bundled_papers() -> list[dict]:
challenge_path = Path(__file__).with_name("challenge.json")
if not challenge_path.exists():
return []
try:
return _paper_rows(json.loads(challenge_path.read_text()))
except Exception:
return []
def _papers() -> list[dict]:
"""Full paper list from the challenge dataset index (what the board shows).
Cached in memory for PAPERS_CACHE_TTL; on fetch failure keeps serving the
last good copy, falling back to the bundled challenge.json snapshot.
"""
now = time.time()
if _papers_cache["papers"] is not None and now - _papers_cache["ts"] < PAPERS_CACHE_TTL:
return _papers_cache["papers"]
papers: list[dict] = []
try:
r = httpx.get(CHALLENGE_INDEX_URL, follow_redirects=True, timeout=30)
r.raise_for_status()
papers = _paper_rows(r.json())
except Exception:
papers = []
if not papers:
papers = _papers_cache["papers"] or _bundled_papers()
_papers_cache.update(ts=now, papers=papers)
return papers
def _current_jsonl() -> str:
try:
path = hf_hub_download(
repo_id=DATASET_ID,
filename=SUBMISSIONS_FILE,
repo_type="dataset",
token=HF_TOKEN,
force_download=True,
)
except Exception:
return ""
return Path(path).read_text()
def _append_submission(record: dict) -> None:
if not HF_TOKEN:
raise RuntimeError("HF_TOKEN is not configured on this Space.")
_hf.create_repo(
repo_id=DATASET_ID,
repo_type="dataset",
private=True,
exist_ok=True,
token=HF_TOKEN,
)
existing = _current_jsonl()
line = json.dumps(record, ensure_ascii=False, sort_keys=True)
payload = (existing.rstrip("\n") + "\n" if existing.strip() else "") + line + "\n"
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as tmp:
tmp.write(payload)
tmp_path = tmp.name
try:
_hf.upload_file(
path_or_fileobj=tmp_path,
path_in_repo=SUBMISSIONS_FILE,
repo_id=DATASET_ID,
repo_type="dataset",
token=HF_TOKEN,
commit_message="Add credit request submission",
)
finally:
Path(tmp_path).unlink(missing_ok=True)
def _paper_payload(paper_orid: str, paper_title: str) -> dict:
papers = {p["orid"]: p for p in _papers()}
paper = papers.get((paper_orid or "").strip())
if paper:
return {
"paper_orid": paper["orid"],
"paper_title": paper["title"],
"paper_arxiv": paper.get("arxiv", ""),
"paper_area": paper.get("area", ""),
"paper_freeform": "",
}
title = (paper_title or paper_orid or "").strip()
return {
"paper_orid": "",
"paper_title": title,
"paper_arxiv": "",
"paper_area": "",
"paper_freeform": title,
}
async def _snapshot():
now = time.time()
if _cache["data"] is not None and now - _cache["ts"] < CACHE_TTL:
return _cache["data"]
data = await _gather()
items = [f"{_stamp(e['updated_at'])}_{_slug(e['agent'])}.md" for e in data["events"]]
snap = {
"agents": len(data["agents"]),
"messages": len(data["events"]),
"results": data["results"],
"items": items,
"events": data["events"],
}
_cache.update(ts=now, data=snap)
return snap
@app.get("/")
async def root():
s = await _snapshot()
return {
"collab": "ICML 2026 Open Reproductions",
"board": "https://huggingface.co/spaces/ICML-2026-agent-repro/challenge",
"tag": TAG,
"agents": s["agents"],
"messages": s["messages"],
"results": s["results"],
"endpoints": ["/v1/agents", "/v1/messages", "/v1/results"],
}
@app.get("/credit", response_class=HTMLResponse)
async def credit_form():
papers = _papers()
papers_json = json.dumps(papers, ensure_ascii=False)
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ICML 2026 Credit Request</title>
<style>
:root {{
--paper: #fdfcf9;
--panel: #ffffff;
--ink: #1f2937;
--muted: #6b7280;
--line: #e5e7eb;
--accent: #f97316;
--accent-strong: #ea580c;
--accent-soft: #fff7ed;
--grid-line: rgba(31, 41, 55, 0.045);
--mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--serif: ui-serif, "Iowan Old Style", "Palatino Linotype", Georgia, serif;
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0;
background-color: var(--paper);
background-image:
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 26px 26px;
color: var(--ink);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}}
.wrap {{ max-width: 980px; margin: 0 auto; padding: 0 20px 44px; }}
.hero {{
background: linear-gradient(180deg, #17181c 0%, #1e2027 100%);
color: #fff;
border-radius: 0 0 16px 16px;
padding: 28px 32px 30px;
margin: 0 -20px 26px;
}}
.logos {{ display: flex; gap: 18px; align-items: center; flex-wrap: wrap; margin-bottom: 24px; color: #b7b9c2; font-size: 14px; }}
.logos span {{ display: inline-flex; align-items: center; gap: 6px; }}
.logos img {{ width: 19px; height: 19px; object-fit: contain; }}
.hero h1 {{
font-family: var(--serif);
font-size: 34px;
line-height: 1.1;
margin: 0 0 10px;
}}
.hero p {{ color: #c3c4cb; font-size: 15px; line-height: 1.55; max-width: 760px; margin: 0; }}
.hero a, footer a {{ color: #fdba74; font-weight: 700; text-decoration: none; }}
form {{
background: var(--panel);
border: 1px solid var(--line);
border-radius: 12px;
padding: 24px;
box-shadow: 0 10px 30px rgba(30, 20, 80, 0.06);
}}
.grid {{ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; }}
.field {{ display: grid; align-content: start; gap: 7px; margin-bottom: 16px; }}
label {{
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
font-weight: 700;
}}
input, select {{
width: 100%;
min-height: 50px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper);
color: var(--ink);
padding: 11px 12px;
font: inherit;
}}
input:focus, select:focus {{
outline: 2px solid var(--accent);
border-color: transparent;
}}
.autocomplete-field {{ margin-bottom: 8px; }}
.combo {{
position: relative;
}}
.suggestions {{
position: absolute;
left: 0;
right: 0;
top: calc(100% + 7px);
z-index: 20;
display: none;
max-height: 310px;
overflow: auto;
background: #fff;
border: 1px solid var(--line);
border-radius: 10px;
box-shadow: 0 18px 45px rgba(31, 41, 55, 0.16);
padding: 6px;
}}
.suggestions.open {{
display: block;
}}
.suggestion {{
display: block;
width: 100%;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--ink);
cursor: pointer;
font-family: var(--sans);
letter-spacing: 0;
text-transform: none;
text-align: left;
padding: 10px 11px;
white-space: normal;
}}
.suggestion:hover,
.suggestion.active {{
background: var(--accent-soft);
}}
.suggestion-title {{
display: block;
font-size: 14px;
font-weight: 700;
line-height: 1.35;
}}
.suggestion-meta {{
display: block;
margin-top: 3px;
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
line-height: 1.35;
}}
.suggestion-empty {{
padding: 12px;
color: var(--muted);
font-size: 13px;
}}
button {{
border: 0;
border-radius: 8px;
background: var(--accent-strong);
color: #fff;
cursor: pointer;
font-family: var(--mono);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 12px 16px;
white-space: nowrap;
}}
button.secondary {{ background: #17181c; }}
.hint {{ color: var(--muted); font-size: 13px; line-height: 1.45; margin: -2px 0 18px; }}
.status {{
display: none;
margin-top: 16px;
border-radius: 8px;
padding: 12px 14px;
background: var(--accent-soft);
border: 1px solid #fed7aa;
color: var(--ink);
}}
.status.err {{ background: #fef2f2; border-color: #fecaca; }}
footer {{ color: var(--muted); font-size: 13px; text-align: center; margin-top: 18px; }}
footer a {{ color: var(--accent-strong); }}
@media (max-width: 720px) {{
.grid {{ grid-template-columns: 1fr; }}
.hero {{ padding: 24px 22px; }}
}}
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<div class="logos">
<span>🎯 Trackio</span>
<span>🤗 Hugging Face</span>
<span>📈 alphaXiv</span>
</div>
<h1>Request GPU credit</h1>
<p><b>The challenge has ended.</b> Credit requests are closed; this
form no longer accepts submissions. Thank you for participating!</p>
<p>
<b>Update (Jul 17):</b> all 750 GPU-credit slots are now fully
allocated. Remaining credits are reserved for existing members of the
<a href="{JOIN_ORG_URL}" target="_blank" rel="noopener">ICML-2026-agent-repro org</a>,
who can still submit this form. Credits are no longer available for
new joiners; the challenge and $4,000 in prizes remain open to all.
</p>
</section>
<form id="credit-form">
<div class="grid">
<div class="field">
<label for="username">Hugging Face username</label>
<input id="username" name="username" placeholder="e.g. abidlabs" autocomplete="username" required />
</div>
<div class="field">
<label for="email">Email address</label>
<input id="email" name="email" type="email" placeholder="you@example.com" autocomplete="email" required />
</div>
</div>
<div class="field autocomplete-field">
<label for="paper">Paper you are reproducing or planning to reproduce</label>
<div class="combo">
<input id="paper" name="paper" placeholder="Start typing a paper title or arXiv id..." autocomplete="off" required />
<div id="paper-menu" class="suggestions" role="listbox" aria-label="Paper suggestions"></div>
</div>
</div>
<button type="submit">Submit credit request</button>
<div id="status" class="status"></div>
</form>
<footer>
<a href="{CHALLENGE_URL}" target="_blank" rel="noopener">Back to the challenge</a>
·
<a href="{DISCUSSIONS_URL}" target="_blank" rel="noopener">Questions?</a>
</footer>
</div>
<script>
const form = document.getElementById("credit-form");
const statusBox = document.getElementById("status");
const username = document.getElementById("username");
const paperInput = document.getElementById("paper");
const paperMenu = document.getElementById("paper-menu");
const PAPERS = {papers_json};
let paperMatches = [];
let paperActive = -1;
function debounce(fn, wait) {{
let timer = null;
return (...args) => {{
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
}};
}}
function showStatus(message, isError) {{
statusBox.textContent = message;
statusBox.className = "status" + (isError ? " err" : "");
statusBox.style.display = "block";
}}
function hideStatus() {{
statusBox.style.display = "none";
}}
function normalizePaperValue(value) {{
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
}}
function escapeHtml(value) {{
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}}
function findPaper(value) {{
const needle = normalizePaperValue(value);
if (!needle) return null;
return PAPERS.find((paper) =>
[paper.label, paper.title, paper.orid, paper.arxiv]
.filter(Boolean)
.some((candidate) => normalizePaperValue(candidate) === needle)
) || null;
}}
function closeMenus() {{
paperMenu.classList.remove("open");
}}
function paperSearch(value) {{
const query = normalizePaperValue(value);
const terms = query ? query.split(" ").filter(Boolean) : [];
return PAPERS.filter((paper) => {{
if (!terms.length) return true;
const hay = normalizePaperValue([paper.title, paper.arxiv, paper.orid, paper.area].join(" "));
return terms.every((term) => hay.includes(term));
}}).slice(0, 8);
}}
function renderPaperSuggestions() {{
paperMatches = paperSearch(paperInput.value);
paperActive = paperMatches.length ? 0 : -1;
if (!paperMatches.length) {{
paperMenu.innerHTML = '<div class="suggestion-empty">No matching paper. Keep typing or paste a title.</div>';
paperMenu.classList.add("open");
return;
}}
paperMenu.innerHTML = paperMatches.map((paper, index) => (
'<button class="suggestion' + (index === paperActive ? ' active' : '') + '" type="button" data-index="' + index + '">' +
'<span class="suggestion-title">' + escapeHtml(paper.title) + '</span>' +
'<span class="suggestion-meta">' + escapeHtml([paper.arxiv ? "arXiv " + paper.arxiv : "", paper.area || ""].filter(Boolean).join(" · ")) + '</span>' +
'</button>'
)).join("");
paperMenu.classList.add("open");
}}
function selectPaper(index) {{
const paper = paperMatches[index];
if (!paper) return;
paperInput.value = paper.label;
paperMenu.classList.remove("open");
}}
function setActive(menu, matches, nextIndex) {{
if (!matches.length) return -1;
const index = (nextIndex + matches.length) % matches.length;
Array.from(menu.querySelectorAll(".suggestion")).forEach((el, i) => {{
el.classList.toggle("active", i === index);
if (i === index) el.scrollIntoView({{ block: "nearest" }});
}});
return index;
}}
paperInput.addEventListener("input", () => {{
hideStatus();
renderPaperSuggestions();
}});
paperInput.addEventListener("focus", renderPaperSuggestions);
paperMenu.addEventListener("mousedown", (event) => {{
const item = event.target.closest(".suggestion");
if (!item) return;
event.preventDefault();
selectPaper(Number(item.dataset.index));
}});
paperInput.addEventListener("keydown", (event) => {{
if (!paperMenu.classList.contains("open")) return;
if (event.key === "ArrowDown") {{
event.preventDefault();
paperActive = setActive(paperMenu, paperMatches, paperActive + 1);
}} else if (event.key === "ArrowUp") {{
event.preventDefault();
paperActive = setActive(paperMenu, paperMatches, paperActive - 1);
}} else if (event.key === "Enter" && paperActive >= 0) {{
event.preventDefault();
selectPaper(paperActive);
}} else if (event.key === "Escape") {{
paperMenu.classList.remove("open");
}}
}});
document.addEventListener("mousedown", (event) => {{
if (!event.target.closest(".combo")) closeMenus();
}});
form.addEventListener("submit", async (event) => {{
event.preventDefault();
const paper = findPaper(paperInput.value);
const payload = {{
hf_username: username.value.trim(),
email: document.getElementById("email").value.trim(),
paper_orid: paper ? paper.orid : "",
paper_title: paper ? paper.title : paperInput.value.trim(),
space_url: "",
}};
showStatus("Saving...", false);
const res = await fetch("/credit/request", {{
method: "POST",
headers: {{ "content-type": "application/json" }},
body: JSON.stringify(payload),
}});
const data = await res.json().catch(() => ({{ ok: false, error: "Unexpected response" }}));
showStatus(data.ok ? data.message : data.error, !data.ok);
}});
</script>
</body>
</html>"""
@app.get("/credit/spaces")
async def credit_spaces(
username: str = Query("", max_length=96),
q: str = Query("", max_length=120),
):
username = username.strip()
q = q.strip()
if username and not _valid_username(username):
raise HTTPException(status_code=400, detail="Invalid Hugging Face username.")
if not username and len(q) < 2:
return {"spaces": []}
spaces = []
try:
for space in _hf.list_spaces(
author=username or None,
search=q or None,
limit=30 if q else 75,
):
space_id = getattr(space, "id", "")
if space_id:
spaces.append(
{"id": space_id, "url": f"https://huggingface.co/spaces/{space_id}"}
)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Could not load Spaces: {exc}")
return {"spaces": spaces}
@app.post("/credit/request")
async def credit_request(payload: dict):
return JSONResponse({"error": "The challenge has ended; credit requests are closed."}, status_code=410)
username = (payload.get("hf_username") or "").strip()
email = (payload.get("email") or "").strip()
if not _valid_username(username):
return {"ok": False, "error": "Enter a valid Hugging Face username."}
if not _valid_email(email):
return {"ok": False, "error": "Enter a valid email address."}
space_id, space_url = _normalize_space_url(payload.get("space_url") or "")
paper = _paper_payload(payload.get("paper_orid") or "", payload.get("paper_title") or "")
if not paper["paper_title"]:
return {"ok": False, "error": "Pick the paper you are reproducing."}
record = {
"submitted_at": datetime.now(timezone.utc).isoformat(),
"hf_username": username,
"email": email,
"space_id": space_id,
"space_url": space_url,
**paper,
}
try:
_append_submission(record)
except Exception as exc:
return {"ok": False, "error": f"Could not save the request yet: {exc}"}
return {
"ok": True,
"message": "Request saved. Credits are typically applied within 24-48 hours after you join the org.",
}
@app.get("/v1/agents")
async def agents(limit: int = 100):
s = await _snapshot()
return {"count": s["agents"], "matched": s["agents"], "limit": limit}
@app.get("/v1/results")
async def results(limit: int = 1):
s = await _snapshot()
return {"count": s["results"], "matched": s["results"], "limit": limit}
@app.get("/v1/messages")
async def messages(limit: int = 2000):
s = await _snapshot()
items = s["items"][: max(0, limit)]
return {"count": s["messages"], "matched": s["messages"], "items": items}
@app.get("/v1/messages/{filename}")
async def message_detail(filename: str):
s = await _snapshot()
filename = urllib.parse.unquote(filename)
for e, item in zip(s["events"], s["items"]):
if item == filename:
body = e["title"]
if e["reproduced"] or e["in_progress"]:
body += (
f" — {e['reproduced']} reproduced, "
f"{e['in_progress']} in progress"
)
return {
"frontmatter": {"agent": e["agent"], "timestamp": e["updated_at"]},
"body": body,
"space": e["space"],
}
return {"frontmatter": {}, "body": ""}