Spaces:
Running
Running
| """Stratified puller for the FinWorkBench/Finch dataset. | |
| Selects 50 tasks across the most-frequent task_type tags, downloads source | |
| and reference xlsx files, and emits a manifest.jsonl row for each task. | |
| Usage: | |
| python data_pipeline/finch_pull.py | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import random | |
| import urllib.request | |
| from collections import defaultdict | |
| from pathlib import Path | |
| from datasets import load_dataset | |
| REPO_ROOT = Path(__file__).resolve().parent.parent | |
| DATA_DIR = REPO_ROOT / "data" / "finch_50" | |
| MANIFEST_PATH = REPO_ROOT / "data" / "manifest.jsonl" | |
| # Per-tag pick budgets. Sum = 50. Web Search tasks have non-xlsx sources | |
| # (web/PDF), so we drop them and reallocate slots to denser tags. | |
| TAG_BUDGET = { | |
| "Calculation": 16, | |
| "Structuring / Formatting": 11, | |
| "Data Entry / Import": 6, | |
| "Validation / Review": 5, | |
| "Cross-sheet/file Retrieval": 5, | |
| "Summary / Visualization": 4, | |
| "Financial Modeling": 3, | |
| } | |
| # Eval holdout per tag (rest go to train). Sum = 10. | |
| EVAL_HOLDOUT = { | |
| "Calculation": 3, | |
| "Structuring / Formatting": 2, | |
| "Data Entry / Import": 1, | |
| "Validation / Review": 1, | |
| "Cross-sheet/file Retrieval": 1, | |
| "Summary / Visualization": 1, | |
| "Financial Modeling": 1, | |
| } | |
| def primary_tag(task_type: str) -> str: | |
| """Return the first tag in the comma-separated task_type field.""" | |
| return task_type.split(",")[0].strip() | |
| def is_xlsx_task(row) -> bool: | |
| """Pure-xlsx tasks: source files are all xlsx, reference output is xlsx.""" | |
| srcs = row["source_files"] | |
| if not srcs or any(not s.lower().endswith(".xlsx") for s in srcs): | |
| return False | |
| refs = row["reference_outputs"].get("files") or [] | |
| if refs and any(not r.lower().endswith(".xlsx") for r in refs): | |
| return False | |
| return True | |
| def download(url: str, dest: Path, timeout: float = 30.0, retries: int = 3) -> None: | |
| if dest.exists() and dest.stat().st_size > 0: | |
| return | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| last_exc: Exception | None = None | |
| for _ in range(retries): | |
| try: | |
| with urllib.request.urlopen(url, timeout=timeout) as r, open(dest, "wb") as f: | |
| f.write(r.read()) | |
| return | |
| except Exception as e: | |
| last_exc = e | |
| raise RuntimeError(f"download failed after {retries} retries: {last_exc}") | |
| def select(ds, seed: int = 17) -> dict: | |
| """Return {tag: [row, ...]} sized per TAG_BUDGET, xlsx-only, single-source.""" | |
| rng = random.Random(seed) | |
| by_primary: dict[str, list] = defaultdict(list) | |
| for row in ds: | |
| if not is_xlsx_task(row): | |
| continue | |
| if len(row["source_files"]) != 1: | |
| continue | |
| if not row["reference_outputs"].get("files"): | |
| # Skip pure-QA for now; MODIFY tasks dominate and grade cleanly. | |
| continue | |
| by_primary[primary_tag(row["task_type"])].append(row) | |
| picked: dict[str, list] = {} | |
| for tag, budget in TAG_BUDGET.items(): | |
| pool = by_primary.get(tag, []) | |
| rng.shuffle(pool) | |
| picked[tag] = pool[:budget] | |
| if len(picked[tag]) < budget: | |
| print(f" ⚠ tag {tag!r}: wanted {budget}, got {len(picked[tag])}") | |
| return picked | |
| def emit_manifest(picked: dict) -> list[dict]: | |
| """Download files and build manifest rows. Returns the list of rows.""" | |
| rows: list[dict] = [] | |
| rng = random.Random(31) | |
| for tag, items in picked.items(): | |
| rng.shuffle(items) | |
| eval_n = EVAL_HOLDOUT.get(tag, 0) | |
| for i, row in enumerate(items): | |
| split = "eval" if i < eval_n else "train" | |
| tid = f"finch_{row['id']}" | |
| task_dir = DATA_DIR / row["id"] | |
| src_name = row["source_files"][0] | |
| src_url = row["source_files_urls"][0] | |
| ref_name = row["reference_outputs"]["files"][0] | |
| ref_url = row["reference_file_urls"][0] | |
| src_path = task_dir / src_name | |
| ref_path = task_dir / ref_name | |
| try: | |
| download(src_url, src_path) | |
| download(ref_url, ref_path) | |
| except Exception as e: | |
| print(f" ✗ {tid}: download failed: {e}") | |
| continue | |
| rows.append({ | |
| "id": tid, | |
| "family": "xlsx", | |
| "origin": "finch", | |
| "orig_id": row["id"], | |
| "split": split, | |
| "primary_tag": tag, | |
| "all_tags": [t.strip() for t in row["task_type"].split(",")], | |
| "business_type": row["business_type"], | |
| "instruction": row["instruction_en"], | |
| "constraints": row.get("task_constraints", "") or "", | |
| "source_file": str(src_path.relative_to(REPO_ROOT)), | |
| "reference_file": str(ref_path.relative_to(REPO_ROOT)), | |
| "task_type": "MODIFY", | |
| "max_steps": 15, | |
| }) | |
| print(f" ✓ {tid:14s} {split:5s} {tag}") | |
| return rows | |
| def main(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--dry-run", action="store_true", help="Don't download, just print picks") | |
| args = p.parse_args() | |
| import sys | |
| print("Loading FinWorkBench/Finch …", flush=True) | |
| ds = load_dataset("FinWorkBench/Finch", split="test") | |
| print(f" {len(ds)} rows", flush=True) | |
| picked = select(ds) | |
| total = sum(len(v) for v in picked.values()) | |
| print(f"\nSelected {total} tasks across {len(picked)} tags", flush=True) | |
| if args.dry_run: | |
| for tag, items in picked.items(): | |
| print(f" {tag}: {[r['id'] for r in items]}", flush=True) | |
| return | |
| DATA_DIR.mkdir(parents=True, exist_ok=True) | |
| sys.stdout.reconfigure(line_buffering=True) | |
| rows = emit_manifest(picked) | |
| rows.sort(key=lambda r: (r["split"], r["primary_tag"], r["orig_id"])) | |
| MANIFEST_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| with open(MANIFEST_PATH, "w") as f: | |
| for r in rows: | |
| f.write(json.dumps(r) + "\n") | |
| train_n = sum(1 for r in rows if r["split"] == "train") | |
| eval_n = sum(1 for r in rows if r["split"] == "eval") | |
| print(f"\nManifest written: {MANIFEST_PATH}", flush=True) | |
| print(f" train: {train_n} | eval: {eval_n}", flush=True) | |
| if __name__ == "__main__": | |
| main() |