import json import os import re import sys import time import html import urllib.error import urllib.request import xml.etree.ElementTree as ET from pathlib import Path from typing import Any, Optional import streamlit as st try: from huggingface_hub import HfApi except Exception: HfApi = None SRC = Path(__file__).resolve().parent REPO_ROOT = SRC.parent for extra in (SRC, REPO_ROOT / "src"): extra_str = str(extra) if extra_str not in sys.path: sys.path.insert(0, extra_str) import runner as runner_module from runner import PipelineConfig from common.paper_package import load_paper_package from step_08_annotation.pipeline import TwoPassAnnotationPipeline from streamlit_config import EXAMPLES, TAB_NAMES import replay as replay_module import system_run_data as system_run_module import system_live_runner as system_live_module from workflow_graph import STEP_COPY, WORK_PANEL_STEPS try: from streamlit_config import APP_NAV, APP_VIEWS except ImportError: APP_VIEWS = [ "Annotation Process", "System Run", ] APP_NAV = [ {"view": "Annotation Process", "icon": ":material/account_tree:"}, {"view": "System Run", "icon": ":material/play_circle:"}, ] DEFAULT_SOURCE_ROOT = str(REPO_ROOT / "src" / "processed_papers") DEFAULT_OUTPUT_ROOT = str(REPO_ROOT / "hf_space" / "runs") REPLAY_STEP_DELAY_SEC = float(os.getenv("REPLAY_STEP_DELAY_SEC", "0.35")) # Extra hold after a step completes so the workflow pulse is readable. REPLAY_STEP_HOLD_SEC = float(os.getenv("REPLAY_STEP_HOLD_SEC", "0.55")) DEFAULT_APP_VIEW = APP_VIEWS[0] CUSTOM_CSS = """ """ def get_secret(name: str, default: str = "") -> str: value = os.getenv(name) if value: return value try: return st.secrets[name] except Exception: return default def run_repo_config() -> tuple[str | None, str, str | None]: repo_id = get_secret("RUNS_REPO_ID", "") repo_type = get_secret("RUNS_REPO_TYPE", "dataset") token = get_secret("HF_WRITE_TOKEN", "") or get_secret("HF_TOKEN", "") return repo_id or None, repo_type, token or None def remote_run_prefix(job_id: str) -> str: return f"runs/{job_id}" def upload_run_artifact(job_dir: Path) -> str: repo_id, repo_type, token = run_repo_config() if not repo_id or not token: return "" if HfApi is None: return "upload_failed: huggingface_hub is not installed" job_id = job_dir.name remote_prefix = remote_run_prefix(job_id) uploaded: list[str] = [] try: api = HfApi(token=token) for name in ["input_ids.json", "run_config.json", "summary.txt"]: path = job_dir / name if path.exists(): api.upload_file( path_or_fileobj=str(path), path_in_repo=f"{remote_prefix}/{name}", repo_id=repo_id, repo_type=repo_type, commit_message=f"Upload {name} for {job_id}", ) uploaded.append(name) for folder_name in ["logs", "processed_papers", "two_pass_outputs"]: folder = job_dir / folder_name if not folder.exists(): continue files = [path for path in folder.rglob("*") if path.is_file()] if not files: continue api.upload_folder( folder_path=str(folder), path_in_repo=f"{remote_prefix}/{folder_name}", repo_id=repo_id, repo_type=repo_type, commit_message=f"Upload {folder_name} for {job_id}", ignore_patterns=["__pycache__/*", "*.pyc", "*.zip"], ) uploaded.append(f"{folder_name}[{len(files)} files]") return f"{repo_type}:{repo_id}/{remote_prefix}/ (uploaded: {', '.join(uploaded) or 'nothing'})" except Exception as exc: return f"upload_failed: {exc}" def _load_json(path: Path) -> Optional[dict]: if not path.exists(): return None try: return json.loads(path.read_text(encoding="utf-8")) except Exception: return None def _status_from_line(line: str, current: str) -> str: text = (line or "").strip() text = _display_log_line(text) if text.startswith("Pipeline stopped:"): return "Stopped" if text.startswith("Step "): return text if "failed" in text.lower(): return f"Failed: {text}" if "completed successfully" in text.lower(): return "Completed" return current def _display_log_line(line: str) -> str: text = (line or "").strip() if text.startswith("Step ") and " failed." in text: return text.splitlines()[0] if text == "[annotation] starting cluster-first two-pass annotation": return f"Step 8/8: {STEP_COPY[8]}" if text.startswith("[annotation] complete:"): return "Step 8 complete" # Rewrite engineering step labels to the shared audience-facing copy. m = re.match(r"Step\s+(\d+)\s*/\s*\d+\s*:\s*(.*)$", text) if m: n = int(m.group(1)) if n in STEP_COPY: return f"Step {n}/8: {STEP_COPY[n]}" if text == "Pipeline completed successfully.": return text return text def _resolve_graph_sources(paper_input: str) -> tuple[Optional[Path], Optional[dict]]: """Paper dir + annotation payload for the workflow graph (trace or session).""" entry = _paper_result_for_input(paper_input) if entry: paper_dir_path = entry.get("paper_dir_path") payload_path = entry.get("annotation_payload_path") paper_dir = Path(paper_dir_path) if paper_dir_path else None payload = _load_json(Path(payload_path)) if payload_path else None if paper_dir and paper_dir.exists(): return paper_dir, payload if isinstance(payload, dict) else None trace = replay_module.load_trace(paper_input) if not trace: return None, None paper_dir_path = trace.get("paper_dir_path") payload_path = trace.get("annotation_payload_path") paper_dir = Path(paper_dir_path) if paper_dir_path else None payload = _load_json(Path(payload_path)) if payload_path else None return paper_dir, payload if isinstance(payload, dict) else None def _workflow_run_id(paper_input: str) -> str: arxiv_id = replay_module.parse_arxiv_id(paper_input or "") or "default" return f"scipaths:{arxiv_id}" def _mount_graph_shell(graph_slot) -> None: """Mount the graph canvas shell (st.iframe HTML srcdoc + tree layout JS).""" if graph_slot is None: return # Call-time import; avoid importlib.reload so identical shell HTML can be # reused across Streamlit runs instead of forcing a fresh iframe each time. from workflow_graph import render_graph_shell render_graph_shell(graph_slot) def _push_graph(courier_slot, paper_input: str, events: list[str]) -> None: """Push an incremental graph model into the mounted shell via postMessage. This never re-emits the graph iframe, so the canvas grows in place instead of reloading on every step. """ if courier_slot is None: return paper_dir, payload = _resolve_graph_sources(paper_input) render_events = list(events or []) if not render_events and paper_dir is not None: # Idle selected paper: show target only (no active pulse). render_events = ["Step 1/8: Load the paper", "Step 1 complete"] # Import at call-time so Streamlit always picks up workflow_graph changes. from workflow_graph import build_synced_model, push_graph_update model = build_synced_model( paper_dir=paper_dir, payload=payload, events=render_events, run_id=_workflow_run_id(paper_input), ) push_graph_update(courier_slot, model) def _format_step_event(line: str) -> str: text = _display_log_line(line) if not text: return "" return text STATE_DEFAULTS = { "paper_input": "", "run_status": "Idle", "run_logs": [], "run_events": [], "artifact_path": None, "run_dir_path": None, "paper_dir_path": None, "annotation_payload_path": None, "run_summary": None, "annotation_skipped_reason": None, "pipeline_stopped_reason": None, "pipeline_failed_reason": None, "remote_artifact_ref": "", "replay_mode": False, "live_run_mode": False, # Locked demo selection (survives run clears / post-run reruns). "selected_demo_url": "", # System Run case-study selection (independent of Annotation Process). "system_run_mode": "prerun", # "prerun" | "live" "selected_system_method": system_run_module.DEFAULT_SYSTEM_METHOD, "selected_system_case": system_run_module.DEFAULT_SYSTEM_CASE, "selected_live_method": "codeagent_parametric", # Per-method Live Run state (parametric vs websearch_deep are independent). "live_run_by_method": {}, "app_view": DEFAULT_APP_VIEW, # Per-paper result cache so Clusters/Decomposition follow the selected paper. "run_results_by_paper": {}, } # Widget keys owned by Streamlit; assign defaults instead of deleting them. WIDGET_STATE_KEYS = {"paper_input"} # Survives per-run clears; only wiped by Reset session. PRESERVED_STATE_KEYS = { "app_view", "run_results_by_paper", "live_run_mode", "selected_demo_url", "system_run_mode", "selected_system_method", "selected_system_case", "selected_live_method", "live_run_by_method", } def _copy_default(value: Any) -> Any: if isinstance(value, list): return list(value) if isinstance(value, dict): return dict(value) return value def _ensure_state(): for key, value in STATE_DEFAULTS.items(): st.session_state.setdefault(key, _copy_default(value)) def _clear_run_state(*, keep_paper_input: bool = True) -> None: """Reset transient run UI state for a new pipeline/replay. Never writes ``paper_input`` after the text-input widget exists. Preserves ``run_results_by_paper`` so other papers' results stay available. """ for key, value in STATE_DEFAULTS.items(): if key in WIDGET_STATE_KEYS or key in PRESERVED_STATE_KEYS: continue st.session_state[key] = _copy_default(value) if not keep_paper_input: # Defer clearing/replacing the widget value until before it is created. st.session_state["_pending_paper_input"] = "" def _store_paper_result(arxiv_id: str, result: dict) -> None: if not arxiv_id: return cache = st.session_state.setdefault("run_results_by_paper", {}) cache[str(arxiv_id)] = dict(result) def _paper_result_for_input(paper_input: str) -> Optional[dict]: arxiv_id = replay_module.parse_arxiv_id(paper_input or "") if not arxiv_id: return None cache = st.session_state.get("run_results_by_paper") or {} entry = cache.get(arxiv_id) return entry if isinstance(entry, dict) else None def _apply_pending_paper_input() -> None: """Apply deferred paper_input changes before the text_input widget mounts.""" if "_pending_paper_input" not in st.session_state: return st.session_state["paper_input"] = st.session_state.pop("_pending_paper_input") def _paper_1_url() -> str: urls = list(EXAMPLES.values()) return urls[0] if urls else "" def _set_demo_paper(url: str) -> None: """Pin the demo paper selection (source of truth across reruns).""" st.session_state["live_run_mode"] = False st.session_state["selected_demo_url"] = url st.session_state["_pending_paper_input"] = url def _on_pick_demo(url: str) -> None: _set_demo_paper(url) def _on_pick_live_run() -> None: st.session_state["live_run_mode"] = True st.session_state["_pending_paper_input"] = "" def _select_paper_1() -> None: """Select demo Paper 1 in the picker (clears live-run mode).""" _set_demo_paper(_paper_1_url()) def _set_system_method(method_id: str) -> None: """Pin System Run method and reset case when the method changes.""" prev = st.session_state.get("selected_system_method") st.session_state["selected_system_method"] = method_id if prev != method_id: st.session_state["selected_system_case"] = system_run_module.default_case_for_method( method_id ) def _set_system_case(case_key: str) -> None: st.session_state["selected_system_case"] = case_key def _on_pick_system_method(method_id: str) -> None: _set_system_method(method_id) def _on_pick_system_case(case_key: str) -> None: _set_system_case(case_key) def _on_pick_system_prerun() -> None: st.session_state["system_run_mode"] = "prerun" def _on_pick_system_live() -> None: st.session_state["system_run_mode"] = "live" def _on_pick_live_method(method_id: str) -> None: st.session_state["selected_live_method"] = method_id def _empty_live_method_state() -> dict[str, Any]: return { "result": None, "judged": None, "logs": [], "status": "Idle", "pending_action": None, # "run" | "evaluate" | None "ran_once": False, "evaluated_once": False, } def _live_method_state(method_id: str) -> dict[str, Any]: """Return the mutable per-method Live Run slot (isolated across settings).""" store = st.session_state.get("live_run_by_method") if not isinstance(store, dict): store = {} st.session_state["live_run_by_method"] = store slot = store.get(method_id) if not isinstance(slot, dict): slot = _empty_live_method_state() store[method_id] = slot else: slot.setdefault("result", None) slot.setdefault("judged", None) slot.setdefault("logs", []) slot.setdefault("status", "Idle") slot.setdefault("pending_action", None) slot.setdefault("ran_once", False) slot.setdefault("evaluated_once", False) return slot def _render_system_live_run_view() -> None: """Live Run: one AVerImaTeC example + parametric / websearch_deep + Task A eval.""" try: example = system_live_module.example_claim() except Exception as exc: st.error(f"Could not load demo claim: {exc}") return live_method = str( st.session_state.get("selected_live_method") or "codeagent_parametric" ).strip() method_ids = {m["id"] for m in system_live_module.METHOD_UI} if live_method not in method_ids: live_method = "codeagent_parametric" st.session_state["selected_live_method"] = live_method slot = _live_method_state(live_method) status = str(slot.get("status") or "Idle") busy = status in {"Running", "Evaluating"} can_evaluate = bool(slot.get("result")) and not busy run_label = "Re-run System" if slot.get("ran_once") else "Run SciFy System" eval_label = "Re-evaluate" if slot.get("evaluated_once") else "Evaluate" st.markdown('
Example claim
', unsafe_allow_html=True) paper_title = str(example.get("paper_title") or "").strip() or "Unknown paper" st.markdown( f"""
Claim
{_escape(_ensure_claim_period(example.get("claim")))}
Paper: {_escape(paper_title)}
""", unsafe_allow_html=True, ) st.markdown('
Setting
', unsafe_allow_html=True) method_cols = st.columns(len(system_live_module.METHOD_UI)) for i, item in enumerate(system_live_module.METHOD_UI): with method_cols[i]: st.button( item["label"], key=f"live_method::{item['id']}", type="primary" if item["id"] == live_method else "secondary", use_container_width=True, on_click=_on_pick_live_method, args=(item["id"],), disabled=busy, ) st.markdown('
Action
', unsafe_allow_html=True) run_col, eval_col = st.columns(2) with run_col: run_clicked = st.button( run_label, type="secondary", use_container_width=True, key=f"live_run_system_btn::{live_method}", disabled=busy, ) with eval_col: eval_clicked = st.button( eval_label, type="secondary", use_container_width=True, key=f"live_eval_btn::{live_method}", disabled=busy or not can_evaluate, ) with st.container(key="live_run_terminal"): log_box = st.empty() logs = list(slot.get("logs") or []) def _flush_logs() -> None: body = "\n".join(logs[-400:]) if logs else "Waiting for run output…" log_box.code(body, language="text") st.caption(f"Status: {status}") _flush_logs() # Phase 1: click → mark this method busy and rerun so buttons grey out immediately. if run_clicked and not busy: slot["pending_action"] = "run" slot["judged"] = None slot["result"] = None slot["logs"] = [] slot["status"] = "Running" st.rerun() if eval_clicked and not busy and slot.get("result"): slot["pending_action"] = "evaluate" slot["status"] = "Evaluating" st.rerun() # Phase 2: execute pending action while buttons stay disabled. pending = slot.get("pending_action") if pending == "run" and status == "Running": slot["pending_action"] = None logs = [] status = "Running" def _log(line: str) -> None: logs.append(line) slot["logs"] = list(logs) log_box.code("\n".join(logs[-400:]), language="text") try: result = system_live_module.run_live_method(live_method, log_callback=_log) slot["result"] = result slot["status"] = "Completed" slot["ran_once"] = True status = "Completed" _log(f"Done. ingredients={len(result.get('ingredients') or [])}") except Exception as exc: slot["result"] = None slot["status"] = "Failed" slot["ran_once"] = True status = "Failed" _log(f"ERROR: {exc}") st.rerun() if pending == "evaluate" and status == "Evaluating" and slot.get("result"): slot["pending_action"] = None logs = list(slot.get("logs") or []) status = "Evaluating" def _log_eval(line: str) -> None: if line.strip().startswith("{") and '"ok"' in line: return logs.append(line) slot["logs"] = list(logs) log_box.code("\n".join(logs[-400:]), language="text") try: judged = system_live_module.evaluate_live_run( slot["result"], log_callback=_log_eval, ) slot["judged"] = judged slot["status"] = "Evaluated" slot["evaluated_once"] = True status = "Evaluated" _log_eval( f"Evaluated. F1={float(judged.get('f1') or 0):.2f} " f"R={float(judged.get('recall') or 0):.2f} " f"P={float(judged.get('precision') or 0):.2f}" ) except Exception as exc: slot["status"] = "Eval failed" slot["evaluated_once"] = True status = "Eval failed" _log_eval(f"ERROR: {exc}") st.rerun() run_result = slot.get("result") judged = slot.get("judged") if run_result: case = system_live_module.live_result_to_case_card(run_result, judged) # Claim/paper already shown above in the example block; card is results only. result_html = _system_case_card_html(case) st.markdown( f'
{result_html}
', unsafe_allow_html=True, ) def _normalize_abstract(text: str) -> str: cleaned = re.sub(r"\s+", " ", (text or "").strip()) cleaned = re.sub(r"^(abstract)\s*[:.]?\s*", "", cleaned, flags=re.IGNORECASE) # Drop trailing PDF footnote markers like ").1" / ".”2". cleaned = re.sub(r"(?<=[.!?\)\]\"'”’])\d+$", "", cleaned) return cleaned.strip() def _metadata_from_paper_dir(paper_dir: Path) -> Optional[dict[str, str]]: data = _load_json(paper_dir / "paper_metadata.json") record = None if isinstance(data, list) and data: record = data[0] if isinstance(data[0], dict) else None elif isinstance(data, dict): record = data if not record: return None title = str(record.get("title") or "").strip() abstract = _normalize_abstract(str(record.get("abstract") or "")) if not title and not abstract: return None return {"title": title, "abstract": abstract} @st.cache_data(show_spinner=False, ttl=6 * 60 * 60) def _fetch_arxiv_title_abstract(arxiv_id: str) -> Optional[dict[str, str]]: url = f"http://export.arxiv.org/api/query?id_list={arxiv_id}" try: with urllib.request.urlopen(url, timeout=12) as resp: # noqa: S310 payload = resp.read() except (urllib.error.URLError, TimeoutError, OSError): return None try: root = ET.fromstring(payload) except ET.ParseError: return None ns = {"atom": "http://www.w3.org/2005/Atom"} entry = root.find("atom:entry", ns) if entry is None: return None title = re.sub(r"\s+", " ", (entry.findtext("atom:title", default="", namespaces=ns) or "").strip()) abstract = _normalize_abstract(entry.findtext("atom:summary", default="", namespaces=ns) or "") if not title and not abstract: return None return {"title": title, "abstract": abstract} def _resolve_paper_preview(paper_input: str) -> Optional[dict[str, str]]: """Title/abstract for the selected paper (trace metadata, run dir, or arXiv).""" arxiv_id = replay_module.parse_arxiv_id(paper_input or "") if not arxiv_id: return None entry = _paper_result_for_input(paper_input) if entry and entry.get("paper_dir_path"): meta = _metadata_from_paper_dir(Path(entry["paper_dir_path"])) if meta: return meta trace_dir = replay_module.find_trace_dir(paper_input) if trace_dir is not None: meta = _metadata_from_paper_dir(trace_dir / "processed_papers" / arxiv_id) if meta: return meta return _fetch_arxiv_title_abstract(arxiv_id) def _render_paper_preview(paper_input: str) -> None: preview = _resolve_paper_preview(paper_input) if not preview: st.caption("Paper metadata unavailable for this ID yet.") return title = preview.get("title") or "Untitled paper" abstract = preview.get("abstract") or "" # Title as HTML; abstract as markdown so `$...$` goes through Streamlit KaTeX. body = ( f"
" f"
{_esc(title)}
" f"
" ) if abstract: body = f"{body}\n\n{abstract}" st.markdown(body, unsafe_allow_html=True) def _derive_work_step_states(events: list[str]) -> dict[int, str]: """Map pipeline step numbers to pending | active | done | failed.""" order = [n for n, _ in WORK_PANEL_STEPS] started: set[int] = set() completed: set[int] = set() failed = False pipeline_complete = False for raw in events or []: text = str(raw) lower = text.lower() if text == "Pipeline completed successfully.": pipeline_complete = True if "failed" in lower: failed = True m_done = re.search(r"Step\s+(\d+)\s+complete", text, re.IGNORECASE) if m_done: completed.add(int(m_done.group(1))) continue m_start = re.search(r"Step\s+(\d+)\s*/", text) if m_start: started.add(int(m_start.group(1))) continue if "[annotation]" in lower or "annotate target contributions" in lower: if "complete" in lower or "skipped" in lower: completed.add(8) else: started.add(8) max_started = max(started) if started else 0 states: dict[int, str] = {} for n in order: if n in completed or (n in started and n < max_started): states[n] = "done" elif n in started: states[n] = "failed" if failed else "active" else: states[n] = "pending" if pipeline_complete and not failed: for n in order: # Replay traces often omit explicit completes for every step. if n < 8 or 8 in completed or 8 in started: states[n] = "done" if 8 in completed: states[8] = "done" return states def _display_work_status(status: str) -> str: """Steps badge only shows Idle / Running (Stopped/Failed/Completed → Idle).""" text = (status or "").strip() if not text: return "Idle" lower = text.lower() if lower in {"running", "starting"} or text.startswith("Step "): return "Running" return "Idle" def _work_panel_html( events: list[str], *, status: str = "Idle", mode_note: str = "", ) -> str: states = _derive_work_step_states(events) status_label = f"{_display_work_status(status)}{mode_note}" rows: list[str] = [] for n, label in WORK_PANEL_STEPS: state = states.get(n, "pending") busy = " aria-busy='true'" if state == "active" else "" rows.append( "
  • " "" f"{_esc(label)}" "
  • " ) body = ( f"" if rows else "

    Waiting for the first step…

    " ) return ( "
    " "
    " "
    Steps
    " f"
    {_esc(status_label)}
    " "
    " f"{body}" "
    " ) def _render_work_panel( placeholder, events: list[str], *, status: str = "Idle", mode_note: str = "", ) -> None: placeholder.markdown( _work_panel_html(events, status=status, mode_note=mode_note), unsafe_allow_html=True, ) def _reset_all_session_state() -> None: """Hard reset used by Reset session: wipe every session key, then re-seed defaults.""" for key in list(st.session_state.keys()): del st.session_state[key] for key, value in STATE_DEFAULTS.items(): st.session_state[key] = _copy_default(value) def _metric_card(label: str, value: Any): return ( f"
    " f"
    {_esc(label)}
    " f"
    {_esc(value)}
    " f"
    " ) def _esc(value: Any) -> str: return html.escape("" if value is None else str(value)) def _safe_int(value: Any, default: int = 0) -> int: try: return int(value) except (TypeError, ValueError): return default def _grounding_html(grounding: Optional[dict], label: str, kind: str) -> str: if not grounding: return "" title = ( grounding.get("ref_title") or grounding.get("title") or grounding.get("paper_id") or grounding.get("ref_id") or "__NONE__" ) meta = [] if grounding.get("paper_id"): meta.append(f"paper_id: {grounding.get('paper_id')}") elif grounding.get("ref_id"): meta.append(f"ref_id: {grounding.get('ref_id')}") if grounding.get("ref_year"): meta.append(str(grounding.get("ref_year"))) authors = grounding.get("ref_authors") if isinstance(authors, list) and authors: meta.append(", ".join(str(author) for author in authors[:3])) meta_html = f"
    {_esc(' · '.join(meta))}
    " if meta else "" return ( "
    " f"
    {_esc(label)}
    " f"
    {_esc(title)}
    " f"{meta_html}" "
    " ) def _study_key(item: dict) -> str: for key in ["paper_id", "ref_id", "ref_title", "title"]: value = item.get(key) if value: return str(value).lower() return "" def _collect_grounded_studies(discoveries: list[dict], ingredients: list[dict]) -> list[dict]: studies: list[dict] = [] seen: set[str] = set() for item in discoveries: if not isinstance(item, dict): continue copied = dict(item) copied["_grounding_kind"] = "grounding" copied["_grounding_label"] = "Grounding" key = _study_key(copied) if key: seen.add(key) studies.append(copied) for idx, ingredient in enumerate(ingredients, start=1): if not isinstance(ingredient, dict): continue canonical = ingredient.get("canonical_grounding") or {} canonical_key = _study_key(canonical) if isinstance(canonical, dict) else "" annotation = ingredient.get("canonical_annotation") or {} for ref in ingredient.get("additional_groundings") or []: if not isinstance(ref, dict): continue key = _study_key(ref) if key and (key == canonical_key or key in seen): continue copied = dict(ref) copied["_grounding_kind"] = "grounding" copied["_grounding_label"] = f"Grounding for enabling contribution {idx}" copied.setdefault("role", annotation.get("role") or ", ".join(annotation.get("roles") or [])) copied.setdefault("contribution", annotation.get("contribution")) copied.setdefault("rationale", annotation.get("rationale")) if key: seen.add(key) studies.append(copied) return studies def _render_reference_list(discoveries: list[dict], ingredients: Optional[list[dict]] = None): studies = _collect_grounded_studies(discoveries, ingredients or []) if not studies: st.markdown("
    No grounded studies listed for this target contribution.
    ", unsafe_allow_html=True) return for item in studies: title = item.get("ref_title") or item.get("title") or item.get("ref_id") or item.get("paper_id") or "Untitled reference" meta = [] if item.get("_grounding_label"): meta.append(str(item.get("_grounding_label"))) if item.get("role"): meta.append(str(item.get("role"))) if item.get("ref_year"): meta.append(str(item.get("ref_year"))) class_name = "cluster-card additional-study" body = [f"
    {_esc(title)}
    "] if meta: body.append(f"
    {_esc(' · '.join(meta))}
    ") if item.get("contribution"): body.append(f"
    Contribution. {_esc(item.get('contribution'))}
    ") if item.get("rationale"): body.append(f"
    Rationale. {_esc(item.get('rationale'))}
    ") body.append("
    ") st.markdown("".join(body), unsafe_allow_html=True) def _render_claims_tab(payload: Optional[dict]): if not payload: st.markdown("
    No annotation payload is available yet.
    ", unsafe_allow_html=True) return claims = payload.get("claims") or [] if not claims: st.markdown("
    The run completed, but no target contributions were produced.
    ", unsafe_allow_html=True) return st.markdown( "

    This decomposition is done in hindsight: SciPaths uses observed downstream citation clusters to identify reusable target contributions, then reconstructs the enabling contributions and groundings behind each one.

    ", unsafe_allow_html=True, ) for idx, claim in enumerate(claims, start=1): claim_id = claim.get("claim_id") or f"C{idx}" claim_text = claim.get("rewritten_claim") or claim.get("text") or "(missing target contribution text)" ingredients = claim.get("ingredients") or [] discoveries = claim.get("enabling_discoveries") or [] grounded_studies = _collect_grounded_studies(discoveries, ingredients) meta_bits = [] if claim.get("decision"): meta_bits.append(str(claim.get("decision"))) if claim.get("cluster_id"): meta_bits.append(f"cluster {claim.get('cluster_id')}") meta_bits.append(f"{len(ingredients)} enabling contribution{'s' if len(ingredients) != 1 else ''}") meta_bits.append(f"{len(grounded_studies)} grounded stud{'ies' if len(grounded_studies) != 1 else 'y'}") st.markdown( f"""
    Target contribution {idx} · {_esc(claim_id)}
    {_esc(claim_text)}
    {_esc(' · '.join(meta_bits))}
    """, unsafe_allow_html=True, ) left, right = st.columns([1.7, 1.0], gap="large") with left: st.markdown("
    Decomposition
    ", unsafe_allow_html=True) if not ingredients: st.markdown("
    No enabling contributions for this target contribution.
    ", unsafe_allow_html=True) for ingredient_idx, ingredient in enumerate(ingredients, start=1): annotation = ingredient.get("canonical_annotation") or {} role = annotation.get("role") or ", ".join(annotation.get("roles") or []) or "UNSPECIFIED" canonical_grounding = ingredient.get("canonical_grounding") or {} extras = ingredient.get("additional_groundings") or [] grounding_parts = [] if canonical_grounding: grounding_parts.append( _grounding_html(canonical_grounding, "Grounding", "grounding") ) for ref in extras: if not isinstance(ref, dict): continue if canonical_grounding and ( ref.get("paper_id") == canonical_grounding.get("paper_id") or ref.get("ref_id") == canonical_grounding.get("ref_id") ): continue grounding_parts.append( _grounding_html(ref, "Grounding", "grounding") ) if not grounding_parts: canonical_ref_id = ingredient.get("canonical_ref_id") or "__NONE__" grounding_parts.append( "
    " "
    Grounding
    " f"
    {_esc(canonical_ref_id)}
    " "
    " ) grounding_block = ( "
    " f"
    Groundings for enabling contribution {ingredient_idx}
    " + "".join(grounding_parts) + "
    " ) st.markdown( f"""
    {ingredient_idx}. {_esc(ingredient.get('ingredient') or '(missing enabling contribution)')}
    {_esc(role)}
    Contribution. {_esc(annotation.get('contribution') or '')}
    Rationale. {_esc(annotation.get('rationale') or '')}
    Evidence. {_esc(annotation.get('evidence_span') or '')}
    {grounding_block}
    """, unsafe_allow_html=True, ) with right: st.markdown("
    Groundings
    ", unsafe_allow_html=True) _render_reference_list(discoveries, ingredients) def _render_clusters_tab(discovery: Optional[dict], contributions: list[dict]): if not discovery: st.markdown("
    No refined cluster file is available yet.
    ", unsafe_allow_html=True) return st.markdown( "

    Citation clusters look forward: they summarize how later papers use or extend the input paper, showing which contributions became reusable.

    ", unsafe_allow_html=True, ) clusters = discovery.get("clusters") or [] dropped = discovery.get("dropped_clusters") or [] if not clusters: st.markdown("
    No valid downstream usage clusters survived refinement and filtering.
    ", unsafe_allow_html=True) if dropped: with st.expander(f"Dropped clusters ({len(dropped)})", expanded=False): st.json(dropped) return for cluster in clusters: cluster_id = cluster.get("cluster_id", "") rep = cluster.get("representative_claim") or cluster.get("cluster_title") or "(missing representative claim)" count = _safe_int(cluster.get("count"), len(cluster.get("claim_indices") or [])) source_ids = cluster.get("source_cluster_ids") or [] merge_rationale = cluster.get("merge_rationale") or "" st.markdown( f"""
    {_esc(rep)}
    Cluster {_esc(cluster_id)} · {count} contribution instance{'s' if count != 1 else ''}
    """, unsafe_allow_html=True, ) meta_cols = st.columns([1.3, 1.3, 1.4]) with meta_cols[0]: st.caption("Cluster ID") st.code(str(cluster_id), language="text") with meta_cols[1]: st.caption("Source clusters") st.code(", ".join(str(x) for x in source_ids) if source_ids else "singleton", language="text") with meta_cols[2]: st.caption("Merge rationale") st.write(merge_rationale or "—") claim_indices = cluster.get("claim_indices") or [] if claim_indices: with st.expander(f"Linked contribution instances ({len(claim_indices)})", expanded=False): for idx in claim_indices: try: j = int(idx) except Exception: continue if 0 <= j < len(contributions): item = contributions[j] or {} title = item.get("citing_title") or item.get("citing_paper_id") or "Unknown citing paper" claim = item.get("paper_claim") or item.get("claim") or "(missing claim)" rationale = item.get("rationale") or "" evidence = item.get("evidence_span") or "" st.markdown(f"**{title}**") st.write(claim) if rationale: st.caption(f"Rationale: {rationale}") if evidence: st.caption(f"Evidence: {evidence}") st.divider() if dropped: with st.expander(f"Dropped clusters ({len(dropped)})", expanded=False): st.json(dropped) def run_replay_stream( paper_input: str, work_panel=None, metrics_slot=None, graph_slot=None, courier_slot=None, ) -> bool: """Replay a saved demo trace into session state. Returns False if no trace exists.""" trace = replay_module.load_trace(paper_input) if not trace: return False _clear_run_state(keep_paper_input=True) st.session_state["run_status"] = "Running" st.session_state["replay_mode"] = True # Graph can read the finished trace artifacts while steps reveal progressively. st.session_state["paper_dir_path"] = trace.get("paper_dir_path") st.session_state["annotation_payload_path"] = trace.get("annotation_payload_path") try: import neo4j_workflow as _neo _neo.reset_run(_workflow_run_id(paper_input)) except Exception: pass panel = work_panel if work_panel is not None else st.empty() events: list[str] = [] _render_work_panel(panel, events, status="Running") _push_graph(courier_slot, paper_input, events) for line in trace.get("run_events") or []: display_line = _display_log_line(str(line)) if not display_line: continue if display_line not in events: events.append(display_line) _render_work_panel(panel, events, status="Running") _push_graph(courier_slot, paper_input, events) delay = REPLAY_STEP_DELAY_SEC if re.search(r"Step\s+\d+\s+complete", display_line, re.IGNORECASE): delay += REPLAY_STEP_HOLD_SEC time.sleep(max(0.0, delay)) # Commit final replay payload for the Clusters / Decomposition tabs. st.session_state["run_status"] = trace.get("run_status") or trace.get("status") or "Completed" st.session_state["run_logs"] = list(trace.get("run_logs") or events) st.session_state["run_events"] = list(trace.get("run_events") or events) st.session_state["artifact_path"] = trace.get("artifact_path") st.session_state["run_dir_path"] = trace.get("run_dir_path") st.session_state["paper_dir_path"] = trace.get("paper_dir_path") st.session_state["annotation_payload_path"] = trace.get("annotation_payload_path") st.session_state["annotation_skipped_reason"] = trace.get("annotation_skipped_reason") st.session_state["pipeline_stopped_reason"] = trace.get("pipeline_stopped_reason") st.session_state["pipeline_failed_reason"] = trace.get("pipeline_failed_reason") st.session_state["remote_artifact_ref"] = "" st.session_state["replay_mode"] = True arxiv_id = str(trace.get("arxiv_id") or replay_module.parse_arxiv_id(paper_input) or "") _store_paper_result( arxiv_id, { "run_status": st.session_state["run_status"], "run_logs": list(st.session_state["run_logs"]), "run_events": list(st.session_state["run_events"]), "artifact_path": st.session_state.get("artifact_path"), "run_dir_path": st.session_state.get("run_dir_path"), "paper_dir_path": st.session_state.get("paper_dir_path"), "annotation_payload_path": st.session_state.get("annotation_payload_path"), "annotation_skipped_reason": st.session_state.get("annotation_skipped_reason"), "pipeline_stopped_reason": st.session_state.get("pipeline_stopped_reason"), "pipeline_failed_reason": st.session_state.get("pipeline_failed_reason"), "replay_mode": True, }, ) _render_work_panel( panel, st.session_state["run_events"], status=st.session_state["run_status"], ) _push_graph(courier_slot, paper_input, st.session_state["run_events"]) _refresh_overview_for_paper(paper_input, metrics_slot) return True def run_two_pass_annotation( paper_dir: Path, annotation_output_root: Path, llm_provider: str, llm_model: str, formatter_model: str, judge_model: str, candidate_count: int, ): paper = load_paper_package(paper_dir) pipeline = TwoPassAnnotationPipeline( provider=llm_provider, model=llm_model, formatter_model=formatter_model or None, judge_model=judge_model or None, output_root=annotation_output_root, annotator_id="streamlit_hf_space", candidate_count=max(1, int(candidate_count)), formatter_max_attempts=3, include_reference_examples=True, prompt_profile="full", ) result = pipeline.run(paper) return result.result, result.run_dir def run_pipeline_stream( paper_input: str, source_root: str, output_root: str, llm_provider: str, llm_model: str, llm_model_step4: str, formatter_model: str, judge_model: str, candidate_count: int, work_panel=None, metrics_slot=None, graph_slot=None, courier_slot=None, ): gemini_key = get_secret("GEMINI_API_KEY") if gemini_key: os.environ["GEMINI_API_KEY"] = gemini_key # Drop previous success/failure UI before this run starts rendering progress. _clear_run_state(keep_paper_input=True) st.session_state["run_status"] = "Starting" try: import neo4j_workflow as _neo _neo.reset_run(_workflow_run_id(paper_input)) except Exception: pass cfg = PipelineConfig( repo_root=REPO_ROOT, source_root=Path(source_root).expanduser().resolve(), paper_input=paper_input.strip(), llm_provider=llm_provider.strip() or "gemini", llm_model=llm_model.strip() or "gemini-3.1-pro-preview", llm_model_step4=llm_model_step4.strip() or "gemini-3-flash-preview", model_path="Deep-Citation/Workspace/acl_scicite_wksp_trl/best_model.pt", model_data_dir="Deep-Citation/Data", model_class_def="Deep-Citation/Data/class_def.json", model_lm="scibert", device="cpu", embedding_model="sentence-transformers/all-mpnet-base-v2", ) panel = work_panel if work_panel is not None else st.empty() status = "Starting" logs: list[str] = [] events: list[str] = [] seen_events: set[str] = set() artifact_path = None annotation_payload_path = None annotation_skipped_reason = None run_summary = None pipeline_stopped_reason = None pipeline_failed_reason = None def render_activity(items: list[str], *, current_status: str | None = None): _render_work_panel( panel, items, status=current_status or status, mode_note="", ) _push_graph(courier_slot, paper_input, items) def append_display_line(line: str): nonlocal status display_line = _display_log_line(line) if not display_line: return logs.append(display_line) status = _status_from_line(display_line, status) event = _format_step_event(display_line) if event and event not in seen_events: seen_events.add(event) events.append(event) render_activity(events, current_status=status) render_activity(events, current_status=status) for line, maybe_artifact in runner_module.run_pipeline(cfg, Path(output_root).expanduser().resolve()): if line: if line.strip() == "Pipeline completed successfully.": if maybe_artifact: artifact_path = maybe_artifact continue display_line = _display_log_line(line) if display_line: logs.append(display_line) status = _status_from_line(display_line, status) if display_line.startswith("Pipeline stopped:"): pipeline_stopped_reason = display_line if "failed" in display_line.lower(): pipeline_failed_reason = display_line event = _format_step_event(display_line) if event and event not in seen_events: seen_events.add(event) events.append(event) if maybe_artifact: artifact_path = maybe_artifact try: job_dir = Path(str(maybe_artifact)).with_suffix("") paper_id = runner_module.parse_arxiv_id(paper_input.strip()) st.session_state["paper_dir_path"] = str(job_dir / "processed_papers" / paper_id) except Exception: pass render_activity(events, current_status=status) run_dir_path = None paper_dir_path = None remote_artifact_ref = "" if artifact_path: job_dir = Path(str(artifact_path)).with_suffix("") run_dir_path = str(job_dir) paper_id = runner_module.parse_arxiv_id(paper_input.strip()) paper_dir = job_dir / "processed_papers" / paper_id paper_dir_path = str(paper_dir) if pipeline_failed_reason: annotation_skipped_reason = f"{pipeline_failed_reason} Annotation was not run." elif pipeline_stopped_reason: annotation_skipped_reason = f"{pipeline_stopped_reason} Annotation was not run." else: discovery = _load_json(paper_dir / "usage_discovery_from_contributions.json") or {} refined_clusters = discovery.get("clusters") or [] if not refined_clusters: annotation_skipped_reason = "No valid downstream usage clusters remained after refinement and filtering. Annotation was skipped." logs.append("[annotation] skipped: no refined downstream usage clusters") else: append_display_line("[annotation] starting cluster-first two-pass annotation") try: run_output, annotation_run_dir = run_two_pass_annotation( paper_dir=paper_dir, annotation_output_root=job_dir / "two_pass_outputs", llm_provider=llm_provider, llm_model=llm_model, formatter_model=formatter_model, judge_model=judge_model, candidate_count=candidate_count, ) payload_path = run_output.get("ui_payload_path") if isinstance(run_output, dict) else None if payload_path and Path(payload_path).exists(): annotation_payload_path = str(Path(payload_path)) append_display_line(f"[annotation] complete: {annotation_run_dir}") except Exception as exc: pipeline_failed_reason = f"Annotation failed: {exc}" annotation_skipped_reason = pipeline_failed_reason logs.append(f"[annotation] failed: {exc}") logs.append("[upload] uploading run artifact to Hugging Face dataset") remote_artifact_ref = upload_run_artifact(job_dir) if remote_artifact_ref: logs.append(f"[upload] {remote_artifact_ref}") else: logs.append("[upload] skipped: RUNS_REPO_ID/HF_WRITE_TOKEN not configured") if not pipeline_stopped_reason and not pipeline_failed_reason: append_display_line("Pipeline completed successfully.") if pipeline_failed_reason: status = "Failed" elif artifact_path and pipeline_stopped_reason: status = "Stopped" else: status = "Completed" if artifact_path else "Failed" render_activity(events, current_status=status) st.session_state["run_status"] = status st.session_state["run_logs"] = logs st.session_state["run_events"] = events st.session_state["artifact_path"] = artifact_path st.session_state["run_dir_path"] = run_dir_path st.session_state["paper_dir_path"] = paper_dir_path st.session_state["annotation_payload_path"] = annotation_payload_path st.session_state["annotation_skipped_reason"] = annotation_skipped_reason st.session_state["pipeline_stopped_reason"] = pipeline_stopped_reason st.session_state["pipeline_failed_reason"] = pipeline_failed_reason st.session_state["run_summary"] = run_summary st.session_state["remote_artifact_ref"] = remote_artifact_ref st.session_state["replay_mode"] = False arxiv_id = replay_module.parse_arxiv_id(paper_input) or "" _store_paper_result( arxiv_id, { "run_status": status, "run_logs": list(logs), "run_events": list(events), "artifact_path": artifact_path, "run_dir_path": run_dir_path, "paper_dir_path": paper_dir_path, "annotation_payload_path": annotation_payload_path, "annotation_skipped_reason": annotation_skipped_reason, "pipeline_stopped_reason": pipeline_stopped_reason, "pipeline_failed_reason": pipeline_failed_reason, "replay_mode": False, }, ) _push_graph(courier_slot, paper_input, events) _refresh_overview_for_paper(paper_input, metrics_slot) def _load_result_bundle(paper_input: Optional[str] = None): """Load clusters/decomposition for the selected paper only (session cache).""" selected = paper_input if paper_input is not None else st.session_state.get("paper_input", "") entry = _paper_result_for_input(selected) if not entry: return None, None, [], None paper_dir_path = entry.get("paper_dir_path") annotation_payload_path = entry.get("annotation_payload_path") paper_dir = Path(paper_dir_path) if paper_dir_path else None payload = _load_json(Path(annotation_payload_path)) if annotation_payload_path else None discovery = ( _load_json(paper_dir / "usage_discovery_from_contributions.json") if paper_dir and paper_dir.exists() else None ) contributions_data = ( _load_json(paper_dir / "usage_contributions.json") if paper_dir and paper_dir.exists() else None ) contributions = (contributions_data or {}).get("contributions") or [] return paper_dir, discovery, contributions, payload def _overview_counts( payload: Optional[dict], discovery: Optional[dict] ) -> tuple[int, int, int, int]: claims = (payload or {}).get("claims") or [] ingredients = sum(len(claim.get("ingredients") or []) for claim in claims) studies = sum( len(_collect_grounded_studies(claim.get("enabling_discoveries") or [], claim.get("ingredients") or [])) for claim in claims ) clusters = len((discovery or {}).get("clusters") or []) return clusters, len(claims), ingredients, studies def _overview_html(payload: Optional[dict], discovery: Optional[dict]) -> str: clusters, n_claims, ingredients, studies = _overview_counts(payload, discovery) return ( "
    " f"{_metric_card('Refined clusters', clusters)}" f"{_metric_card('Target contributions', n_claims)}" f"{_metric_card('Enabling contributions', ingredients)}" f"{_metric_card('Grounded studies', studies)}" "
    " ) def _render_overview( payload: Optional[dict], discovery: Optional[dict], placeholder=None, ) -> None: html_body = _overview_html(payload, discovery) if placeholder is not None: placeholder.markdown(html_body, unsafe_allow_html=True) else: st.markdown(html_body, unsafe_allow_html=True) def _refresh_overview_for_paper(paper_input: str, placeholder) -> None: """Update metrics in-place after a run finishes (labels stay put; counts change).""" if placeholder is None: return _paper_dir, discovery, _contributions, payload = _load_result_bundle(paper_input) _render_overview(payload, discovery, placeholder=placeholder) def _build_public_export(discovery: Optional[dict], payload: Optional[dict]) -> dict: claims = [] for claim in (payload or {}).get("claims") or []: if not isinstance(claim, dict): continue ingredients = [] for ingredient in claim.get("ingredients") or []: if not isinstance(ingredient, dict): continue ingredients.append({ "ingredient_id": ingredient.get("ingredient_id"), "enabling_contribution": ingredient.get("ingredient"), "canonical_annotation": ingredient.get("canonical_annotation") or {}, "primary_grounding": ingredient.get("canonical_grounding") or {}, "additional_groundings": ingredient.get("additional_groundings") or [], }) claims.append({ "claim_id": claim.get("claim_id"), "target_contribution": claim.get("rewritten_claim") or claim.get("text"), "cluster_id": claim.get("cluster_id"), "decision": claim.get("decision"), "enabling_contributions": ingredients, "grounded_studies": _collect_grounded_studies(claim.get("enabling_discoveries") or [], claim.get("ingredients") or []), }) return { "citation_clusters": (discovery or {}).get("clusters") or [], "target_contribution_decompositions": claims, } def _escape(text: Any) -> str: return html.escape(str(text or ""), quote=True) def _format_analysis_bullet(text: Any) -> str: """Bold leading taxonomy tag; render as 'Tag:' instead of '[Tag]'.""" raw = str(text or "") match = re.match(r"^\[([^\]]+)\]\s*(.*)$", raw, flags=re.DOTALL) if not match: return _escape(raw) tag = match.group(1).strip() rest = match.group(2) label = f"{tag}:" if rest: return f"{_escape(label)} {_escape(rest)}" return f"{_escape(label)}" def _render_gold_item( item: dict, *, paired: bool = False, show_match_notes: bool = True, show_judge: bool = True ) -> str: badge = str(item.get("badge") or "miss") role = _escape(item.get("role") or "") desc = _escape(item.get("description") or "") notes = [] if show_match_notes: for note in item.get("match_notes") or []: cls = "system-match-note partial" if badge == "partial" else "system-match-note" notes.append(f"
    {_escape(note)}
    ") if show_judge: judge = item.get("judge_note") or "" if judge: notes.append(f"
    {_escape(judge)}
    ") paired_cls = " is-paired" if paired else "" return ( f"
    " "
    " f"{badge}" f"{role}" "
    " f"
    {desc}
    " f"{''.join(notes)}" "
    " ) def _render_pred_item(item: dict, *, paired: bool = False, show_rationale: bool = True) -> str: idx = item.get("idx") or "" role = _escape(item.get("role") or "") desc = _escape(item.get("description") or "") rationale = (item.get("rationale") or "") if show_rationale else "" rat_html = f"
    {_escape(rationale)}
    " if rationale else "" paired_cls = " is-paired" if paired else "" return ( f"
    " "
    " f"{_escape(idx)}" f"{role}" "
    " f"
    {desc}
    " f"{rat_html}" "
    " ) def _render_gold_column(items: list[dict]) -> str: rows = [_render_gold_item(item) for item in items] body = "".join(rows) if rows else "
    None
    " return f"

    Gold ingredients

    {body}
    " def _render_pred_column(items: list[dict]) -> str: rows = [_render_pred_item(item) for item in items] body = "".join(rows) if rows else "
    None
    " return f"

    SciFy Predictions

    {body}
    " def _render_paired_ingredient_grid(case: dict) -> str: """Success layout: gold left, predictions right, lines between matched pairs.""" gold = case.get("gold_ingredients") or [] predicted = case.get("predicted_ingredients") or [] rows = system_run_module.build_pair_rows(gold, predicted) body = [ "
    ", "

    Gold ingredients

    SciFy Predictions

    ", ] for row in rows: linked = bool(row.get("linked")) gold_item = row.get("gold") pred_item = row.get("pred") left = ( # Pair lines already show the match; keep judge/rationale text. _render_gold_item( gold_item, paired=linked, show_match_notes=False, show_judge=True ) if gold_item else "
    " ) right = ( _render_pred_item(pred_item, paired=linked, show_rationale=True) if pred_item else "
    " ) link_cls = "system-pair-link" if linked else "system-pair-link is-empty" body.append( f"
    {left}{right}
    " ) body.append("
    ") return "".join(body) def _render_tool_panel_html(panel: dict) -> str: kind = panel.get("kind") if kind == "retrieval": queries = "".join( f"
  • {_escape(q)}
  • " for q in (panel.get("queries") or []) ) docs = "".join( f"
  • {_escape(d)}
  • " for d in (panel.get("retrieved_docs") or []) ) return ( "
    " "

    Tool evidence used

    " f"

    {_escape(panel.get('intro') or '')}

    " "
    Example web search queries
    " f"" "
    Retrieved documents
    " "

    Documents returned for nested NER preprocessing / split construction:

    " f"" "
    " ) if kind == "deep": queries = "".join( f"
  • {_escape(q)}
  • " for q in (panel.get("queries") or []) ) hits = "".join(f"
  • {_escape(h)}
  • " for h in (panel.get("hits") or [])) deep_items = [] for item in panel.get("deep_fetches") or []: cls = "ok" if item.get("ok") else "bad" deep_items.append( f"
  • {_escape(item.get('url'))}" f"
    {_escape(item.get('outcome'))}
  • " ) return ( "
    " "

    Tool trace · web_search_tool

    " "
    Queries
    " f"" "
    Notable hits
    " f"" "

    Tool trace · deep_web_search_tool

    " f"" "
    " ) return "" def _ensure_claim_period(text: Any) -> str: claim = str(text or "").strip() if claim and claim[-1] not in ".!?": claim += "." return claim def _system_claim_header_html(case: dict) -> str: """Claim + paper outside the result card (Decomposition-style claim layout).""" paper_title = str(case.get("paper_title") or "").strip() paper_line = ( f'
    Paper: {_escape(paper_title)}
    ' if paper_title else "" ) return ( '
    ' '
    ' '
    Claim
    ' f'
    {_escape(_ensure_claim_period(case.get("claim")))}
    ' f"{paper_line}" "
    " "
    " ) def _system_case_card_html(case: dict) -> str: tone = str(case.get("outcome_tone") or "part") f1 = float(case.get("f1") or 0.0) recall = float(case.get("recall") or 0.0) precision = float(case.get("precision") or 0.0) case_key = str(case.get("case_key") or "") if case_key == "featured": pills = ( "
    " "Unevaluated" "
    " ) else: pills = ( "
    " f"F1 {f1:.2f}" f"Recall {recall:.2f}" f"Precision {precision:.2f}" "
    " ) # Success: gold | predictions with pair connector lines. # Failure: keep the original two-column layout (pairing UI deferred). # Featured / unevaluated live runs: predictions only. if case_key == "success": ingredients_html = _render_paired_ingredient_grid(case) elif case_key == "featured": pred_col = _render_pred_column(case.get("predicted_ingredients") or []) ingredients_html = f'
    {pred_col}
    ' else: gold_col = _render_gold_column(case.get("gold_ingredients") or []) pred_col = _render_pred_column(case.get("predicted_ingredients") or []) ingredients_html = ( f'
    {gold_col}{pred_col}
    ' ) tool_html = _render_tool_panel_html(case.get("tool_panel") or {}) return ( f'
    ' f'
    {_escape(case.get("case_chip"))}
    ' f"{pills}" f"{ingredients_html}" f"{tool_html}" "
    " ) def _system_analysis_panel_html( method_id: str, case_key: str, *, tone: str = "bad" ) -> str: view = system_run_module.analysis_view_for_case(method_id, case_key) blocks = view.get("blocks") or [] if not blocks: return ( "
    " "

    No analysis available for this case.

    " "
    " ) html_parts = [ "
    ", ] for block in blocks: btype = block.get("type") if btype == "diagnosis": text = str(block.get("text") or "") diag_cls = "system-diagnosis is-part" if tone == "part" else "system-diagnosis" if block.get("strong_first") and text.startswith("Diagnosis."): rest = text[len("Diagnosis.") :].lstrip() html_parts.append( f"
    Diagnosis. {_escape(rest)}
    " ) elif block.get("strong_first"): lines = text.split("\n", 1) if len(lines) == 2: html_parts.append( f"
    {_escape(lines[0])}" f"
    {_escape(lines[1])}
    " ) else: html_parts.append( f"
    {_escape(text)}
    " ) else: html_parts.append(f"
    {_escape(text)}
    ") elif btype == "two_col": cols = [] for col in block.get("cols") or []: items = "".join( f"
  • {_format_analysis_bullet(x)}
  • " for x in (col.get("items") or []) ) cols.append( "
    " f"

    {_escape(col.get('title'))}

    " f"" "
    " ) html_parts.append(f"
    {''.join(cols)}
    ") elif btype == "list": items = "".join( f"
  • {_format_analysis_bullet(x)}
  • " for x in (block.get("items") or []) ) html_parts.append(f"

    {_escape(block.get('title'))}

    ") html_parts.append("
    ") return "".join(html_parts) def _render_system_run_view(): method_id = str(st.session_state.get("selected_system_method") or "").strip() if method_id not in system_run_module.method_ids(): method_id = system_run_module.DEFAULT_SYSTEM_METHOD st.session_state["selected_system_method"] = method_id available_cases = system_run_module.list_cases(method_id) case_keys = {key for key, _label in available_cases} case_key = str(st.session_state.get("selected_system_case") or "").strip() if case_key not in case_keys: case_key = system_run_module.default_case_for_method(method_id) st.session_state["selected_system_case"] = case_key mode = str(st.session_state.get("system_run_mode") or "prerun").strip() if mode not in {"prerun", "live"}: mode = "prerun" st.session_state["system_run_mode"] = mode st.markdown( """
    SciFy System Run on SciPaths Claims
    SciFy CodeAgent
    """, unsafe_allow_html=True, ) st.markdown('
    Mode
    ', unsafe_allow_html=True) mode_cols = st.columns(2) with mode_cols[0]: st.button( "Pre-run Cases", key="system_mode::prerun", type="primary" if mode == "prerun" else "secondary", use_container_width=True, on_click=_on_pick_system_prerun, ) with mode_cols[1]: st.button( "Live Run", key="system_mode::live", type="primary" if mode == "live" else "secondary", use_container_width=True, on_click=_on_pick_system_live, ) if mode == "live": _render_system_live_run_view() return st.markdown('
    Setting
    ', unsafe_allow_html=True) method_items = system_run_module.SYSTEM_RUN_METHODS method_cols = st.columns(len(method_items)) for i, item in enumerate(method_items): with method_cols[i]: mid = item["id"] st.button( item["label"], key=f"system_method::{mid}", type="primary" if mid == method_id else "secondary", use_container_width=True, on_click=_on_pick_system_method, args=(mid,), ) st.markdown('
    Case
    ', unsafe_allow_html=True) case_cols = st.columns(max(len(available_cases), 1)) for i, (ckey, clabel) in enumerate(available_cases): with case_cols[i]: st.button( clabel, key=f"system_case::{method_id}::{ckey}", type="primary" if ckey == case_key else "secondary", use_container_width=True, on_click=_on_pick_system_case, args=(ckey,), ) try: case = system_run_module.get_case(method_id, case_key) except Exception as exc: st.error(f"Could not load case study: {exc}") return claim_html = _system_claim_header_html(case) result_html = _system_case_card_html(case) analysis_html = _system_analysis_panel_html( method_id, case_key, tone=str(case.get("outcome_tone") or "bad") ) st.markdown(claim_html, unsafe_allow_html=True) result_col, analysis_col = st.columns([1.25, 1], gap="large") with result_col: st.markdown( f'
    {result_html}
    ', unsafe_allow_html=True, ) with analysis_col: st.markdown(analysis_html, unsafe_allow_html=True) def _render_annotation_process_view( *, source_root: str, output_root: str, llm_provider: str, llm_model: str, llm_model_step4: str, formatter_model: str, judge_model: str, candidate_count: int, ): if not get_secret("GEMINI_API_KEY"): st.warning("GEMINI_API_KEY is missing for live runs. Demo examples still work without it.") # Pin demo selection so post-run reruns cannot jump to another paper. if not st.session_state.get("live_run_mode"): demo = str(st.session_state.get("selected_demo_url") or "").strip() if not demo: _select_paper_1() else: # Re-assert the locked demo before the picker / text input mounts. st.session_state["_pending_paper_input"] = demo _apply_pending_paper_input() current_paper = st.session_state.get("paper_input", "") selected_demo = str(st.session_state.get("selected_demo_url") or "").strip() selected_demo_id = replay_module.parse_arxiv_id(selected_demo) st.markdown( """
    Annotation Process
    Forecasting pathways to scientific discovery
    """, unsafe_allow_html=True, ) live_run_mode = bool(st.session_state.get("live_run_mode")) st.markdown('
    Paper
    ', unsafe_allow_html=True) example_items = list(EXAMPLES.items()) picker_count = len(example_items) + 1 # demos + Live run cols = st.columns(picker_count) for i, (button_label, value) in enumerate(example_items): with cols[i]: example_id = replay_module.parse_arxiv_id(value) is_selected = (not live_run_mode) and bool( selected_demo_id and example_id == selected_demo_id ) st.button( button_label, key=f"example::{button_label}", type="primary" if is_selected else "secondary", use_container_width=True, on_click=_on_pick_demo, args=(value,), ) with cols[-1]: st.button( "Live Run", key="example::live_run", icon=":material/edit_note:", type="primary" if live_run_mode else "secondary", use_container_width=True, on_click=_on_pick_live_run, ) tabs = st.tabs(TAB_NAMES) with tabs[0]: if live_run_mode: st.caption("Enter any arXiv paper and run the live pipeline.") paper_input = st.text_input( "arXiv URL or ID", key="paper_input", placeholder="https://arxiv.org/abs/2311.14919", ) else: paper_input = selected_demo or current_paper if not paper_input.strip(): st.caption("Select a demo paper above, or choose Live Run to enter a custom arXiv ID.") paper_result = _paper_result_for_input(paper_input) if paper_input.strip() else None status = (paper_result or {}).get("run_status") or "Idle" events = list((paper_result or {}).get("run_events") or []) work_panel = None graph_slot = None courier_slot = None run_clicked = False if paper_input.strip(): left_col, right_col = st.columns([1.55, 1], gap="large") with left_col: _render_paper_preview(paper_input) with right_col: work_panel = st.empty() _render_work_panel(work_panel, events, status=status) run_clicked = st.button( "Run pipeline", type="primary", use_container_width=True, key="run_pipeline_btn", ) # Persistent graph canvas (mounted once) + invisible courier that # streams incremental model updates into it without re-emitting it. graph_slot = st.empty() courier_slot = st.empty() _mount_graph_shell(graph_slot) _push_graph(courier_slot, paper_input, events) if paper_result and paper_result.get("pipeline_failed_reason"): st.error(paper_result["pipeline_failed_reason"]) if paper_result and paper_result.get("annotation_skipped_reason"): st.warning(paper_result["annotation_skipped_reason"]) if ( paper_result and paper_result.get("pipeline_stopped_reason") and not paper_result.get("pipeline_failed_reason") ): st.warning(paper_result["pipeline_stopped_reason"]) # Stable slot so only metric values change when a run finishes (not a full remount flash). metrics_slot = st.empty() paper_dir, discovery, contributions, payload = _load_result_bundle(paper_input) _render_overview(payload, discovery, placeholder=metrics_slot) if run_clicked: if not paper_input.strip(): _clear_run_state() st.session_state["pipeline_failed_reason"] = "Paper input is required." st.session_state["run_status"] = "Failed" st.rerun() # Demo papers use saved traces; Live run always executes the live pipeline. used_trace = ( False if live_run_mode else run_replay_stream( paper_input, work_panel=work_panel, metrics_slot=metrics_slot, graph_slot=graph_slot, courier_slot=courier_slot, ) ) if not used_trace: _clear_run_state() st.session_state["run_status"] = "Starting" st.session_state["replay_mode"] = False run_pipeline_stream( paper_input=paper_input, source_root=source_root, output_root=output_root, llm_provider=llm_provider, llm_model=llm_model, llm_model_step4=llm_model_step4, formatter_model=formatter_model, judge_model=judge_model, candidate_count=candidate_count, work_panel=work_panel, metrics_slot=metrics_slot, graph_slot=graph_slot, courier_slot=courier_slot, ) # Keep the same demo selected after finish; do not advance to Paper 2. # No st.rerun() here: the stream already painted the final work panel, # overview metrics, and graph via placeholders/courier. A full rerun # remounts the page (and graph iframe) and looks like a refresh. if not live_run_mode and paper_input.strip(): st.session_state["live_run_mode"] = False st.session_state["selected_demo_url"] = paper_input.strip() with tabs[1]: paper_dir, discovery, contributions, payload = _load_result_bundle( st.session_state.get("paper_input", "") or st.session_state.get("selected_demo_url", "") ) _render_clusters_tab(discovery, contributions) with tabs[2]: paper_dir, discovery, contributions, payload = _load_result_bundle( st.session_state.get("paper_input", "") or st.session_state.get("selected_demo_url", "") ) _render_claims_tab(payload) def main(): llm_provider = os.getenv("LLM_PROVIDER", "gemini") llm_model = os.getenv("LLM_MODEL", "gemini-3.1-pro-preview") llm_model_step4 = os.getenv("LLM_MODEL_STEP4", "gemini-3-flash-preview") formatter_model = os.getenv("ANNOTATION_FORMATTER_MODEL", "gemini/gemini-3.1-pro-preview") judge_model = os.getenv("ANNOTATION_JUDGE_MODEL", "gemini/gemini-3.1-pro-preview") candidate_count = int(os.getenv("ANNOTATION_CANDIDATE_COUNT", "3")) source_root = DEFAULT_SOURCE_ROOT output_root = DEFAULT_OUTPUT_ROOT st.set_page_config( page_title="SciPaths", page_icon="🔬", layout="wide", initial_sidebar_state="collapsed", ) st.markdown(CUSTOM_CSS, unsafe_allow_html=True) _ensure_state() if st.button("Reset session", type="tertiary", key="reset_session_btn"): _reset_all_session_state() st.rerun() nav_col, main_col = st.columns([1, 5.5], gap="medium") with nav_col: current = st.session_state.get("app_view", DEFAULT_APP_VIEW) if current not in APP_VIEWS: current = DEFAULT_APP_VIEW st.session_state["app_view"] = current st.markdown( """ """, unsafe_allow_html=True, ) for item in APP_NAV: view = item["view"] icon = item.get("icon") or "" is_active = view == current if st.button( view, key=f"nav::{view}", icon=icon, type="primary" if is_active else "secondary", use_container_width=True, ): if view != current: st.session_state["app_view"] = view if view == "Annotation Process": _select_paper_1() st.rerun() selected = st.session_state.get("app_view", DEFAULT_APP_VIEW) with main_col: if selected == "System Run": _render_system_run_view() else: _render_annotation_process_view( source_root=source_root, output_root=output_root, llm_provider=llm_provider, llm_model=llm_model, llm_model_step4=llm_model_step4, formatter_model=formatter_model, judge_model=judge_model, candidate_count=candidate_count, ) if __name__ == "__main__": main()