Download evals/codebase_navigability/lookup_sim.py from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 7.31 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/evals/codebase_navigability/lookup_sim.py
- Command line
-
hf download hf://SaylorTwift/hermes-agent/evals/codebase_navigability/lookup_sim.py
-
curl -L -o lookup_sim.py https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/evals/codebase_navigability/lookup_sim.py
7.31 kB
| #!/usr/bin/env python3 | |
| """Simulated agent lookups: how many tool calls / tokens to answer "show me the definition of X"? | |
| Deterministic agent policy (the one a careful model actually follows, given read_file's 2,000-line window): | |
| 1. grep -n "def X\\b|class X\\b|^X\\s*=" across the tree -> 1 call, returns (file, line) hits | |
| 2. read_file(file, offset=hit_line-20, limit=W) for W in (200,) -> 1 call; if the symbol's end is past the | |
| window, read the next 2,000-line window until it is -> +1 call each | |
| 3. done when the whole definition is in context | |
| Costs charged per task: | |
| tool_calls = 1 grep + N reads | |
| tokens = tokens of grep output (all hits, one line each) + tokens of every read window returned | |
| Both trees get the same symbol set: the intersection of symbols that exist (by name) on both, so a symbol moving | |
| to a smaller file counts as a WIN and a symbol that was deleted on one side is excluded, not scored. | |
| Usage: python evals/codebase_navigability/lookup_sim.py <base_tree> <head_tree> [--sample N] [--seed S] | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import ast | |
| import json | |
| import os | |
| import random | |
| import statistics | |
| import sys | |
| from pathlib import Path | |
| SKIP_TOP = {".git", "node_modules", "apps", "website", "build", ".venv", "venv", "MagicMock", "__pycache__", ".worktrees", "dist", "evals", "skills", "optional-skills", "docs", "tests"} | |
| WINDOW = 2000 # read_file's default max lines per call | |
| FIRST_READ = 200 # a careful agent's first targeted read around the grep hit (override: --first-read) | |
| MARGIN = 5 # lines of context above the grep hit | |
| def tokenizer(): | |
| try: | |
| import tiktoken | |
| enc = tiktoken.get_encoding("o200k_base") | |
| return lambda s: len(enc.encode(s, disallowed_special=())) | |
| except Exception: | |
| return lambda s: len(s.encode()) // 4 | |
| def index(tree: Path): | |
| """name -> list of (relpath, start_line, end_line, lines_of_file)""" | |
| idx: dict[str, list] = {} | |
| files: dict[str, list[str]] = {} | |
| for dp, dns, fns in os.walk(tree): | |
| rel = os.path.relpath(dp, tree) | |
| if rel != "." and rel.split(os.sep)[0] in SKIP_TOP: | |
| dns[:] = [] | |
| continue | |
| for f in fns: | |
| if not f.endswith(".py"): | |
| continue | |
| p = Path(dp) / f | |
| src = p.read_text(encoding="utf-8", errors="replace") | |
| lines = src.split("\n") | |
| r = os.path.relpath(p, tree) | |
| files[r] = lines | |
| try: | |
| t = ast.parse(src) | |
| except SyntaxError: | |
| continue | |
| for n in ast.walk(t): | |
| if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): | |
| s = min([d.lineno for d in n.decorator_list] + [n.lineno]) | |
| idx.setdefault(n.name, []).append((r, s, n.end_lineno, len(lines))) | |
| return idx, files | |
| def grep_hits(name, idx): | |
| """Simulate `grep -rn` for the symbol: every def/class of that name, one line each.""" | |
| return idx.get(name, []) | |
| def simulate(name, idx, files, tok): | |
| hits = grep_hits(name, idx) | |
| if not hits: | |
| return None | |
| grep_out = "\n".join(f"{r}:{s}: def {name}(...)" for r, s, _, _ in hits) | |
| tokens = tok(grep_out) | |
| calls = 1 | |
| # The agent opens the FIRST hit (deterministic; ambiguity costs are the same policy on both trees) | |
| r, s, e, n = hits[0] | |
| lines = files[r] | |
| start = max(1, s - MARGIN) | |
| # The agent doesn't know where the definition ends until it reads it: it asks for a window | |
| # sized to the typical definition (FIRST_READ) and pages forward only if the def keeps going. | |
| end = min(n, start + FIRST_READ - 1) | |
| tokens += tok("\n".join(lines[start - 1:end])) | |
| calls += 1 | |
| while end < e: # definition continues past the window: page forward | |
| start = end + 1 | |
| end = min(n, start + WINDOW - 1) | |
| tokens += tok("\n".join(lines[start - 1:end])) | |
| calls += 1 | |
| exact = tok("\n".join(lines[s - 1:e])) | |
| return {"name": name, "file": r, "file_lines": n, "def_lines": e - s + 1, "hits": len(hits), "calls": calls, "tokens": tokens, "exact_tokens": exact} | |
| def main(): | |
| global FIRST_READ | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("base"); ap.add_argument("head") | |
| ap.add_argument("--sample", type=int, default=3000); ap.add_argument("--seed", type=int, default=7) | |
| ap.add_argument("--first-read", type=int, default=FIRST_READ, help="lines in the first targeted read") | |
| ap.add_argument("--out", default=".") | |
| a = ap.parse_args() | |
| FIRST_READ = a.first_read | |
| tok = tokenizer() | |
| bi, bf = index(Path(a.base)); hi, hf = index(Path(a.head)) | |
| common = sorted(set(bi) & set(hi)) | |
| # weight the sample toward symbols that are actually looked up: public names, exclude dunders/tests | |
| common = [n for n in common if not n.startswith("__") and not n.startswith("test_")] | |
| random.Random(a.seed).shuffle(common) | |
| sample = common[: a.sample] | |
| B = [simulate(n, bi, bf, tok) for n in sample] | |
| H = [simulate(n, hi, hf, tok) for n in sample] | |
| pairs = [(b, h) for b, h in zip(B, H) if b and h] | |
| def agg(rows): | |
| c = [r["calls"] for r in rows]; t = [r["tokens"] for r in rows] | |
| return {"tasks": len(rows), "calls_mean": round(statistics.mean(c), 3), "calls_total": sum(c), "multi_window_tasks": sum(1 for x in c if x > 2), | |
| "tokens_mean": round(statistics.mean(t)), "tokens_p50": int(statistics.median(t)), "tokens_p90": sorted(t)[int(len(t) * .9)], "tokens_total": sum(t), | |
| "file_lines_p50": int(statistics.median(r["file_lines"] for r in rows)), "def_lines_p50": int(statistics.median(r["def_lines"] for r in rows)), | |
| "ambiguous_hits_mean": round(statistics.mean(r["hits"] for r in rows), 2), | |
| "exact_def_tokens_mean": round(statistics.mean(r["exact_tokens"] for r in rows)), "exact_def_tokens_total": sum(r["exact_tokens"] for r in rows)} | |
| res = {"common_symbols": len(common), "sampled": len(pairs), "seed": a.seed, "window": WINDOW, "first_read": FIRST_READ, | |
| "base": agg([b for b, _ in pairs]), "head": agg([h for _, h in pairs]), | |
| "head_cheaper": sum(1 for b, h in pairs if h["tokens"] < b["tokens"]), "head_costlier": sum(1 for b, h in pairs if h["tokens"] > b["tokens"]), "equal": sum(1 for b, h in pairs if h["tokens"] == b["tokens"]), | |
| "worst_regressions": sorted(({"name": h["name"], "base_tok": b["tokens"], "head_tok": h["tokens"], "head_file": h["file"]} for b, h in pairs), key=lambda x: x["base_tok"] - x["head_tok"])[:8], | |
| "best_wins": sorted(({"name": h["name"], "base_tok": b["tokens"], "head_tok": h["tokens"], "base_file": b["file"], "head_file": h["file"]} for b, h in pairs), key=lambda x: x["head_tok"] - x["base_tok"])[:8]} | |
| Path(a.out).mkdir(parents=True, exist_ok=True) | |
| (Path(a.out) / f"lookup_sim_w{FIRST_READ}.json").write_text(json.dumps(res, indent=1)) | |
| print(json.dumps({k: v for k, v in res.items() if k not in ("worst_regressions", "best_wins")}, indent=1)) | |
| print("best wins:", json.dumps(res["best_wins"][:4], indent=1)); print("worst regressions:", json.dumps(res["worst_regressions"][:4], indent=1)) | |
| if __name__ == "__main__": | |
| main() | |