Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

311 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Build an evaluation dataset: cross-product of queries × search engines × judge LLMs.
For each (query, engine, model) cell, runs `quick_summary` end-to-end and
saves (a) the formatted report and (b) a verbose log capturing the
filter's KEPT/REMOVED decisions. Writes a summary CSV so you can rank
runs by source count, timing, and visually inspect outliers.
Resumable: cells whose report file already exists are skipped unless
--force is passed. Use this to recover from interrupted runs or to
extend an existing dataset with new rows.
Usage (defaults produce a ~12-cell grid ≈ 60-90min total on qwen3.5:9b):
LDR_BOOTSTRAP_ALLOW_UNENCRYPTED=true LDR_TESTING_WITH_MOCKS=false \\
pdm run python tests/performance/_shared/build_eval_dataset.py \\
--output-dir ./ldr_eval_output
Override any axis from the CLI:
... --queries "q1|q2" --engines "arxiv,openalex" --models "qwen3.5:9b,gemma3:12b"
Be conservative with --parallel — each parallel slot loads an LLM on the
Ollama endpoint; running 2+ different model tags concurrently may OOM
the server. Same-model parallel is usually fine (Ollama queues).
"""
from __future__ import annotations
import argparse
import csv
import os
import re
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
DEFAULT_QUERIES = [
"LLM interpretability latest research",
"mechanistic interpretability of transformer language models",
"sparse autoencoders for neural network feature discovery",
"safety alignment and refusal in large language models",
]
DEFAULT_ENGINES = ["arxiv", "openalex"]
DEFAULT_MODELS = ["qwen3.5:9b", "gemma3:12b", "ministral-3:14b"]
def slugify(text: str, max_len: int = 50) -> str:
s = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
return s[:max_len]
@dataclass
class Cell:
query: str
engine: str
model: str
def key(self) -> str:
return f"{self.engine}__{slugify(self.model)}__{slugify(self.query)}"
def already_done(cell: Cell, out_dir: Path) -> bool:
return (out_dir / "reports" / f"{cell.key()}.md").exists()
def run_cell(cell: Cell, out_dir: Path, ollama_url: str) -> dict:
"""Invoke run_full_search.py as a subprocess for isolation between cells."""
report_path = out_dir / "reports" / f"{cell.key()}.md"
log_path = out_dir / "logs" / f"{cell.key()}.log"
script = Path(__file__).parent / "run_full_search.py"
cmd = [
sys.executable,
str(script),
"--query",
cell.query,
"--engine",
cell.engine,
"--model",
cell.model,
"--ollama-url",
ollama_url,
"--verbose",
"--output",
str(report_path),
]
env = os.environ.copy()
env.setdefault("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED", "true")
env.setdefault("LDR_TESTING_WITH_MOCKS", "false")
t0 = time.monotonic()
try:
with log_path.open("w") as logf:
proc = subprocess.run(
cmd,
env=env,
stdout=logf,
stderr=subprocess.STDOUT,
timeout=60 * 30, # 30-min hard cap per cell
check=False,
)
elapsed = time.monotonic() - t0
exit_code = proc.returncode
except subprocess.TimeoutExpired:
elapsed = time.monotonic() - t0
exit_code = -1
# A partially-written report would otherwise cause ``already_done``
# to skip this cell on the next run even though it never finished.
report_path.unlink(missing_ok=True)
except Exception as exc:
elapsed = time.monotonic() - t0
exit_code = -2
log_path.write_text(f"Exception: {exc!r}\n")
report_path.unlink(missing_ok=True)
result = {
"query": cell.query,
"engine": cell.engine,
"model": cell.model,
"elapsed_s": round(elapsed, 1),
"exit_code": exit_code,
"report_path": str(report_path) if report_path.exists() else "",
"sources": 0,
"log_path": str(log_path),
}
if report_path.exists():
text = report_path.read_text()
m = re.search(r"^\s*-\s*\*\*Sources:\*\*\s*(\d+)", text, re.M)
if m:
result["sources"] = int(m.group(1))
return result
def parse_list(
s: str, sep_primary: str = "|", sep_fallback: str = ","
) -> list[str]:
"""Parse a delimiter-separated list — '|' preferred for queries with commas."""
sep = sep_primary if sep_primary in s else sep_fallback
return [x.strip() for x in s.split(sep) if x.strip()]
def main() -> int:
p = argparse.ArgumentParser(
description="Build a cross-product eval dataset (query × engine × model).",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p.add_argument(
"--queries",
default=None,
help="Queries separated by '|'. Default: a 4-query interpretability set.",
)
p.add_argument(
"--engines",
default=",".join(DEFAULT_ENGINES),
help="Comma-separated engine names.",
)
p.add_argument(
"--models",
default=",".join(DEFAULT_MODELS),
help="Comma-separated Ollama model tags.",
)
p.add_argument(
"--output-dir",
default="./ldr_eval_output",
help="Where reports/, logs/, summary.csv land.",
)
p.add_argument(
"--ollama-url",
default=os.environ.get(
"LDR_TEST_OLLAMA_BASE_URL", "http://localhost:11434"
),
)
p.add_argument(
"--parallel",
type=int,
default=1,
help="Cells to run concurrently. 1 = sequential. 2+ may OOM Ollama if different models are loaded simultaneously.",
)
p.add_argument(
"--force",
action="store_true",
help="Re-run cells even if their report file already exists.",
)
args = p.parse_args()
queries = (
parse_list(args.queries) if args.queries else list(DEFAULT_QUERIES)
)
engines = parse_list(args.engines)
models = parse_list(args.models)
out_dir = Path(args.output_dir).resolve()
(out_dir / "reports").mkdir(parents=True, exist_ok=True)
(out_dir / "logs").mkdir(parents=True, exist_ok=True)
cells = [
Cell(query=q, engine=e, model=m)
for q in queries
for e in engines
for m in models
]
total = len(cells)
todo = [c for c in cells if args.force or not already_done(c, out_dir)]
skipped = total - len(todo)
print(
f"Grid: {len(queries)} queries × {len(engines)} engines × {len(models)} models = {total} cells",
file=sys.stderr,
)
print(f" {skipped} already done (use --force to re-run)", file=sys.stderr)
print(f" {len(todo)} to run, parallel={args.parallel}", file=sys.stderr)
print(f" Output: {out_dir}", file=sys.stderr)
summary_path = out_dir / "summary.csv"
summary_rows: list[dict] = []
if summary_path.exists():
with summary_path.open() as f:
summary_rows = list(csv.DictReader(f))
def run_and_log(cell: Cell) -> dict:
print(
f" >>> {cell.engine} | {cell.model} | {cell.query[:60]}",
file=sys.stderr,
)
r = run_cell(cell, out_dir, args.ollama_url)
status = "ok" if r["exit_code"] == 0 else f"exit={r['exit_code']}"
print(
f" {status} {r['elapsed_s']}s sources={r['sources']} {cell.key()}",
file=sys.stderr,
)
return r
t0 = time.monotonic()
if args.parallel <= 1:
for cell in todo:
summary_rows = _upsert(summary_rows, run_and_log(cell))
_write_summary(summary_path, summary_rows)
else:
with ThreadPoolExecutor(max_workers=args.parallel) as pool:
futures = {pool.submit(run_and_log, c): c for c in todo}
for fut in as_completed(futures):
summary_rows = _upsert(summary_rows, fut.result())
_write_summary(summary_path, summary_rows)
elapsed = time.monotonic() - t0
print(
f"\nDataset build complete in {elapsed / 60:.1f}min.", file=sys.stderr
)
print(f" Summary: {summary_path}", file=sys.stderr)
_print_table(summary_rows)
return 0
def _upsert(rows: list[dict], new: dict) -> list[dict]:
"""Replace a row with the same (query, engine, model) or append."""
out = [
r
for r in rows
if not (
r.get("query") == new["query"]
and r.get("engine") == new["engine"]
and r.get("model") == new["model"]
)
]
out.append({k: str(v) for k, v in new.items()})
return out
def _write_summary(path: Path, rows: list[dict]) -> None:
if not rows:
return
fieldnames = [
"query",
"engine",
"model",
"sources",
"elapsed_s",
"exit_code",
"report_path",
"log_path",
]
with path.open("w", newline="") as f:
w = csv.DictWriter(f, fieldnames=fieldnames)
w.writeheader()
for r in rows:
w.writerow({k: r.get(k, "") for k in fieldnames})
def _print_table(rows: list[dict]) -> None:
if not rows:
return
print("\n=== Dataset summary ===")
print(
f"{'engine':10s} {'model':20s} {'sources':>8s} {'elapsed_s':>10s} query"
)
for r in rows:
print(
f"{r.get('engine', ''):10s} {r.get('model', ''):20s} "
f"{str(r.get('sources', '')):>8s} {str(r.get('elapsed_s', '')):>10s} "
f"{r.get('query', '')[:60]}"
)
if __name__ == "__main__":
sys.exit(main())