CoolFace
Apppublic

lspcloud/prolific-preferences-personalized

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py276 linesDownload Raw Back to src
1"""2Entry point for the user study Streamlit app.3 4Run from repo root:5    streamlit run src/app.py6    streamlit run src/app.py -- --debug   (sets DEBUG_MODE via env)7 8HuggingFace Space secrets required:9    HF_TOKEN        — read/write HuggingFace token10    GH_TOKEN        — GitHub PAT (ehejin account) for the private lsp submodule11    TINKER_API_KEY  — Tinker inference API key12    DEBUG_MODE      — "true" to skip all validation (optional)13"""14import os15import sys16import json17import subprocess18from pathlib import Path19 20# ---------------------------------------------------------------------------21# 1. Initialise lsp git submodule before any lsp imports22#    On a cold HF Space start the submodule directory exists but is empty;23#    GH_TOKEN lets us authenticate against the private GitHub repo.24# ---------------------------------------------------------------------------25_BASE     = Path(__file__).resolve().parent.parent26_LSP_PATH = _BASE / "lsp"27 28 29def _init_submodule() -> None:30    prompts_exist = (_LSP_PATH / "src" / "prompts").exists()31    if not prompts_exist:32        token = os.getenv("GH_TOKEN", "")33        if not token:34            raise RuntimeError("GH_TOKEN secret is not set.")35 36        import shutil37        import tarfile38        import urllib.request39        import time as _time40 41        # Clean any stale state42        if _LSP_PATH.exists():43            shutil.rmtree(str(_LSP_PATH), ignore_errors=True)44        git_modules = _BASE / ".git" / "modules" / "lsp"45        if git_modules.exists():46            shutil.rmtree(str(git_modules), ignore_errors=True)47 48        # GitHub serves a tarball of any branch/tag/SHA at this URL.49        # Pinned to a specific commit SHA so future lsp changes don't break us.50        branch      = "74582acd911f81309ba8b22cef9286c2887dda18"51        tarball_url = f"https://api.github.com/repos/batu-el/lsp/tarball/{branch}"52        tmp_tar     = Path("/tmp/lsp.tar.gz")53        tmp_extract = Path("/tmp/lsp_extract")54 55        for attempt in range(1, 4):56            print(f"[SUBMODULE] tarball download attempt {attempt}/3 ...")57            try:58                req = urllib.request.Request(59                    tarball_url,60                    headers={61                        "Authorization": f"Bearer {token}",62                        "Accept":        "application/vnd.github+json",63                        "User-Agent":    "prolific-preferences",64                    },65                )66                with urllib.request.urlopen(req, timeout=60) as resp:67                    tmp_tar.write_bytes(resp.read())68                print(f"[SUBMODULE] downloaded {tmp_tar.stat().st_size} bytes")69 70                # Extract71                if tmp_extract.exists():72                    shutil.rmtree(str(tmp_extract), ignore_errors=True)73                tmp_extract.mkdir(parents=True)74                with tarfile.open(str(tmp_tar)) as tar:75                    tar.extractall(str(tmp_extract))76 77                # GitHub tarballs have a top-level dir like batu-el-lsp-abc123/78                subdirs = [d for d in tmp_extract.iterdir() if d.is_dir()]79                if not subdirs:80                    raise RuntimeError("tarball had no top-level directory")81                top = subdirs[0]82 83                # Verify the prompts dir is present84                if not (top / "src" / "prompts").exists():85                    raise RuntimeError(f"src/prompts not found in extracted tarball at {top}")86 87                # Move extracted contents to /app/lsp88                shutil.copytree(str(top), str(_LSP_PATH))89                tmp_tar.unlink(missing_ok=True)90                shutil.rmtree(str(tmp_extract), ignore_errors=True)91 92                print("[SUBMODULE] ready.")93                break94            except Exception as e:95                msg = str(e).replace(token, "***") if token else str(e)96                print(f"[SUBMODULE] attempt {attempt} failed: {msg}")97                _time.sleep(3)98        else:99            raise RuntimeError(f"Failed to download lsp tarball after 3 attempts.")100 101    lsp_src = str(_LSP_PATH / "src")102    if lsp_src not in sys.path:103        sys.path.insert(0, lsp_src)104    if str(_BASE) not in sys.path:105        sys.path.insert(0, str(_BASE))106 107 108_init_submodule()109 110# Wipe stale local state ONLY on the first container load (not on every Streamlit rerun).111# We use a marker file — once created, subsequent imports skip the wipe.112# Completions stay durable in HF; we re-scan HF fresh after wipe.113_data_root = _BASE / "data"114_data_root.mkdir(parents=True, exist_ok=True)115_wipe_marker = _data_root / ".startup_wiped"116if not _wipe_marker.exists():117    for pattern in ("reservations.json", "local_completions_*.json", "completion_cache_*.json"):118        for f in _data_root.glob(pattern):119            try:120                f.unlink()121                print(f"[STARTUP] Wiped stale file: {f.name}")122            except Exception as e:123                print(f"[STARTUP] Could not wipe {f.name}: {e}")124    _wipe_marker.touch()125    print("[STARTUP] Marked container as wiped")126 127# ---------------------------------------------------------------------------128# 2. App imports (only after submodule is initialised)129# ---------------------------------------------------------------------------130import streamlit as st131 132from src.config import load_config133from src.data import ensure_datasets, init_state134from src.ui.components import inject_css135from src.ui.screens_shared import (136    screen_background,137    screen_chat,138    screen_demographics,139    screen_done,140    screen_post_rating,141    screen_reflection,142    screen_welcome,143)144from src.ui.screens_likelihood import screen_item_intro145from src.ui.screens_preference import screen_pair_intro146 147 148# ---------------------------------------------------------------------------149# 3. Admin dashboard — visit ?admin=1150# ---------------------------------------------------------------------------151def _screen_admin(cfg: dict) -> None:152    """Coverage dashboard — visit ?admin=1 to see this."""153    from src.data import (154        _get_accepted_counts, _load_pool, _pool_path,155        _load_reservations, _save_reservations,156        _expire_reservations, _release_returned_reservations,157        _reservation_lock_path,158    )159    from filelock import FileLock160 161    st.markdown("## 📊 Study Coverage Dashboard")162    st.caption(163        f"Study type: `{cfg['study_type']}` · "164        f"Seed: `{cfg['pair_selection_seed']}` · "165        f"Output repo: `{cfg['output_dataset_repo']}`"166    )167 168    if st.button("🔄 Refresh", type="primary"):169        # Invalidate caches so we re-scan HF and re-poll Prolific170        from src.data import _data_dir171        for f in _data_dir(cfg).glob("completion_cache*"):172            f.unlink()173        prolific_cache = _data_dir(cfg) / "prolific_returned_cache.json"174        if prolific_cache.exists():175            prolific_cache.unlink()176        st.rerun()177 178    # Release expired + returned/timed-out reservations before displaying179    lock = FileLock(str(_reservation_lock_path(cfg)), timeout=10)180    with lock:181        reservations = _load_reservations(cfg)182        _expire_reservations(reservations)183        _release_returned_reservations(reservations, cfg)184        _save_reservations(reservations, cfg)185 186    for cat_cfg in cfg["categories"]:187        cat   = cat_cfg["name"]188        pool  = _load_pool(str(_pool_path(cat, cfg)))189        total = len(pool)190 191        counts = _get_accepted_counts(cat, cfg)192 193        covered = sum(1 for v in counts.values() if v >= 1)194        reserved_uncovered = sum(195            1 for k in reservations196            if counts.get(k, 0) == 0197        )198        truly_uncovered = total - covered - reserved_uncovered199 200        st.markdown(f"### {cat.capitalize()}")201        col1, col2, col3, col4 = st.columns(4)202        col1.metric("Total items",     total)203        col2.metric("Covered ✅",      covered)204        col3.metric("In progress 🔄",  reserved_uncovered,205                    help="Reserved by active Prolific participants")206        col4.metric("Still needed ⚠️", truly_uncovered,207                    delta=f"-{truly_uncovered}" if truly_uncovered > 0 else None,208                    delta_color="inverse")209 210        if truly_uncovered == 0 and reserved_uncovered == 0:211            st.success(f"✅ All {total} items covered!")212        elif truly_uncovered == 0:213            st.info(f"🔄 {reserved_uncovered} item(s) in progress.")214        else:215            st.warning(216                f"⚠️ {truly_uncovered} item(s) still need a participant. "217                f"Send more Prolific slots."218            )219 220        st.markdown("---")221 222 223# ---------------------------------------------------------------------------224# 4. Main225# ---------------------------------------------------------------------------226def main() -> None:227    cfg = load_config()228 229    st.set_page_config(230        page_title="Product Study",231        page_icon="🛒",232        layout="centered",233    )234    inject_css()235 236    # Admin dashboard — visit ?admin=1237    try:238        params = st.query_params239    except Exception:240        params = {}241    if params.get("admin") == "1":242        ensure_datasets(cfg)243        _screen_admin(cfg)244        return245 246    if "study_state" not in st.session_state:247        ensure_datasets(cfg)248        st.session_state.study_state = init_state(cfg)249 250    s      = st.session_state.study_state251    screen = s.get("screen", "welcome")252 253    dispatch = {254        "welcome":      lambda: screen_welcome(s, cfg),255        "demographics": lambda: screen_demographics(s, cfg),256        "background":   lambda: screen_background(s, cfg),257        "item_intro":   lambda: (258            screen_pair_intro(s, cfg)259            if cfg["study_type"] == "preference"260            else screen_item_intro(s, cfg)261        ),262        "chat":         lambda: screen_chat(s, cfg),263        "post_rating":  lambda: screen_post_rating(s, cfg),264        "reflection":   lambda: screen_reflection(s, cfg),265        "done":         lambda: screen_done(s, cfg),266    }267 268    handler = dispatch.get(screen)269    if handler:270        handler()271    else:272        st.error(f"Unknown screen: {screen!r}")273 274 275if __name__ == "__main__":276    main()