wavereader / scripts /eval_agent.py
lucashudsn's picture
agent fully implemented, cleaned ui merged data etc
a1f52bf
Raw
History Blame Contribute Delete
5.2 kB
"""Agent eval: 10 golden questions x expected tool names (Worker C).
``--dry-run`` (default in CI): offline keyword router asserts each golden
question maps to its expected tools β€” no network, no LLM, no token.
Exit code 0 + "10/10 routed" on success.
Live mode (``--live``): runs :func:`wavereader.agent.run_stream` per
question with the per-turn budget guard (standard profile: 6 steps, 700
tokens, 2 score_week calls) and checks the expected tools were actually
called. Small token budget: 10 questions x ~1 short turn each β€” a few
cents on Nemotron 3 Ultra. Requires ``HF_TOKEN``.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
GOLDEN: list[tuple[str, list[str]]] = [
("When should I surf Bells Beach in Victoria this week?", ["get_spot_knowledge", "score_week"]),
("Where should I surf in Queensland as a beginner?", ["rank_region_week"]),
("Bells Beach or Winkipop on Saturday morning β€” which is better?", ["score_week", "find_best_windows"]),
("Why did Bells Beach get that score this morning? Break it down.", ["explain_score"]),
("Find beginner beach breaks near Byron Bay.", ["find_spots"]),
("I like Snapper Rocks β€” what similar spots are quieter?", ["find_similar_spots"]),
("What states and regions do you cover?", ["list_regions"]),
("Is Snapper Rocks good in autumn? How is the climate there?", ["get_climate_profile"]),
("What's the seafloor like at Bells Beach β€” reef or sandy shelf?", ["get_seafloor_profile"]),
("What wetsuit do I need at Bells Beach tomorrow, and when is sunrise?", ["get_session_brief"]),
]
# Keyword -> tool routing rules (mirrors the agent system prompt).
_RULES: list[tuple[str, tuple[str, ...]]] = [
("seafloor|reef|shelf|bathymetry|structure", ("get_seafloor_profile",)),
("wetsuit|sunrise|sunset|what do i wear|gear", ("get_session_brief",)),
("climate|autumn|season|best month|climatology", ("get_climate_profile",)),
("why|break it down|breakdown|explain.*score|component", ("explain_score",)),
("what states|what regions|which regions|do you cover|list.*region", ("list_regions",)),
("similar|like .* but|quieter|like\\ssnapper", ("find_similar_spots",)),
("\\bor\\b.*morning|\\bor\\b.*saturday|morning|weekend|saturday|sunday|best window", ("find_best_windows", "score_week")),
("where should i surf|where.*beginner|best.*in\\s", ("rank_region_week",)),
("find.*break|search|near\\s", ("find_spots",)),
("when should|bells beach|should i surf|this week|today", ("get_spot_knowledge", "score_week")),
]
def predict_tools(question: str) -> list[str]:
"""Deterministic keyword router: question -> predicted tool names."""
q = (question or "").lower()
for pattern, tools in _RULES:
if re.search(pattern, q):
return list(tools)
return ["find_spots"]
def run_dry_run() -> int:
"""Offline routing check: every golden question must predict its tools."""
print(f"{'question':60s} expected -> predicted ok?")
passed = 0
for question, expected in GOLDEN:
predicted = predict_tools(question)
ok = all(t in predicted for t in expected)
passed += ok
mark = "OK " if ok else "MISS"
print(f"[{mark}] {question[:58]:60s} {expected} -> {predicted}")
print(f"\ndry-run: {passed}/{len(GOLDEN)} routed")
return 0 if passed == len(GOLDEN) else 1
def run_live(max_steps: int = 6) -> int:
"""Live eval through the real agent (needs HF_TOKEN; small budget)."""
from wavereader.agent import run_stream
from wavereader.llm import has_token
if not has_token():
print("live eval needs HF_TOKEN (arg or env); aborting.")
return 2
passed = 0
for i, (question, expected) in enumerate(GOLDEN, 1):
called: list[str] = []
final = ""
usage: dict = {}
for event in run_stream(question, max_steps=max_steps):
if event.get("kind") == "tool_call":
called.append(str(event.get("name")))
elif event.get("kind") == "final":
final = str(event.get("text", ""))[:200]
elif event.get("kind") == "usage":
usage = event
ok = all(t in called for t in expected)
passed += ok
print(f"[{'OK ' if ok else 'MISS'}] Q{i}: expected={expected} called={called} usage={usage}")
print(f" final: {final!r}")
print(f"\nlive: {passed}/{len(GOLDEN)} passed")
return 0 if passed == len(GOLDEN) else 1
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="wavereader agent eval (10 goldens)")
parser.add_argument("--dry-run", action="store_true", help="offline routing check (default)")
parser.add_argument("--live", action="store_true", help="live run through the agent (needs HF_TOKEN)")
parser.add_argument("--max-steps", type=int, default=6)
args = parser.parse_args(argv)
if args.live:
return run_live(max_steps=args.max_steps)
return run_dry_run()
if __name__ == "__main__":
raise SystemExit(main())