wandler67 commited on
Commit
704baa5
·
verified ·
1 Parent(s): 6da2b63

Deploy H2EPR-Bench Explorer Space

Browse files
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+ ENV PIP_NO_CACHE_DIR=1
6
+
7
+ WORKDIR /app
8
+
9
+ COPY requirements.txt /app/requirements.txt
10
+ RUN pip install --upgrade pip && pip install -r /app/requirements.txt
11
+
12
+ COPY . /app
13
+
14
+ EXPOSE 7860
15
+
16
+ CMD streamlit run app.py --server.port=7860 --server.address=0.0.0.0
README.md CHANGED
@@ -1,10 +1,32 @@
1
  ---
2
- title: H2EPR Bench Explorer
3
- emoji: 👀
4
  colorFrom: gray
5
- colorTo: indigo
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: H2EPR-Bench Explorer
 
3
  colorFrom: gray
4
+ colorTo: blue
5
  sdk: docker
6
  pinned: false
7
+ license: cc-by-nc-4.0
8
  ---
9
 
10
+ # H²EPR-Bench Explorer
11
+
12
+ H²EPR-Bench Explorer is the planned interactive browsing layer for `AgenticFinLab/H2EPR-Bench`. It is separate from the canonical dataset repository: the dataset repo remains the release package, while this Docker Space runs a Streamlit app for search, event detail, stage inspection, public FinalCascade JSON browsing, and Gantt-style timelines.
13
+
14
+ **Release boundary:** public records are intended for browsing, reuse, and presentation. Official scoring uses the [manual-gated Gold companion](https://huggingface.co/datasets/AgenticFinLab/H2EPR-Bench-Gold). Public FinalCascade and Gantt views are supplementary inspection assets, not official scoring references.
15
+
16
+ ## Data Sources
17
+
18
+ | Source | Role |
19
+ |---|---|
20
+ | `AgenticFinLab/H2EPR-Bench` | Public event catalog, stage table, public sanitized FinalCascade, and Gantt artifact paths. |
21
+ | `AgenticFinLab/H2EPR-Bench-Gold` | Manual-gated Gold companion for official scoring references. Linked for users who need scoring access; not loaded by this app. |
22
+
23
+ ## Local Development
24
+
25
+ The app can run from a local staged dataset package before upload:
26
+
27
+ ```bash
28
+ export H2EPR_EXPLORER_LOCAL_DATASET_DIR=../../build/hf_dataset_repo_staging/eventmycelium-v1_1000-public
29
+ streamlit run app.py
30
+ ```
31
+
32
+ Without `H2EPR_EXPLORER_LOCAL_DATASET_DIR`, the app downloads public files from the Hugging Face dataset repo.
app.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ APP_ROOT = Path(__file__).resolve().parent
8
+ sys.path.insert(0, str(APP_ROOT / "src"))
9
+
10
+ import streamlit as st
11
+
12
+ from h2epr_explorer.constants import (
13
+ CATALOG_COLUMNS,
14
+ GOLD_COMPANION_REPO,
15
+ PUBLIC_DATASET_REPO,
16
+ RELEASE_BOUNDARY_NOTICE,
17
+ )
18
+ from h2epr_explorer.data_loader import load_catalog, load_event_graph, load_finalcascade_summary, load_stages
19
+ from h2epr_explorer.filters import filter_catalog
20
+ from h2epr_explorer.render_gantt import build_timeline_figure
21
+
22
+
23
+ def _as_records(frame):
24
+ return frame.to_dict(orient="records")
25
+
26
+
27
+ def _select_columns(frame, columns):
28
+ present = [column for column in columns if column in frame.columns]
29
+ return frame[present] if present else frame
30
+
31
+
32
+ st.set_page_config(page_title="H2EPR-Bench Explorer", layout="wide")
33
+
34
+ st.title("H²EPR-Bench Explorer")
35
+ st.caption("Interactive browser for public event metadata, stage rows, FinalCascade summaries, and Gantt-style views.")
36
+ st.info(RELEASE_BOUNDARY_NOTICE)
37
+
38
+ catalog = load_catalog()
39
+ stages = load_stages()
40
+ summary = load_finalcascade_summary()
41
+
42
+ catalog_rows = _as_records(catalog)
43
+
44
+ with st.sidebar:
45
+ st.header("Filter events")
46
+ query = st.text_input("Search", placeholder="event name, ID, category, keyword")
47
+ domains = st.multiselect("Domain", sorted(catalog["domain"].dropna().unique().tolist()))
48
+ categories = st.multiselect("Category", sorted(catalog["event_category"].dropna().unique().tolist()))
49
+ min_source_count = st.slider("Minimum sources", 0, int(catalog["source_count"].max()), 0)
50
+ min_stage_count = st.slider("Minimum stages", 0, int(catalog["stage_count"].max()), 0)
51
+
52
+ filtered_rows = filter_catalog(
53
+ catalog_rows,
54
+ query=query,
55
+ domains=domains,
56
+ categories=categories,
57
+ min_source_count=min_source_count,
58
+ min_stage_count=min_stage_count,
59
+ )
60
+
61
+ if not filtered_rows:
62
+ st.warning("No event matches the current filters.")
63
+ st.stop()
64
+
65
+ selected_event = st.selectbox(
66
+ "Selected event",
67
+ [row["event_id"] for row in filtered_rows],
68
+ format_func=lambda event_id: f"{event_id} · {catalog.loc[catalog['event_id'] == event_id, 'event_name'].iloc[0]}",
69
+ )
70
+
71
+ event_row = catalog[catalog["event_id"] == selected_event].iloc[0]
72
+ event_stages = stages[stages["event_id"] == selected_event].sort_values("stage_order")
73
+ summary_row = summary[summary["event_id"] == selected_event]
74
+
75
+ tabs = st.tabs(["Catalog", "Event detail", "Timeline", "Stages", "FinalCascade JSON", "Release boundary"])
76
+
77
+ with tabs[0]:
78
+ st.subheader(f"Event catalog: {len(filtered_rows)} of {len(catalog_rows)} events")
79
+ st.dataframe(_select_columns(catalog[catalog["event_id"].isin([row["event_id"] for row in filtered_rows])], CATALOG_COLUMNS), use_container_width=True, height=520)
80
+
81
+ with tabs[1]:
82
+ st.subheader(str(event_row.get("event_name", selected_event)))
83
+ st.write(str(event_row.get("short_description", "")))
84
+ c1, c2, c3, c4 = st.columns(4)
85
+ c1.metric("Domain", str(event_row.get("domain", "")))
86
+ c2.metric("Category", str(event_row.get("event_category", "")))
87
+ c3.metric("Sources", int(event_row.get("source_count", 0)))
88
+ c4.metric("Stages", int(event_row.get("stage_count", 0)))
89
+ if not summary_row.empty:
90
+ st.markdown("#### Public FinalCascade summary")
91
+ st.dataframe(summary_row, use_container_width=True)
92
+
93
+ with tabs[2]:
94
+ figure = build_timeline_figure(_as_records(event_stages), selected_event)
95
+ if figure is None:
96
+ st.warning("No public stage rows are available for this event.")
97
+ else:
98
+ st.plotly_chart(figure, use_container_width=True)
99
+ if "gantt_html_path" in event_row and event_row.get("gantt_html_path"):
100
+ st.markdown(f"Gantt HTML artifact path: `{event_row.get('gantt_html_path')}`")
101
+
102
+ with tabs[3]:
103
+ st.dataframe(event_stages, use_container_width=True, height=520)
104
+
105
+ with tabs[4]:
106
+ graph = load_event_graph(selected_event)
107
+ st.download_button(
108
+ "Download selected public FinalCascade JSON",
109
+ data=json.dumps(graph, ensure_ascii=False, indent=2),
110
+ file_name=f"{selected_event}_finalcascade_public.json",
111
+ mime="application/json",
112
+ )
113
+ st.json(graph, expanded=False)
114
+
115
+ with tabs[5]:
116
+ st.markdown(
117
+ f"""
118
+ ### Release boundary
119
+
120
+ - Public dataset repo: [`{PUBLIC_DATASET_REPO}`](https://huggingface.co/datasets/{PUBLIC_DATASET_REPO})
121
+ - Manual-gated Gold companion: [`{GOLD_COMPANION_REPO}`](https://huggingface.co/datasets/{GOLD_COMPANION_REPO})
122
+ - This Explorer loads public event metadata, public stages, public sanitized FinalCascade records, and public visualization paths.
123
+ - It does not load gated Gold references.
124
+ - Public FinalCascade and Gantt views are supplementary inspection assets, not official scoring references.
125
+ """
126
+ )
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ streamlit>=1.36
2
+ pandas>=2.0
3
+ pyarrow>=14.0
4
+ plotly>=5.20
5
+ huggingface_hub>=0.23
src/h2epr_explorer/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Support modules for the H2EPR-Bench Explorer Space."""
2
+
src/h2epr_explorer/constants.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PUBLIC_DATASET_REPO = "AgenticFinLab/H2EPR-Bench"
2
+ GOLD_COMPANION_REPO = "AgenticFinLab/H2EPR-Bench-Gold"
3
+ LOCAL_DATASET_ENV = "H2EPR_EXPLORER_LOCAL_DATASET_DIR"
4
+
5
+ CATALOG_PARQUET = "data/viewer_mirrors/event_catalog.parquet"
6
+ CATALOG_JSONL = "data/event_catalog.jsonl"
7
+ STAGES_PARQUET = "data/viewer_mirrors/event_stages.parquet"
8
+ STAGES_JSONL = "data/event_stages.jsonl"
9
+ FINALCASCADE_JSONL = "data/finmycelium_finalcascade_public.jsonl"
10
+ FINALCASCADE_SUMMARY_PARQUET = "data/viewer_mirrors/finalcascade_summary.parquet"
11
+
12
+ RELEASE_BOUNDARY_NOTICE = (
13
+ "Official scoring uses the manual-gated Gold companion repository. "
14
+ "Public FinalCascade and Gantt views are supplementary inspection assets, "
15
+ "not official scoring references."
16
+ )
17
+
18
+ CATALOG_COLUMNS = [
19
+ "event_id",
20
+ "event_name",
21
+ "domain",
22
+ "event_category",
23
+ "short_description",
24
+ "keywords",
25
+ "source_count",
26
+ "stage_count",
27
+ "gantt_html_path",
28
+ ]
29
+
src/h2epr_explorer/data_loader.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .constants import (
10
+ CATALOG_JSONL,
11
+ CATALOG_PARQUET,
12
+ FINALCASCADE_JSONL,
13
+ FINALCASCADE_SUMMARY_PARQUET,
14
+ LOCAL_DATASET_ENV,
15
+ PUBLIC_DATASET_REPO,
16
+ STAGES_JSONL,
17
+ STAGES_PARQUET,
18
+ )
19
+
20
+
21
+ class SimpleTable:
22
+ """Small fallback table used when pandas is unavailable in local checks."""
23
+
24
+ def __init__(self, rows: list[dict[str, Any]]):
25
+ self._rows = rows
26
+
27
+ def __len__(self) -> int:
28
+ return len(self._rows)
29
+
30
+ def to_dict(self, orient: str = "records") -> list[dict[str, Any]]:
31
+ if orient != "records":
32
+ raise ValueError("SimpleTable only supports orient='records'")
33
+ return list(self._rows)
34
+
35
+
36
+ def _as_local_root(local_dataset_dir: Path | str | None = None) -> Path | None:
37
+ value = local_dataset_dir or os.environ.get(LOCAL_DATASET_ENV)
38
+ if not value:
39
+ return None
40
+ return Path(value).expanduser().resolve()
41
+
42
+
43
+ def resolve_dataset_file(filename: str, local_dataset_dir: Path | str | None = None) -> Path:
44
+ local_root = _as_local_root(local_dataset_dir)
45
+ if local_root is not None:
46
+ path = (local_root / filename).resolve()
47
+ if not path.is_relative_to(local_root):
48
+ raise ValueError(f"Refusing to read outside local dataset root: {filename}")
49
+ if not path.exists():
50
+ raise FileNotFoundError(path)
51
+ return path
52
+
53
+ from huggingface_hub import hf_hub_download
54
+
55
+ return Path(
56
+ hf_hub_download(
57
+ repo_id=PUBLIC_DATASET_REPO,
58
+ repo_type="dataset",
59
+ filename=filename,
60
+ )
61
+ )
62
+
63
+
64
+ def load_jsonl_rows(filename: str, local_dataset_dir: Path | str | None = None) -> list[dict[str, Any]]:
65
+ path = resolve_dataset_file(filename, local_dataset_dir=local_dataset_dir)
66
+ rows: list[dict[str, Any]] = []
67
+ with path.open("r", encoding="utf-8") as handle:
68
+ for line in handle:
69
+ line = line.strip()
70
+ if line:
71
+ rows.append(json.loads(line))
72
+ return rows
73
+
74
+
75
+ def read_event_graph_from_jsonl(
76
+ event_id: str, local_dataset_dir: Path | str | None = None
77
+ ) -> dict[str, Any]:
78
+ path = resolve_dataset_file(FINALCASCADE_JSONL, local_dataset_dir=local_dataset_dir)
79
+ with path.open("r", encoding="utf-8") as handle:
80
+ for line in handle:
81
+ if not line.strip():
82
+ continue
83
+ row = json.loads(line)
84
+ if row.get("event_id") == event_id:
85
+ return row
86
+ raise KeyError(f"Event graph not found: {event_id}")
87
+
88
+
89
+ def _read_table(filename: str, fallback_jsonl: str, local_dataset_dir: Path | str | None = None):
90
+ pd = None
91
+ try:
92
+ import pandas as pandas_module
93
+
94
+ pd = pandas_module
95
+ except ImportError:
96
+ pass
97
+ try:
98
+ path = resolve_dataset_file(filename, local_dataset_dir=local_dataset_dir)
99
+ if pd is None:
100
+ raise ImportError("pandas is unavailable")
101
+ return pd.read_parquet(path)
102
+ except (FileNotFoundError, ImportError, ValueError):
103
+ rows = load_jsonl_rows(fallback_jsonl, local_dataset_dir=local_dataset_dir)
104
+ if pd is None:
105
+ return SimpleTable(rows)
106
+ return pd.DataFrame(rows)
107
+
108
+
109
+ @lru_cache(maxsize=1)
110
+ def load_catalog():
111
+ return _read_table(CATALOG_PARQUET, CATALOG_JSONL)
112
+
113
+
114
+ @lru_cache(maxsize=1)
115
+ def load_stages():
116
+ return _read_table(STAGES_PARQUET, STAGES_JSONL)
117
+
118
+
119
+ @lru_cache(maxsize=1)
120
+ def load_finalcascade_summary():
121
+ return _read_table(FINALCASCADE_SUMMARY_PARQUET, CATALOG_JSONL)
122
+
123
+
124
+ @lru_cache(maxsize=128)
125
+ def load_event_graph(event_id: str) -> dict[str, Any]:
126
+ return read_event_graph_from_jsonl(event_id)
src/h2epr_explorer/filters.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterable
4
+
5
+
6
+ SEARCH_FIELDS = ("event_id", "event_name", "short_description", "domain", "event_category", "keywords")
7
+
8
+
9
+ def _contains_query(row: dict[str, Any], query: str) -> bool:
10
+ if not query:
11
+ return True
12
+ needle = query.casefold()
13
+ return any(needle in str(row.get(field, "")).casefold() for field in SEARCH_FIELDS)
14
+
15
+
16
+ def filter_catalog(
17
+ rows: Iterable[dict[str, Any]],
18
+ *,
19
+ query: str = "",
20
+ domains: list[str] | None = None,
21
+ categories: list[str] | None = None,
22
+ min_source_count: int = 0,
23
+ min_stage_count: int = 0,
24
+ ) -> list[dict[str, Any]]:
25
+ domain_set = set(domains or [])
26
+ category_set = set(categories or [])
27
+ filtered: list[dict[str, Any]] = []
28
+ for row in rows:
29
+ if domain_set and row.get("domain") not in domain_set:
30
+ continue
31
+ if category_set and row.get("event_category") not in category_set:
32
+ continue
33
+ if int(row.get("source_count") or 0) < min_source_count:
34
+ continue
35
+ if int(row.get("stage_count") or 0) < min_stage_count:
36
+ continue
37
+ if not _contains_query(row, query):
38
+ continue
39
+ filtered.append(row)
40
+ return filtered
41
+
src/h2epr_explorer/render_gantt.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def _is_known_time(value: Any) -> bool:
7
+ return bool(value) and str(value).strip().lower() not in {"unknown", "none", "nan", "nat"}
8
+
9
+
10
+ def prepare_gantt_rows(stage_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
11
+ ordered = sorted(stage_rows, key=lambda row: (int(row.get("stage_order") or 0), str(row.get("stage_id", ""))))
12
+ prepared: list[dict[str, Any]] = []
13
+ for fallback_index, row in enumerate(ordered, start=1):
14
+ start = row.get("stage_start_time")
15
+ end = row.get("stage_end_time")
16
+ if _is_known_time(start) and _is_known_time(end):
17
+ display_start = start
18
+ display_end = end
19
+ axis_mode = "calendar"
20
+ else:
21
+ display_start = int(row.get("stage_order") or fallback_index)
22
+ display_end = display_start + 0.85
23
+ axis_mode = "relative_order"
24
+
25
+ time_note = row.get("temporal_anchor_summary") or ""
26
+ if not time_note and int(row.get("known_action_time_anchor_count") or 0) > 0:
27
+ time_note = "Action-level time anchors available"
28
+
29
+ prepared.append(
30
+ {
31
+ **row,
32
+ "display_start": display_start,
33
+ "display_end": display_end,
34
+ "axis_mode": axis_mode,
35
+ "time_note": time_note,
36
+ }
37
+ )
38
+ return prepared
39
+
40
+
41
+ def build_timeline_figure(stage_rows: list[dict[str, Any]], event_id: str):
42
+ import pandas as pd
43
+ import plotly.express as px
44
+
45
+ prepared = prepare_gantt_rows(stage_rows)
46
+ if not prepared:
47
+ return None
48
+
49
+ frame = pd.DataFrame(prepared)
50
+ if set(frame["axis_mode"]) == {"calendar"}:
51
+ fig = px.timeline(
52
+ frame,
53
+ x_start="display_start",
54
+ x_end="display_end",
55
+ y="stage_label_public",
56
+ color="stage_label_public",
57
+ hover_data=["stage_id", "stage_order", "time_note"],
58
+ title=f"{event_id}: public stage timeline",
59
+ )
60
+ fig.update_yaxes(autorange="reversed")
61
+ return fig
62
+
63
+ fig = px.bar(
64
+ frame,
65
+ x=[row["display_end"] - row["display_start"] for row in prepared],
66
+ y="stage_label_public",
67
+ base="display_start",
68
+ orientation="h",
69
+ color="stage_label_public",
70
+ hover_data=["stage_id", "stage_order", "time_note"],
71
+ title=f"{event_id}: relative stage order",
72
+ )
73
+ fig.update_yaxes(autorange="reversed")
74
+ fig.update_layout(xaxis_title="Relative stage order")
75
+ return fig
76
+