Download compare_extractors.py from DHDRL/AgentSquares: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/DHDRL/AgentSquares/resolve/main/compare_extractors.py
- Command line
-
hf download hf://DHDRL/AgentSquares/compare_extractors.py
-
curl -L -o compare_extractors.py https://huggingface.co/DHDRL/AgentSquares/resolve/main/compare_extractors.py
10.4 kB
| """Rule-based vs LLM extractors (multi-provider) on the fixtures, same agent, same scorer. | |
| python compare_extractors.py --providers flint --limit 5 --sleep 4 | |
| python compare_extractors.py --mock # plumbing check only (not a result) | |
| Options: --dev-only | --heldout-only, --only ID[,ID...] (e.g. --only s18 or --only real10,real11), | |
| --limit N (new scenarios per provider this run), --sleep S, --fresh, --allow-heldout-rerun | |
| (voids the "scored once" held-out lock — see README). | |
| Partial results go in results/extractor_compare_state.json, and reruns skip finished (provider, scenario) pairs. | |
| A provider without a key, or one marked unsupported, is SKIPPED with a reason. No score is ever filled in. | |
| Per-scenario LLM errors fall back to the rule extractor inside LLMExtractor, so llm_errors are counted and shown.""" | |
| import argparse, hashlib, json, os, sys, time | |
| from agent import Config, run | |
| from eval_harness import bounds_for, load_scenarios, score, summary | |
| from bounds import load_bounds | |
| from interfaces import FixedPlanner, LLMExtractor, LLMPlanner, MockLLMClient, OpenAICompatClient, RegexGuardExtractor, RuleExtractor | |
| MAX_FALLBACK = 0.10 # above this share of chunks falling back to rules, a row is NOT an LLM result | |
| from tools import MockTools | |
| STATE = "results/extractor_compare_state.json" | |
| LOCK = "results/heldout_scored.json" | |
| CODE_FILES = ("interfaces.py", "tools.py", "agent.py", "bounds.py", "packing.py", "providers.json") | |
| def code_hash(): | |
| h = hashlib.sha256() | |
| for f in CODE_FILES: | |
| with open(f, "rb") as fh: | |
| h.update(fh.read()) | |
| return h.hexdigest()[:16] | |
| def run_one(sc, extractor, planner, base): | |
| ms = sc.get("max_steps", 8) | |
| rep = run(sc["prompt"], MockTools(sc["tools"]), bounds_for(sc, base), ms, Config(extractor=extractor, planner=planner)) | |
| r = score(rep, sc["expected"], ms) | |
| r.update(id=sc["id"], held_out=sc["held_out"], llm_errors=sum("llm error" in n for n in rep["notes"]), | |
| llm_chunks=sum(("llm error" in n) or ("llm chunk ok" in n) for n in rep["notes"])) | |
| return r | |
| def _provider_cfg(cfg, name): | |
| p = dict(cfg.get(name) or {}) | |
| loc_path = "fixtures/private_unredacted/providers.local.json" | |
| if os.path.exists(loc_path): | |
| loc = (json.load(open(loc_path)).get(name) or {}) | |
| p.update({k: v for k, v in loc.items() if not str(k).startswith("_")}) | |
| return p | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--providers", default=""); ap.add_argument("--config", default="providers.json") | |
| ap.add_argument("--mock", action="store_true"); ap.add_argument("--limit", type=int) | |
| ap.add_argument("--sleep", type=float); ap.add_argument("--fresh", action="store_true") | |
| ap.add_argument("--only", default="", help="comma-separated scenario ids to keep (e.g. s18 or real10,real11)") | |
| ap.add_argument("--allow-heldout-rerun", action="store_true", | |
| help="voids the held-out scored-once lock (results no longer count as scored once)") | |
| ap.add_argument("--with-synthetic", action="store_true", help="include synthetic scenarios alongside real") | |
| g = ap.add_mutually_exclusive_group(); g.add_argument("--dev-only", action="store_true"); g.add_argument("--heldout-only", action="store_true") | |
| a = ap.parse_args() | |
| split = "dev" if a.dev_only else "heldout" if a.heldout_only else "all" | |
| # --only may name synthetic (s18) or real (real10) ids; without it, keep the real-fixture default. | |
| scs = load_scenarios(split, real=None if (a.only or a.with_synthetic) else True) | |
| if a.only: | |
| want = {x.strip() for x in a.only.split(",") if x.strip()} | |
| scs = [sc for sc in scs if sc["id"] in want or any(sc["id"].startswith(w) for w in want)] | |
| if not scs: | |
| raise SystemExit(f"--only matched no scenarios: {sorted(want)}") | |
| watch = set(scs[0]["prompt"]["n"]) if scs else None | |
| cfg = json.load(open(a.config)) | |
| base = load_bounds("bounds.json") | |
| os.makedirs("results", exist_ok=True) | |
| ch = code_hash() | |
| state = {} if a.fresh or not os.path.exists(STATE) else json.load(open(STATE)) | |
| if state.get("_code_hash") != ch: | |
| if any(k for k in state if not str(k).startswith("_")): | |
| print("code/prompt/provider config changed: discarding resumed results") | |
| state = {"_code_hash": ch} | |
| else: | |
| state["_code_hash"] = ch | |
| lock = {} if not os.path.exists(LOCK) else json.load(open(LOCK)) | |
| state["rule"] = {sc["id"]: run_one(sc, RuleExtractor(), FixedPlanner(), base) for sc in scs} | |
| state["regex_guards"] = {sc["id"]: run_one(sc, RegexGuardExtractor(), FixedPlanner(), base) for sc in scs} | |
| meta, skipped = {}, {} | |
| names = (["mock"] if a.mock else []) + [p for p in a.providers.split(",") if p] | |
| for name in names: | |
| if name == "mock": | |
| client = MockLLMClient([{"claims": [], "queries": []}]); meta[name] = {"model": "mock (plumbing only, NOT a result)"} | |
| else: | |
| p = _provider_cfg(cfg, name) | |
| if not p: skipped[name] = "not in config"; continue | |
| if not p.get("supported", True): skipped[name] = p.get("reason", "unsupported"); continue | |
| env = p.get("env", "SQ_LLM_API_KEY") | |
| key = os.environ.get(env, "") | |
| if not key: skipped[name] = f"${env} not set"; continue | |
| model = p.get("model") or os.environ.get(p.get("model_env", "SQ_LLM_MODEL"), "") | |
| base_url = p.get("base_url") or os.environ.get(p.get("base_url_env", "SQ_LLM_BASE_URL"), "") | |
| if not model or not base_url: | |
| skipped[name] = "model/base_url missing (set env or private local config)"; continue | |
| client = OpenAICompatClient(model=model, base_url=base_url, api_key=key, | |
| sleep_s=a.sleep if a.sleep is not None else p.get("sleep_s", 1.0), | |
| timeout=p.get("timeout", 120), | |
| json_mode=p.get("json_mode", True)) | |
| meta[name] = {"model": model, "cost": p.get("cost")} | |
| done = state.setdefault(name, {}); n_new = 0 | |
| if not isinstance(done, dict): | |
| done = {}; state[name] = done | |
| for sc in scs: | |
| if sc["id"] in done: continue | |
| if a.limit is not None and n_new >= a.limit: break | |
| if sc.get("held_out") and name != "mock" and name in lock and lock[name].get("code_hash") != ch and not a.allow_heldout_rerun: | |
| skipped[name] = "held-out already scored under different code; pass --allow-heldout-rerun to override" | |
| break | |
| try: | |
| done[sc["id"]] = run_one(sc, LLMExtractor(client, watch=watch), LLMPlanner(client), base) | |
| except Exception as e: # unexpected failure: stop this provider, keep what we have | |
| skipped[name] = f"stopped at {sc['id']}: {type(e).__name__}: {e}"[:200]; break | |
| n_new += 1 | |
| if sc.get("held_out") and name != "mock": | |
| entry = lock.get(name) or {"code_hash": ch, "arm": name, "held_out_scenarios": []} | |
| entry["code_hash"] = ch | |
| entry["arm"] = name | |
| hs = list(entry.get("held_out_scenarios") or []) | |
| if sc["id"] not in hs: | |
| hs.append(sc["id"]) | |
| entry["held_out_scenarios"] = hs | |
| lock[name] = entry | |
| json.dump(lock, open(LOCK, "w"), indent=1) | |
| json.dump(state, open(STATE, "w"), indent=1) # checkpoint after every scenario | |
| meta[name]["usage_this_run"] = getattr(client, "usage", {}) | |
| json.dump(state, open(STATE, "w"), indent=1) | |
| ids = [sc["id"] for sc in scs] | |
| lines = [f"# Extractor comparison (local-run), split={split}", | |
| "", "| system | model | scenarios done | pass | Wilson 95% | false imp | false cand | missed cand | llm fallback chunks | status |", | |
| "|---|---|---|---|---|---|---|---|---|---|"] | |
| rows = {} | |
| name_list = ["rule", "regex_guards"] + [n for n in names if (n in state and n not in skipped) or n in meta] | |
| for name in name_list: | |
| if str(name).startswith("_"): continue | |
| bucket = state.get(name, {}) | |
| if not isinstance(bucket, dict): continue | |
| R = [bucket[i] for i in ids if i in bucket] | |
| if not R: continue | |
| s = summary(R, name) | |
| fb, chs = sum(r.get("llm_errors", 0) for r in R), sum(r.get("llm_chunks", 0) for r in R) | |
| is_llm = name not in ("rule", "regex_guards") | |
| if is_llm and (chs == 0 or fb / chs > MAX_FALLBACK): | |
| s["status"] = f"INSUFFICIENT: {fb}/{chs} chunks fell back to rules (> {MAX_FALLBACK:.0%}); not an LLM result" | |
| lines.append(f"| {name} | {meta.get(name, {}).get('model', '?')} | {len(R)}/{len(ids)} | - | - | - | - | - | {fb}/{chs} | {s['status']} |") | |
| else: | |
| s["status"] = "ok" if is_llm else "deterministic" | |
| lines.append(f"| {name} | {meta.get(name, {}).get('model', 'regex' if name == 'regex_guards' else 'rules')} | {len(R)}/{len(ids)} | {s['pass']} | {s['wilson95']} | " | |
| f"{s['false_improvements']} | {s['false_candidates']} | {s['missed_candidates']} | {fb}/{chs} | {s['status']} |") | |
| rows[name] = s | |
| lines.append("") | |
| lines += [f"- SKIPPED {k}: {v}" for k, v in skipped.items()] | |
| lines.append("- Partial rows (scenarios done < total) only cover those scenarios and can't be compared head to head yet.") | |
| lines.append("- --allow-heldout-rerun voids the held-out 'scored once' guarantee.") | |
| for name in [k for k in rows if not str(rows[k]["status"]).startswith("INSUFFICIENT")]: | |
| for i in ids: | |
| bucket = state.get(name, {}) | |
| r = bucket.get(i) if isinstance(bucket, dict) else None | |
| if r and not r["pass"]: lines.append(f"- {name} {'*' if r['held_out'] else ''}{i}: {'; '.join(r['fails'])}") | |
| md = "\n".join(lines); print(md) | |
| ts = "local" | |
| open(f"results/extractor_compare_{ts}.md", "w").write(md + "\n") | |
| per = {k: state[k] for k in rows if not str(k).startswith("_")} | |
| json.dump({"split": split, "summary": rows, "skipped": skipped, "meta": meta, "code_hash": ch, | |
| "per_scenario": per}, open(f"results/extractor_compare_{ts}.json", "w"), indent=1) | |
| if __name__ == "__main__": | |
| main() | |