Download sw_eval.py from GhostScientist/semanticwiki-dataset-v3-scripts: direct link, hf CLI and curl.
- Browser
- Download file 19 kB
-
https://huggingface.co/GhostScientist/semanticwiki-dataset-v3-scripts/resolve/main/sw_eval.py
- Command line
-
hf download hf://GhostScientist/semanticwiki-dataset-v3-scripts/sw_eval.py
-
curl -L -o sw_eval.py https://huggingface.co/GhostScientist/semanticwiki-dataset-v3-scripts/resolve/main/sw_eval.py
19 kB
| """SemanticWiki-Eval v3: format + citation validity + LLM-judge fidelity. | |
| Held-out protocol: | |
| 1. Discover candidate eval repos via GitHub search (same quota logic as the | |
| dataset run), EXCLUDE anything already in the dataset registry. | |
| 2. Harvest + build context with the exact dataset pipeline (sw_pipeline). | |
| 3. Generate wiki pages with each student model (merged LoRA -> vLLM). | |
| 4. Score every page on three axes: | |
| a. FORMAT - DeepWiki structure checks (h1/h2/h3, mermaid, sources, table) | |
| b. CITATION VALIDITY - deterministic verifier against the repo snapshot | |
| c. FIDELITY - LLM judge (Qwen3-Coder-30B-A3B) scores factual support | |
| against the provided context, 1-5, with unsupported-claim lists | |
| 5. Upload per-page results and summary to GhostScientist/semanticwiki-eval-v3. | |
| Run: python sw_eval.py [--n-repos 15] [--cats architecture_overview,data_flow] | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| import time | |
| import urllib.parse | |
| import urllib.request | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| OUT_DIR = Path("/work/out") | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| os.environ.setdefault("VLLM_USE_FLASHINFER_SAMPLER", "0") | |
| SCRIPTS_REPO = "GhostScientist/semanticwiki-dataset-v3-scripts" | |
| DATASET_REPO = "GhostScientist/semanticwiki-data-v3" | |
| RESULTS_REPO = "GhostScientist/semanticwiki-eval-v3" | |
| STUDENTS = [ | |
| ("sw-coder-14b-v3", "GhostScientist/semanticwiki-coder-14b-v3", | |
| "Qwen/Qwen2.5-Coder-14B-Instruct"), | |
| ("sw-qwen3-14b-v3", "GhostScientist/semanticwiki-14b-v3-qwen3", | |
| "Qwen/Qwen3-14B"), | |
| ] | |
| BASELINES = [ # un-finetuned bases, same prompts/judge - the ablation arm | |
| ("base-qwen25coder-14b", None, "Qwen/Qwen2.5-Coder-14B-Instruct"), | |
| ("base-qwen3-14b", None, "Qwen/Qwen3-14B"), | |
| ] | |
| JUDGE = "Qwen/Qwen3-Coder-30B-A3B-Instruct" | |
| SYSTEM_PROMPT = ( | |
| "You are an expert software architect writing wiki documentation for codebases.\n" | |
| "Requirements:\n" | |
| "1. Every factual claim about the code must carry a source citation in the form " | |
| "`path:line` or `path:start-end` referencing the provided source files.\n" | |
| "2. Include at least one Mermaid diagram (flowchart or sequenceDiagram) when it " | |
| "clarifies structure or flow.\n" | |
| "3. Organize with H1 title and H2/H3 sections: overview, detailed analysis, and a " | |
| "conclusion.\n" | |
| "4. Explain the WHY behind design decisions, not only the WHAT.\n" | |
| "5. Never invent files, symbols, or line numbers - cite only what the context shows.\n" | |
| "6. Citation format is strict. Every citation must be the token " | |
| "`path/file.ext:line` or `path/file.ext:start-end` - for example " | |
| "`src/click/core.py:370-545`.\n" | |
| "7. Example: 'The `Context` class (`src/click/core.py:238-987`) manages state.'\n" | |
| ) | |
| CATEGORIES = { | |
| "architecture_overview": | |
| "Generate a wiki page describing the overall architecture of this codebase: the " | |
| "main components, how they interact, entry points, and the public interface. " | |
| "Include a Mermaid flowchart of the component structure.", | |
| "data_flow": | |
| "Generate a wiki page tracing the primary data or control flow through this system " | |
| "from entry to exit. Include a Mermaid sequenceDiagram showing the interactions " | |
| "between components.", | |
| } | |
| JUDGE_SYSTEM = ( | |
| "You are a strict technical reviewer. You are given the source code context of a " | |
| "repository and a generated wiki page about it. Judge ONLY factual fidelity to the " | |
| "provided context - not style. Respond with ONLY a JSON object: " | |
| '{"fidelity_score": <int 1-5>, "supported_claims": <int>, ' | |
| '"unsupported_claims": <int>, "notes": "<one sentence>"} ' | |
| "where fidelity_score means: 5 = every claim is verifiably grounded in the context, " | |
| "1 = most claims are invented or contradicted." | |
| ) | |
| def ensure_pipeline(): | |
| try: | |
| import sw_pipeline | |
| return sys.modules["sw_pipeline"] | |
| except ImportError: | |
| pass | |
| from huggingface_hub import hf_hub_download | |
| path = hf_hub_download(SCRIPTS_REPO, "sw_pipeline.py") | |
| sys.path.insert(0, str(Path(path).parent)) | |
| import sw_pipeline | |
| return sw_pipeline | |
| def discover_eval_repos(n_repos, registry_slugs, quota=None): | |
| """Same search recipe as the dataset run, minus repos used for training.""" | |
| repos, seen = [], set(registry_slugs) | |
| if quota is None: | |
| quota = [("python", 6), ("typescript", 4), ("go", 3), ("rust", 2), ("java", 2)] | |
| page_by_lang = {lang: 1 for lang, _ in quota} | |
| idx = 0 | |
| while len(repos) < n_repos and idx < 200: | |
| lang, _ = quota[idx % len(quota)] | |
| idx += 1 | |
| page = page_by_lang[lang] | |
| if page > 4: | |
| continue | |
| page_by_lang[lang] = page + 1 | |
| q = (f"language:{lang} stars:800..100000 size:500..30000 " | |
| f"license:mit license:apache-2.0 license:bsd-3-clause") | |
| url = ("https://api.github.com/search/repositories?q=" + urllib.parse.quote(q) | |
| + f"&sort=stars&order=desc&per_page=30&page={page}") | |
| req = urllib.request.Request(url, headers={"User-Agent": "semanticwiki-eval"}) | |
| try: | |
| with urllib.request.urlopen(req, timeout=30) as r: | |
| data = json.loads(r.read()) | |
| except Exception as e: | |
| print(f"[eval] discovery {lang} p{page}: {e}") | |
| time.sleep(10) | |
| continue | |
| for item in data.get("items", []): | |
| slug = item["full_name"] | |
| if slug in seen or item.get("fork") or item.get("archived"): | |
| continue | |
| seen.add(slug) | |
| repos.append({"org": item["owner"]["login"], "name": item["name"], | |
| "language": lang, | |
| "license": (item.get("license") or {}).get("spdx_id", "unknown")}) | |
| if len(repos) >= n_repos: | |
| break | |
| time.sleep(8) | |
| print(f"[eval] {len(repos)} held-out repos (excluded {len(registry_slugs)} dataset repos)") | |
| return repos | |
| def format_checks(text): | |
| """DeepWiki structural format score (0-1).""" | |
| checks = { | |
| "has_h1": bool(re.search(r"^# ", text, re.M)), | |
| "has_h2": bool(re.search(r"^## ", text, re.M)), | |
| "has_h3": bool(re.search(r"^### ", text, re.M)), | |
| "has_mermaid": "mermaid" in text, | |
| "has_citations": bool(re.search(r"[\w./\-]+\.[A-Za-z0-9]{1,4}:\d+", text)), | |
| "has_table": "|---" in text or "| ---" in text, | |
| "has_conclusion": bool(re.search(r"## .*conclusion", text, re.I)), | |
| "word_count_ok": 300 <= len(text.split()) <= 4000, | |
| } | |
| return {"passed": sum(checks.values()), "total": len(checks), **checks} | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--n-repos", type=int, default=15) | |
| ap.add_argument("--cats", default="architecture_overview,data_flow") | |
| ap.add_argument("--baseline", action="store_true", | |
| help="evaluate the un-finetuned base models instead of adapters") | |
| ap.add_argument("--repo-slugs", default=None, | |
| help="comma-separated org/name list; pins the held-out repo set " | |
| "for a paired comparison instead of re-discovering") | |
| ap.add_argument("--repo-list-file", default=None, | |
| help="path to a JSON file [{org,name,...}] pinning the repo set " | |
| "(overrides discovery and --repo-slugs)") | |
| args = ap.parse_args() | |
| cats = [c.strip() for c in args.cats.split(",")] | |
| sw = ensure_pipeline() | |
| from vllm import LLM, SamplingParams | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| from huggingface_hub import snapshot_download | |
| import torch | |
| from peft import PeftModel | |
| # held-out repos | |
| from huggingface_hub import hf_hub_download | |
| reg_path = hf_hub_download(DATASET_REPO, "repo_registry.json", repo_type="dataset") | |
| registry = json.loads(Path(reg_path).read_text()) | |
| registry_slugs = {f"{r['org']}/{r['name']}" for r in registry} | |
| if args.repo_list_file: | |
| eval_specs = json.loads(Path(args.repo_list_file).read_text()) | |
| print(f"[eval] pinned {len(eval_specs)} repos from {args.repo_list_file}") | |
| elif args.repo_slugs: | |
| eval_specs = [] | |
| for slug in [s.strip() for s in args.repo_slugs.split(",") if s.strip()]: | |
| org, name = slug.split("/") | |
| eval_specs.append({"org": org, "name": name, "language": "unknown", | |
| "license": "unknown"}) | |
| print(f"[eval] pinned {len(eval_specs)} repos (paired comparison)") | |
| else: | |
| eval_specs = discover_eval_repos(args.n_repos, registry_slugs) | |
| tok_judge = AutoTokenizer.from_pretrained(JUDGE) | |
| repos_ctx = [] | |
| for spec in eval_specs: | |
| try: | |
| repo = sw.harvest(spec["org"], spec["name"], OUT_DIR / "repos") | |
| repo.language = spec["language"] | |
| ctx, n_tok = None, None | |
| for budget in (45_000, 30_000, 18_000): | |
| windows = sw.select_files(repo, budget=budget) | |
| ctx = sw.render_context(repo, windows) | |
| n_tok = len(tok_judge.encode(ctx)) | |
| if n_tok <= 20_000: | |
| break | |
| if len(repo.files) < 3: | |
| raise RuntimeError("too few source files") | |
| repos_ctx.append((spec, repo, ctx)) | |
| print(f"[eval] {spec['org']}/{spec['name']}: {len(repo.files)} files, ctx {n_tok} tok") | |
| except Exception as e: | |
| print(f"[eval] HARVEST FAIL {spec['org']}/{spec['name']}: {e}") | |
| # ---------------- generate with each student ---------------- | |
| results = [] # per (model, repo, category): page + all three scores | |
| arms = BASELINES if args.baseline else STUDENTS | |
| for model_name, adapter_id, base_id in arms: | |
| print(f"[eval] loading student {model_name} ({base_id}, " | |
| f"adapter={adapter_id})") | |
| merged_dir = OUT_DIR / f"merged_{model_name}" | |
| if adapter_id: | |
| base = AutoModelForCausalLM.from_pretrained( | |
| base_id, dtype=torch.bfloat16, device_map="cpu", | |
| attn_implementation="sdpa") | |
| base = PeftModel.from_pretrained(base, adapter_id) | |
| base = base.merge_and_unload() | |
| base.save_pretrained(merged_dir) | |
| AutoTokenizer.from_pretrained(base_id).save_pretrained(merged_dir) | |
| del base | |
| torch.cuda.empty_cache() | |
| else: | |
| # un-finetuned baseline: point vLLM straight at the base weights | |
| merged_dir = Path(base_id) | |
| llm = LLM(model=str(merged_dir), max_model_len=24576, | |
| gpu_memory_utilization=0.92, max_num_seqs=8) | |
| sampling = SamplingParams(temperature=0.7, top_p=0.8, max_tokens=4096) | |
| convs, meta = [], [] | |
| for spec, repo, ctx in repos_ctx: | |
| for cat in cats: | |
| user = (f"<START_OF_CONTEXT>\n{ctx}\n<END_OF_CONTEXT>\n\n" | |
| f"<query>\n{CATEGORIES[cat]}\n</query>") | |
| convs.append([ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": user}, | |
| ]) | |
| meta.append((spec, repo, cat)) | |
| outs = llm.chat(convs, sampling) | |
| for (spec, repo, cat), out in zip(meta, outs): | |
| o = out.outputs[0] | |
| text = o.text | |
| verdict = sw.verify_citations(text, repo) | |
| fmt = format_checks(text) | |
| results.append({ | |
| "model": model_name, "source": f"{spec['org']}/{spec['name']}", | |
| "category": cat, "finish_reason": str(o.finish_reason), | |
| "page": text, | |
| "format": fmt, | |
| "citation": {"total": verdict["total_citations"], | |
| "validity_rate": verdict["validity_rate"], | |
| "problems": verdict["problems"][:10]}, | |
| }) | |
| del llm | |
| torch.cuda.empty_cache() | |
| print(f"[eval] {model_name}: {len(meta)} pages generated") | |
| # ---------------- judge pass ---------------- | |
| print(f"[eval] loading judge {JUDGE}") | |
| judge_llm = LLM(model=JUDGE, max_model_len=24576, | |
| gpu_memory_utilization=0.92, max_num_seqs=8) | |
| jsamp = SamplingParams(temperature=0.0, max_tokens=400) | |
| ctx_by_slug = {f"{s['org']}/{s['name']}": ctx for s, _, ctx in repos_ctx} | |
| jconvs = [] | |
| for r in results: | |
| juser = (f"<SOURCE_CONTEXT>\n{ctx_by_slug[r['source']]}\n</SOURCE_CONTEXT>\n\n" | |
| f"<GENERATED_PAGE>\n{r['page']}\n</GENERATED_PAGE>\n\n" | |
| "Score factual fidelity per the system instructions. JSON only.") | |
| jconvs.append([{"role": "system", "content": JUDGE_SYSTEM}, | |
| {"role": "user", "content": juser}]) | |
| jouts = judge_llm.chat(jconvs, jsamp) | |
| for r, jo in zip(results, jouts): | |
| raw = jo.outputs[0].text | |
| try: | |
| m = re.search(r"\{.*\}", raw, re.S) | |
| r["judge"] = json.loads(m.group(0)) if m else {"parse_error": raw[:200]} | |
| except Exception: | |
| r["judge"] = {"parse_error": raw[:200]} | |
| del judge_llm | |
| torch.cuda.empty_cache() | |
| # ---------------- summary + upload ---------------- | |
| suffix = "baseline" if args.baseline else "finetuned" | |
| with (OUT_DIR / f"eval_pages_{suffix}.jsonl").open("w", encoding="utf-8") as f: | |
| for r in results: | |
| f.write(json.dumps(r) + "\n") | |
| summary = { | |
| "generated_at": datetime.now(timezone.utc).isoformat(), | |
| "n_repos": len(repos_ctx), | |
| "categories": cats, | |
| "judge": JUDGE, | |
| "models": {name: _agg(name, results) for name, _, _ in arms}, | |
| } | |
| # family pairings for paired deltas (finetuned vs base, same repo/category) | |
| family_map = { | |
| "sw-coder-14b-v3": "base-qwen25coder-14b", | |
| "sw-qwen3-14b-v3": "base-qwen3-14b", | |
| } | |
| if not args.baseline: | |
| summary["paired_tests"] = {} | |
| for ft, base in family_map.items(): | |
| for axis, key in (("citation_validity", "citation"), ("format", "format"), | |
| ("fidelity", "fidelity")): | |
| summary["paired_tests"][f"{ft}_vs_{base}:{axis}"] = _paired_delta( | |
| [dict(r, **{key: _page_metric(r, key)}) for r in results if r["model"] == ft], | |
| [dict(r, **{key: _page_metric(r, key)}) for r in results if r["model"] == base], | |
| key) | |
| (OUT_DIR / f"eval_summary_{suffix}.json").write_text(json.dumps(summary, indent=2)) | |
| print("[eval] summary:", json.dumps(summary, indent=2)) | |
| if os.environ.get("HF_TOKEN"): | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| api.upload_file(path_or_fileobj=str(OUT_DIR / f"eval_pages_{suffix}.jsonl"), | |
| path_in_repo=f"eval_pages_{suffix}.jsonl", repo_id=RESULTS_REPO, | |
| repo_type="dataset") | |
| api.upload_file(path_or_fileobj=str(OUT_DIR / f"eval_summary_{suffix}.json"), | |
| path_in_repo=f"eval_summary_{suffix}.json", repo_id=RESULTS_REPO, | |
| repo_type="dataset") | |
| # snapshots of held-out repos for later re-verification | |
| snaps = OUT_DIR / "repos" | |
| tar = OUT_DIR / "eval_repo_snapshots.tar.gz" | |
| subprocess.run(["tar", "czf", str(tar), "-C", str(snaps), "."], check=True) | |
| api.upload_file(path_or_fileobj=str(tar), path_in_repo="eval_repo_snapshots.tar.gz", | |
| repo_id=RESULTS_REPO, repo_type="dataset") | |
| print("[eval] uploaded to " + RESULTS_REPO) | |
| def _bootstrap_ci(values, n_boot=1000, seed=7): | |
| """Mean and 95% percentile-bootstrap CI over page-level values.""" | |
| import random | |
| rng = random.Random(seed) | |
| vals = [v for v in values if v is not None] | |
| if not vals: | |
| return None | |
| means = sorted(sum(rng.choices(vals, k=len(vals))) / len(vals) | |
| for _ in range(n_boot)) | |
| lo, hi = means[int(0.025 * n_boot)], means[int(0.975 * n_boot) - 1] | |
| return {"mean": round(sum(vals) / len(vals), 3), | |
| "ci95": [round(lo, 3), round(hi, 3)], "n": len(vals)} | |
| def _paired_delta(rows_a, rows_b, key, n_boot=2000, seed=7): | |
| """Paired bootstrap over (repo, category) cells: distribution of | |
| mean(a) - mean(b). Returns CI and two-sided p (share of resampled deltas | |
| consistent with zero effect, i.e. sign-flipped relative to observed).""" | |
| import random | |
| rng = random.Random(seed) | |
| cell = {} | |
| for r in rows_a: | |
| if r[key] is not None: | |
| cell.setdefault((r["source"], r["category"]), [[], []])[0].append(r[key]) | |
| for r in rows_b: | |
| if r[key] is not None: | |
| cell.setdefault((r["source"], r["category"]), [[], []])[1].append(r[key]) | |
| deltas = [sum(a) / len(a) - sum(b) / len(b) for a, b in cell.values() if a and b] | |
| if len(deltas) < 5: | |
| return {"n_cells": len(deltas), "note": "too few paired cells"} | |
| boot = sorted(sum(rng.choices(deltas, k=len(deltas))) / len(deltas) | |
| for _ in range(n_boot)) | |
| lo, hi = boot[int(0.025 * n_boot)], boot[int(0.975 * n_boot) - 1] | |
| obs = sum(deltas) / len(deltas) | |
| # sign-flip randomization: null = resampled deltas with random sign flips; | |
| # p_one = share of null means at least as extreme as the observed delta | |
| null = [] | |
| for _ in range(n_boot): | |
| samp = [d if rng.random() < 0.5 else -d for d in rng.choices(deltas, k=len(deltas))] | |
| null.append(sum(samp) / len(samp)) | |
| if obs >= 0: | |
| p = sum(1 for m in null if m >= obs) / n_boot | |
| else: | |
| p = sum(1 for m in null if m <= obs) / n_boot | |
| return {"n_cells": len(deltas), "delta": round(obs, 3), | |
| "ci95": [round(lo, 3), round(hi, 3)], "p_two_sided": round(2 * min(p, 1 - p), 3)} | |
| def _page_metric(r, key): | |
| if key == "format": | |
| return r["format"]["passed"] / r["format"]["total"] | |
| if key == "citation": | |
| return r["citation"]["validity_rate"] | |
| if key == "fidelity": | |
| v = r.get("judge", {}).get("fidelity_score") | |
| return v if isinstance(v, int) else None | |
| return None | |
| def _agg(model, results): | |
| rows = [r for r in results if r["model"] == model] | |
| if not rows: | |
| return {} | |
| out = {"n_pages": len(rows)} | |
| for axis, key in (("format_score", "format"), ("citation_validity", "citation"), | |
| ("fidelity_score", "fidelity")): | |
| out[axis] = _bootstrap_ci([_page_metric(r, key) for r in rows]) | |
| out["mean_citations"] = round(sum(r["citation"]["total"] for r in rows) / len(rows), 1) | |
| by_cat = {} | |
| for cat in {r["category"] for r in rows}: | |
| sub = [r for r in rows if r["category"] == cat] | |
| by_cat[cat] = {axis: _bootstrap_ci([_page_metric(r, key) for r in sub]) | |
| for axis, key in (("format", "format"), ("citation", "citation"), | |
| ("fidelity", "fidelity"))} | |
| out["per_category"] = by_cat | |
| return out | |
| if __name__ == "__main__": | |
| main() |