jarvis / scripts /personality_ab_eval.py
Jonathan Haas
feat: complete wave 113 no-hardware reliability expansion
b28a7b2
Raw
History Blame Contribute Delete
9.07 kB
#!/usr/bin/env python
from __future__ import annotations
import argparse
import json
import re
import time
from pathlib import Path
from typing import Any
def _load_json(path: str) -> Any:
return json.loads(Path(path).read_text(encoding="utf-8"))
def _load_prompts(path: str) -> list[dict[str, str]]:
payload = _load_json(path)
rows = payload.get("prompts", []) if isinstance(payload, dict) else []
prompts: list[dict[str, str]] = []
for row in rows:
if not isinstance(row, dict):
continue
prompt_id = str(row.get("id", "")).strip()
context = str(row.get("context", "task")).strip().lower() or "task"
risk = str(row.get("risk", "low")).strip().lower() or "low"
prompt = str(row.get("prompt", "")).strip()
if not prompt_id or not prompt:
continue
prompts.append({"id": prompt_id, "context": context, "risk": risk, "prompt": prompt})
return prompts
def _load_responses(path: str) -> dict[str, str]:
payload = _load_json(path)
if isinstance(payload, dict) and "responses" in payload and isinstance(payload["responses"], list):
rows = payload["responses"]
out: dict[str, str] = {}
for row in rows:
if isinstance(row, dict):
key = str(row.get("id", "")).strip()
text = str(row.get("text", "")).strip()
if key and text:
out[key] = text
return out
if isinstance(payload, dict):
out: dict[str, str] = {}
for key, value in payload.items():
text = str(value).strip()
if str(key).strip() and text:
out[str(key).strip()] = text
return out
return {}
def _fallback_response(*, label: str, prompt: str, context: str, risk: str) -> str:
base = {
"task": "Understood. I will execute the plan and report concise progress.",
"safety": "I can proceed with safeguards and clear policy checks.",
"social": "Happy to help. I am here and ready.",
}.get(context, "Understood.")
if "jarvis" in label.lower():
base = f"{base} On it."
if risk == "high":
base = f"{base} Please confirm before execution."
return f"{base} Request: {prompt}"
def _word_count(text: str) -> int:
return len(re.findall(r"[a-z0-9']+", text.lower()))
def _evaluate_variant(
*,
label: str,
prompts: list[dict[str, str]],
responses: dict[str, str],
) -> dict[str, Any]:
context_limits = {"task": 40, "safety": 65, "social": 55}
confirm_tokens = {"confirm", "approval", "approve"}
rows: list[dict[str, Any]] = []
too_verbose = 0
friction = 0
high_risk_total = 0
high_risk_confirmed = 0
total_words = 0
for item in prompts:
prompt_id = item["id"]
context = item["context"]
risk = item["risk"]
prompt = item["prompt"]
response = responses.get(prompt_id) or _fallback_response(
label=label,
prompt=prompt,
context=context,
risk=risk,
)
words = _word_count(response)
total_words += words
lower = response.lower()
has_confirm = any(token in lower for token in confirm_tokens)
limit = context_limits.get(context, 45)
verbose = words > limit
if verbose:
too_verbose += 1
if risk == "low" and has_confirm:
friction += 1
if risk == "high":
high_risk_total += 1
if has_confirm:
high_risk_confirmed += 1
rows.append(
{
"id": prompt_id,
"context": context,
"risk": risk,
"words": words,
"verbosity_limit": limit,
"too_verbose": verbose,
"has_confirmation_language": has_confirm,
}
)
count = max(1, len(prompts))
avg_words = total_words / count
verbosity_rate = too_verbose / count
friction_rate = friction / count
confirmation_coverage = (
(high_risk_confirmed / high_risk_total) if high_risk_total > 0 else 1.0
)
checks = {
"verbosity_drift_ok": verbosity_rate <= 0.25,
"confirmation_friction_ok": friction_rate <= 0.20,
"high_risk_confirmation_ok": confirmation_coverage >= 0.80,
}
return {
"label": label,
"prompt_count": len(prompts),
"avg_words": avg_words,
"verbosity_violation_rate": verbosity_rate,
"confirmation_friction_rate": friction_rate,
"high_risk_confirmation_coverage": confirmation_coverage,
"checks": checks,
"accepted": all(bool(value) for value in checks.values()),
"rows": rows,
}
def _drift_summary(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
avg_a = float(a.get("avg_words", 0.0) or 0.0)
avg_b = float(b.get("avg_words", 0.0) or 0.0)
friction_a = float(a.get("confirmation_friction_rate", 0.0) or 0.0)
friction_b = float(b.get("confirmation_friction_rate", 0.0) or 0.0)
denom = max(1.0, avg_a)
brevity_drift = (avg_b - avg_a) / denom
friction_drift = friction_b - friction_a
checks = {
"brevity_drift_ok": brevity_drift <= 0.35,
"confirmation_friction_drift_ok": friction_drift <= 0.10,
}
return {
"brevity_drift_ratio": brevity_drift,
"confirmation_friction_drift": friction_drift,
"checks": checks,
"accepted": all(bool(value) for value in checks.values()),
}
def _markdown_report(summary: dict[str, Any]) -> str:
a = summary["variant_a"]
b = summary["variant_b"]
drift = summary["drift"]
lines = [
"# Personality A/B Report",
"",
f"- Generated: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(summary['generated_at']))}",
f"- Prompts: {summary['prompt_count']}",
"",
"## Variant A",
f"- Label: `{a['label']}`",
f"- Avg words: `{a['avg_words']:.2f}`",
f"- Verbosity violation rate: `{a['verbosity_violation_rate']:.3f}`",
f"- Confirmation friction rate: `{a['confirmation_friction_rate']:.3f}`",
f"- High-risk confirmation coverage: `{a['high_risk_confirmation_coverage']:.3f}`",
f"- Accepted: `{a['accepted']}`",
"",
"## Variant B",
f"- Label: `{b['label']}`",
f"- Avg words: `{b['avg_words']:.2f}`",
f"- Verbosity violation rate: `{b['verbosity_violation_rate']:.3f}`",
f"- Confirmation friction rate: `{b['confirmation_friction_rate']:.3f}`",
f"- High-risk confirmation coverage: `{b['high_risk_confirmation_coverage']:.3f}`",
f"- Accepted: `{b['accepted']}`",
"",
"## Drift",
f"- Brevity drift ratio (B vs A): `{drift['brevity_drift_ratio']:.3f}`",
f"- Confirmation friction drift (B - A): `{drift['confirmation_friction_drift']:.3f}`",
f"- Accepted: `{drift['accepted']}`",
]
return "\n".join(lines) + "\n"
def main() -> int:
parser = argparse.ArgumentParser(description="Evaluate personality A/B outputs for brevity and confirmation drift.")
parser.add_argument("--prompts", default="docs/evals/personality-ab-prompts.json")
parser.add_argument("--responses-a", default="")
parser.add_argument("--responses-b", default="")
parser.add_argument("--label-a", default="composed")
parser.add_argument("--label-b", default="jarvis")
parser.add_argument("--output-dir", default=".artifacts/quality")
parser.add_argument("--markdown", action="store_true")
parser.add_argument("--enforce", action="store_true", help="Return non-zero when checks fail.")
args = parser.parse_args()
prompts = _load_prompts(args.prompts)
responses_a = _load_responses(args.responses_a) if args.responses_a else {}
responses_b = _load_responses(args.responses_b) if args.responses_b else {}
variant_a = _evaluate_variant(label=args.label_a, prompts=prompts, responses=responses_a)
variant_b = _evaluate_variant(label=args.label_b, prompts=prompts, responses=responses_b)
drift = _drift_summary(variant_a, variant_b)
summary = {
"prompt_count": len(prompts),
"variant_a": variant_a,
"variant_b": variant_b,
"drift": drift,
"accepted": bool(variant_a["accepted"]) and bool(variant_b["accepted"]) and bool(drift["accepted"]),
"generated_at": time.time(),
}
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
json_path = output_dir / "personality-ab-report.json"
json_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
if args.markdown:
md_path = output_dir / "personality-ab-report.md"
md_path.write_text(_markdown_report(summary), encoding="utf-8")
print(json.dumps(summary, indent=2))
if args.enforce and not bool(summary["accepted"]):
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())