From cc2b0a80b1a270053542e418cca4d54b2e17fd5d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 17:38:08 +0000 Subject: [PATCH 1/2] Add live dashboard badge and link to README The results dashboard is deployed to Streamlit Community Cloud at https://docuparse.streamlit.app. Add an "Open in Streamlit" badge and live link at the top of the README and in the Results Dashboard section so it is easy to find. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01D68SAeZ9GofWeNdMwvuDwC --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 19bdeaa..89c4a93 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # DocuParse +[![Open in Streamlit](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://docuparse.streamlit.app) + **Intelligent document parser for financial filings** - Extracts text, tables, and validates data from SEC 10-K/10-Q documents using multiple AI models and cross-verification. +๐Ÿ”— **Live results dashboard:** [docuparse.streamlit.app](https://docuparse.streamlit.app) + ## What This Does DocuParse is a complete pipeline that: @@ -40,6 +44,10 @@ ls data/exports/ ## ๐Ÿ“Š Results Dashboard +[![Open in Streamlit](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://docuparse.streamlit.app) + +**Live app:** [docuparse.streamlit.app](https://docuparse.streamlit.app) + An interactive Streamlit dashboard visualizes the pipeline's recorded outputs โ€” evaluation metrics, per-stage benchmarks (runtime & memory), build-vs-buy cost analysis, distribution drift, and the analysis reports. It reads the committed From e6e03d46e9e04c90aa0caac4b044221e39c670c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 20:52:49 +0000 Subject: [PATCH 2/2] Add provenance banner and About & Skills page to dashboard Make the dashboard self-explanatory and honest about what it shows: - Provenance banner on every page: states the figures are a static snapshot from the recorded pipeline run (date derived dynamically from the result files) and that the app does not run the pipeline live. - New "About & Skills" landing page (now the default view) describing what the project does, a curated shortlist of the most valuable concepts behind it, the headline tech, and links to the repo, demo video, and tutorial. - data_loader: add data_as_of()/_all_timestamps() to derive the snapshot date; make _latest() honor its root argument so the glob-based loaders are testable. Verified: 28 unit tests pass; all six pages render exception-free via AppTest with the provenance banner present on each. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01D68SAeZ9GofWeNdMwvuDwC --- dashboard/app.py | 63 ++++++++++++++++++++++++++++++- dashboard/data_loader.py | 44 ++++++++++++++++++--- tests/unit/test_dashboard_data.py | 24 ++++++++++++ 3 files changed, 125 insertions(+), 6 deletions(-) diff --git a/dashboard/app.py b/dashboard/app.py index 1505bbe..3395ee9 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -31,6 +31,16 @@ def _fmt(value, digits: int = 4) -> str: return "โ€”" if value is None else f"{value:.{digits}f}" +def render_provenance() -> None: + """A banner shown on every page so viewers know the data is a static snapshot.""" + when = dl.data_as_of() or "a previous run" + st.info( + f"๐Ÿ“Œ **Static snapshot โ€” figures are from the pipeline run on {when}.** " + "This dashboard reads committed result files; it does **not** run the " + "extraction pipeline live." + ) + + # --------------------------------------------------------------------------- # # Sidebar # --------------------------------------------------------------------------- # @@ -41,7 +51,8 @@ def _fmt(value, digits: int = 4) -> str: ) page = st.sidebar.radio( "View", - ["Overview", "Benchmarks", "Cost (Build vs Buy)", "Distribution Drift", "Reports"], + ["About & Skills", "Overview", "Benchmarks", "Cost (Build vs Buy)", + "Distribution Drift", "Reports"], ) st.sidebar.info( "This dashboard visualizes recorded runs. A live 'upload a PDF and parse' " @@ -224,11 +235,61 @@ def render_reports() -> None: st.markdown(dl.read_report(reports[choice])) +# --------------------------------------------------------------------------- # +# About & Skills โ€” the landing page +# --------------------------------------------------------------------------- # +# A curated shortlist of the most valuable concepts behind the project โ€” not an +# exhaustive catalogue of every library used. +KEY_SKILLS = [ + ("Document AI & layout understanding", + "Parsing complex financial PDFs with OCR fallback and layout/table models " + "(Docling, LayoutParser, Tesseract, Camelot)."), + ("Reproducible ML pipelines (MLOps)", + "A staged, parameterized DVC pipeline from download through export."), + ("Quantitative evaluation", + "Text WER/CER and table precision/recall/F1, with regression and " + "distribution-drift monitoring."), + ("Data validation", + "Cross-verifying extracted figures against authoritative SEC XBRL data."), + ("Performance & cost engineering", + "Per-stage runtime/memory benchmarking and a build-vs-buy cost analysis."), +] + + +def render_about() -> None: + st.title("๐Ÿ“„ DocuParse โ€” Financial Filing Parser") + st.markdown( + "An end-to-end pipeline that extracts text, tables, and structure from " + "**SEC financial filings (10-K / 10-Q)**, measures the extraction quality, " + "and cross-checks the numbers against authoritative **XBRL** data. " + "The tabs on the left present the pipeline's recorded results." + ) + + st.subheader("๐Ÿง  Key concepts & skills") + for title, desc in KEY_SKILLS: + st.markdown(f"- **{title}** โ€” {desc}") + + st.subheader("๐Ÿ› ๏ธ Built with") + st.markdown("`Python` ยท `DVC` ยท `Docling` ยท `pandas` ยท `Streamlit`") + + st.subheader("๐Ÿ”— Links") + st.markdown( + "- **Source code:** https://github.com/Effyrt/Docuparse\n" + "- **Demo video:** " + "https://drive.google.com/file/d/1w8RPBch1nPV8BpZIw0tFLPD1BmK0rkfN/view\n" + "- **Interactive tutorial (CodeLabs):** " + "https://codelabs-preview.appspot.com/?file_id=1eoeyKHeNX_qYq6m8oL37XLQMEoLCK7Xv02sBSGAGbwg#0" + ) + + PAGES = { + "About & Skills": render_about, "Overview": render_overview, "Benchmarks": render_benchmarks, "Cost (Build vs Buy)": render_cost, "Distribution Drift": render_drift, "Reports": render_reports, } + +render_provenance() PAGES[page]() diff --git a/dashboard/data_loader.py b/dashboard/data_loader.py index c7ae3a2..674ce3a 100644 --- a/dashboard/data_loader.py +++ b/dashboard/data_loader.py @@ -31,9 +31,9 @@ def _read_json(path: Path) -> Optional[Any]: return None -def _latest(pattern: str) -> Optional[Path]: - """Return the most recently modified file matching a glob under the repo.""" - matches = glob.glob(str(REPO_ROOT / pattern)) +def _latest(pattern: str, root: Path = REPO_ROOT) -> Optional[Path]: + """Return the most recently modified file matching a glob under ``root``.""" + matches = glob.glob(str(root / pattern)) if not matches: return None return Path(max(matches, key=os.path.getmtime)) @@ -55,6 +55,40 @@ def load_metrics_history(root: Path = REPO_ROOT) -> List[Dict[str, Any]]: return sorted(data, key=lambda r: r.get("timestamp", "")) +def _all_timestamps(root: Path = REPO_ROOT) -> List[str]: + """Collect ISO timestamps from every result file that records one.""" + stamps: List[str] = [] + + bench = load_benchmark(root).get("benchmark_info", {}) or {} + stamps.append(bench.get("timestamp")) + + stamps.append((load_drift(root) or {}).get("timestamp")) + + history = load_metrics_history(root) + if history: + stamps.append(history[-1].get("timestamp")) + + summary = _read_json(root / "evaluation" / "latest_evaluation_summary.json") or {} + stamps.append(summary.get("evaluation_timestamp")) + + cost_info = (load_cost_analysis(root).get("analysis_info", {}) or {}) + stamps.append(cost_info.get("timestamp")) + + return [s for s in stamps if s] + + +def data_as_of(root: Path = REPO_ROOT) -> Optional[str]: + """Return the date (YYYY-MM-DD) of the most recent recorded pipeline run. + + Used to tell viewers the dashboard shows a static snapshot, not live data. + ISO-8601 strings sort lexicographically, so max() gives the latest. + """ + stamps = _all_timestamps(root) + if not stamps: + return None + return max(stamps)[:10] + + def metric_cards(metrics: Dict[str, Any]) -> List[Dict[str, Any]]: """Shape the headline metrics into display cards with pass/fail vs thresholds. @@ -115,7 +149,7 @@ def metric_cards(metrics: Dict[str, Any]) -> List[Dict[str, Any]]: # --------------------------------------------------------------------------- # def load_benchmark(root: Path = REPO_ROOT) -> Dict[str, Any]: """Load the most recent corrected pipeline benchmark.""" - path = _latest("benchmarks/results/CORRECTED_pipeline_benchmark_*.json") + path = _latest("benchmarks/results/CORRECTED_pipeline_benchmark_*.json", root) return _read_json(path) if path else {} @@ -152,7 +186,7 @@ def benchmark_failures(benchmark: Dict[str, Any]) -> List[Dict[str, Any]]: # --------------------------------------------------------------------------- # def load_cost_analysis(root: Path = REPO_ROOT) -> Dict[str, Any]: """Load the most recent cloud vs infrastructure cost analysis.""" - path = _latest("benchmarks/results/cost_analysis_*.json") + path = _latest("benchmarks/results/cost_analysis_*.json", root) return _read_json(path) if path else {} diff --git a/tests/unit/test_dashboard_data.py b/tests/unit/test_dashboard_data.py index cdfcdb3..95a272a 100644 --- a/tests/unit/test_dashboard_data.py +++ b/tests/unit/test_dashboard_data.py @@ -99,3 +99,27 @@ def test_real_reports_listed(): names = {p.name for p in dl.list_reports()} assert "benchmarks.md" in names assert "xbrl_cross_verification_report.md" in names + + +# --- snapshot-date provenance ----------------------------------------------- # +def test_data_as_of_none_for_empty_root(tmp_path): + assert dl.data_as_of(tmp_path) is None + + +def test_data_as_of_picks_latest_date(tmp_path): + # Only a metrics history file exists; data_as_of should return its latest date. + hist_dir = tmp_path / "evaluation" / "metrics" + hist_dir.mkdir(parents=True) + (hist_dir / "metrics_history.json").write_text( + '[{"timestamp": "2024-01-01T10:00:00"}, ' + '{"timestamp": "2025-06-15T12:00:00"}]', + encoding="utf-8", + ) + assert dl.data_as_of(tmp_path) == "2025-06-15" + + +def test_data_as_of_real_data_is_a_date(): + as_of = dl.data_as_of() + assert as_of is not None + # YYYY-MM-DD shape + assert len(as_of) == 10 and as_of[4] == "-" and as_of[7] == "-"