cuivienen's picture
Deploy Synderesis API Docker Space
10ff0f9 verified
Raw
History Blame Contribute Delete
4.99 kB
#!/usr/bin/env python3
"""Run offline golden evals for the Customer Agent domain layer."""
from __future__ import annotations
import argparse
import importlib.util
import json
import sys
from pathlib import Path
from types import ModuleType
def load_customer_agent() -> ModuleType:
path = Path(__file__).resolve().parents[2] / "scripts" / "customer_agent.py"
spec = importlib.util.spec_from_file_location("customer_agent_eval", path)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load {path}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def fake_agent(ca: ModuleType, data: dict) -> object:
return ca.CustomerAgentRecord(
id="eval",
customer_id="cust",
name=str(data.get("name") or "Shop"),
greeting="",
voice_instructions="",
escalate_email=str(data.get("escalate_email") or ""),
escalate_url="",
escalate_webhook_url=str(data.get("escalate_webhook_url") or ""),
allowed_origins=list(data.get("allowed_origins") or []),
site_key_prefix="pk_live_eval",
status="active",
created_at="",
updated_at="",
)
def run_case(ca: ModuleType, knowledge_chunks: list[dict], case: dict) -> tuple[bool, str]:
case_type = case.get("type")
case_id = case.get("id", "?")
if case_type == "retrieval":
hits = ca.retrieve_chunks(knowledge_chunks, str(case["question"]))
hit = ca.knowledge_is_hit(hits)
if bool(case.get("expect_knowledge_hit")) != hit:
return False, f"{case_id}: knowledge_hit={hit} expected {case.get('expect_knowledge_hit')}"
needle = str(case.get("expect_title_contains") or "").casefold()
if needle:
title = str(hits[0].get("title") or "").casefold() if hits else ""
if needle not in title and needle not in str(hits[0].get("text") or "").casefold():
return False, f"{case_id}: top hit missing '{needle}' (got title={title!r})"
return True, case_id
if case_type == "guardrail":
q = str(case["question"])
hard = ca.looks_hard_refuse(q)
sensitive = ca.looks_sensitive(q)
if "expect_hard_refuse" in case and bool(case["expect_hard_refuse"]) != hard:
return False, f"{case_id}: hard_refuse={hard}"
if "expect_sensitive" in case and bool(case["expect_sensitive"]) != sensitive:
return False, f"{case_id}: sensitive={sensitive}"
answer = ca.hard_refuse_answer("help@example.com") if hard else "I don't know that from our FAQ."
escalate = ca.should_recommend_escalation(q, answer, knowledge_hit=False)
if "expect_escalate" in case and bool(case["expect_escalate"]) != escalate:
return False, f"{case_id}: escalate={escalate}"
return True, case_id
if case_type == "origin":
allowed = list(case.get("allowed") or [])
origin = case.get("request_origin")
ok = ca.origin_allowed(allowed, origin)
if bool(case.get("expect_allowed")) != ok:
return False, f"{case_id}: allowed={ok}"
return True, case_id
if case_type == "readiness":
agent_data = dict(case.get("agent") or {})
knowledge_count = int(agent_data.pop("knowledge_count", 0))
agent = fake_agent(ca, agent_data)
report = ca.readiness_checklist(agent, knowledge_count=knowledge_count)
ready = bool(report.get("ready_for_production"))
if bool(case.get("expect_ready_for_production")) != ready:
return False, f"{case_id}: ready_for_production={ready}"
return True, case_id
return False, f"{case_id}: unknown type {case_type!r}"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--cases",
default=str(Path(__file__).resolve().parents[1] / "evals" / "golden_cases.json"),
)
args = parser.parse_args(argv)
cases_path = Path(args.cases)
payload = json.loads(cases_path.read_text(encoding="utf-8"))
ca = load_customer_agent()
knowledge = payload.get("knowledge") or []
chunks = [{"title": item["title"], "text": item["text"]} for item in knowledge]
passed = 0
failed: list[str] = []
for case in payload.get("cases") or []:
ok, detail = run_case(ca, chunks, case)
if ok:
passed += 1
print(f"PASS {detail}")
else:
failed.append(detail)
print(f"FAIL {detail}")
total = passed + len(failed)
rate = (passed / total) if total else 0.0
print(f"\n{passed}/{total} passed ({rate:.0%})")
if failed:
print("Failures:")
for item in failed:
print(f" - {item}")
return 1
if rate < 1.0:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())