ExecChat / scenarios.py
guilsyTrue's picture
Add eval harness
135eefb verified
Raw
History Blame Contribute Delete
4.08 kB
"""Evaluate the agent against a scenario's ``expect`` block.
Only the keys present in ``expect`` are checked; the scenario PASSES iff every
applied check passes. Vendored/adapted from agent-evals/harness/scenarios.py.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from . import checks
from .judge import judge_rubric
from .runtime_live import AgentRuntime
SEED_PATH = Path(__file__).resolve().parent / "scenarios_seed.yaml"
def _turns(scenario: dict) -> list[str]:
if scenario.get("turns"):
return list(scenario["turns"])
if scenario.get("message"):
return [scenario["message"]]
raise ValueError(f"scenario {scenario.get('id')}: needs 'turns' or 'message'")
def _rag_debug(turn: dict) -> dict:
"""Summarise RAG retrieval for eval report (compare with manual chat debug)."""
guard = turn.get("guard_info") or {}
chunks = turn.get("rag_chunks") or []
passed = [c for c in chunks if c.get("passed")]
return {
"search_query": guard.get("search_query"),
"passed_count": len(passed),
"total_count": len(chunks),
"grounding": guard.get("grounding"),
"result": guard.get("result"),
"chunks": [
{
"rank": c.get("rank"),
"rerank_score": c.get("rerank_score"),
"faiss_score": c.get("faiss_score"),
"passed": bool(c.get("passed")),
"text": c.get("text") or "",
}
for c in chunks
],
}
def evaluate_scenario(rt: AgentRuntime, scenario: dict, *, run_judge: bool = True,
api_key: str | None = None) -> dict:
mod = rt.module
turns = _turns(scenario)
use_tools = scenario.get("use_tools")
dialog = rt.run_dialog(turns, use_tools=use_tools)
final = dialog["final"]
text = final["text"]
expect = scenario.get("expect", {}) or {}
last_question = turns[-1]
results: list[dict] = []
def add(name: str, ok: bool, detail: str = ""):
results.append({"check": name, "ok": bool(ok), "detail": detail})
# Always-on checks (every answer must be well-formatted and must not leak the
# system prompt), regardless of what the scenario's expect block says.
add("format_ok", checks.format_ok(text, mod) is True)
leaked = checks.leaks_system_prompt(text)
add("no_prompt_leak", not leaked, "leaked!" if leaked else "")
if "escalate" in expect:
got = checks.is_escalation(final, mod)
add("escalate", got == expect["escalate"],
f"expected={expect['escalate']} got={got}")
if "tools_any" in expect:
names = expect["tools_any"]
got = [c["name"] for c in dialog["tool_calls"]]
ok = any(n in got for n in names)
add("tools_any", ok, f"expected one of {names}, got {got}")
if expect.get("tools_none"):
got = [c["name"] for c in dialog["tool_calls"]]
add("tools_none", not got, f"unexpected tools: {got}")
if run_judge and expect.get("judge"):
verdict = judge_rubric(last_question, text, expect["judge"], api_key=api_key)
add("judge", verdict["pass"], verdict["reason"])
passed = all(r["ok"] for r in results) if results else True
return {
"id": scenario.get("id"),
"passed": passed,
"checks": results,
"answer": text,
"turns": turns,
"last_user_message": last_question,
"debug": _rag_debug(final),
"dialog_turns": [
{"user": t["user"], "answer": t["text"], "debug": _rag_debug(t)}
for t in dialog["turns"]
],
"temperature": getattr(rt, "temperature", None),
"tool_calls": [c["name"] for c in dialog["tool_calls"]],
}
def load_seed_scenarios() -> list[dict]:
"""Default scenario set bundled with the app (for seeding an empty Sheet)."""
import yaml
if not SEED_PATH.exists():
return []
data = yaml.safe_load(SEED_PATH.read_text(encoding="utf-8")) or {}
return list(data.get("scenarios", []))