feat: expand family suite and publish eval dashboard

This commit is contained in:
yaojingang
2026-03-31 21:42:39 +08:00
parent 51a479d12f
commit bcaddedb58
13 changed files with 5599 additions and 30 deletions
+4 -1
View File
@@ -1,6 +1,6 @@
PYTHON ?= python3
.PHONY: eval eval-suite failure-regression-check package-check package-failure-check snapshot-check validate lint test clean
.PHONY: eval eval-suite results-panel failure-regression-check package-check package-failure-check snapshot-check validate lint test clean
eval:
$(PYTHON) scripts/trigger_eval.py --description-file evals/improved_description.txt --cases evals/trigger_cases.json --baseline-description-file evals/baseline_description.txt
@@ -8,6 +8,9 @@ eval:
eval-suite:
$(PYTHON) scripts/run_eval_suite.py
results-panel:
$(PYTHON) scripts/render_eval_dashboard.py
failure-regression-check:
$(PYTHON) tests/verify_failure_regressions.py
+40 -3
View File
@@ -46,9 +46,46 @@ make test
## Results
- `make test` passes locally
- semantic trigger eval: `0` false positives, `0` false negatives on the current regression set
- eval suite: train / dev / holdout all pass, with family-based reporting
The homepage panel below is generated from the current eval suite so the family-level outcome is visible without opening raw JSON.
<!-- BEGIN:EVAL_RESULTS -->
- regression corpus: `66` prompts across `21` families
- aggregate result: `0` false positives, `0` false negatives, average precision `1.0`, average recall `1.0`
- suite status:
| Suite | Cases | FP | FN | Precision | Recall |
| --- | ---: | ---: | ---: | ---: | ---: |
| train | 31 | 0 | 0 | 1.0 | 1.0 |
| dev | 22 | 0 | 0 | 1.0 | 1.0 |
| holdout | 13 | 0 | 0 | 1.0 | 1.0 |
| Family | Cases | Pass Rate |
| --- | ---: | ---: |
| `brainstorm_only` | 2 | 1.0 |
| `brainstorm_vs_build` | 1 | 1.0 |
| `complex_multi_asset` | 3 | 1.0 |
| `document_export_vs_agent_skill` | 4 | 1.0 |
| `document_only` | 3 | 1.0 |
| `explain_not_package` | 1 | 1.0 |
| `explain_only` | 5 | 1.0 |
| `future_outline_vs_build` | 4 | 1.0 |
| `iterate_existing_skill` | 5 | 1.0 |
| `long_context_document_only` | 3 | 1.0 |
| `long_context_near_neighbor` | 3 | 1.0 |
| `long_context_summary_only` | 2 | 1.0 |
| `long_context_trigger` | 4 | 1.0 |
| `meta_skill_creation` | 1 | 1.0 |
| `one_off_vs_reusable` | 2 | 1.0 |
| `package_for_team` | 2 | 1.0 |
| `paraphrase_trigger` | 5 | 1.0 |
| `partial_scaffold_not_full_skill` | 4 | 1.0 |
| `summary_only` | 3 | 1.0 |
| `translate_only` | 4 | 1.0 |
| `workflow_to_skill` | 5 | 1.0 |
Full reports: [reports/eval_suite.json](reports/eval_suite.json) and [reports/family_summary.md](reports/family_summary.md)
<!-- END:EVAL_RESULTS -->
- packaging validation: `openai`, `claude`, and `generic` targets pass contract checks
- packaging failure fixtures: invalid metadata, invalid YAML, and unsupported targets fail as expected
- failure library regressions: anti-pattern families pass automated checks
+4
View File
@@ -12,6 +12,7 @@ Contents:
- `sample_trigger_report.json`: example comparison output using the current recommended threshold
- `failure-cases.md`: current weak spots and regression targets
- `packaging_expectations.json`: required packaging behaviors for supported targets
- `../reports/`: generated suite JSON plus the homepage-visible family summary panel source
Use:
@@ -19,6 +20,7 @@ Use:
python3 scripts/trigger_eval.py --description-file evals/improved_description.txt --cases evals/trigger_cases.json
python3 scripts/trigger_eval.py --description-file evals/improved_description.txt --cases evals/trigger_cases.json --baseline-description-file evals/baseline_description.txt
python3 scripts/run_eval_suite.py
python3 scripts/render_eval_dashboard.py
python3 tests/verify_failure_regressions.py
python3 scripts/cross_packager.py . --platform openai --platform claude --expectations evals/packaging_expectations.json --zip
python3 tests/verify_packager_failures.py
@@ -33,5 +35,7 @@ Regression scope now includes:
- mixed-intent negatives
- explicit "do not build a skill" negatives
- semantic exclusion cases such as one-off, document-only, and future-outline prompts
- paraphrase families that avoid the original wording while preserving the same trigger intent
- long-context contamination cases where build intent or no-build intent appears after unrelated setup text
- family-based reporting across workflow-to-skill, iterate-existing-skill, document-only, one-off, and future-outline cases
- holdout verification
+28
View File
@@ -29,6 +29,18 @@
{
"text": "Tighten the trigger boundary on this existing skill and prepare it for team distribution.",
"family": "iterate_existing_skill"
},
{
"text": "Turn the SOP, transcript, and cleanup notes into a maintained skill for the team library.",
"family": "paraphrase_trigger"
},
{
"text": "Codify this workflow into a shared capability package and validate the route boundary before distribution.",
"family": "paraphrase_trigger"
},
{
"text": "After the long handoff notes below, build a skill package for team reuse. The materials include a transcript, rough notes, and an onboarding flow, and the result should include references, scripts, and validation.",
"family": "long_context_trigger"
}
],
"should_not_trigger": [
@@ -51,6 +63,14 @@
{
"text": "Summarize this workflow and list the main points only.",
"family": "summary_only"
},
{
"text": "This is a long incident write-up. Keep it as documentation only, explain it in simpler language, and do not convert it into a reusable capability.",
"family": "long_context_document_only"
},
{
"text": "I pasted a very long process note. For now I only need a recap and a short summary, not a skill package.",
"family": "long_context_summary_only"
}
],
"near_neighbor": [
@@ -73,6 +93,14 @@
{
"text": "Polish these notes into a reusable-looking document without turning them into an agent skill.",
"family": "document_export_vs_agent_skill"
},
{
"text": "Turn the workflow below into a wiki-ready reference document and template only. We may build a skill later, but not now.",
"family": "long_context_near_neighbor"
},
{
"text": "Shape this idea for later and produce a future-ready outline, but do not build the skill package yet.",
"family": "future_outline_vs_build"
}
]
}
+20
View File
@@ -21,6 +21,14 @@
{
"text": "Turn this recurring onboarding process into a reusable skill library entry with validation notes.",
"family": "workflow_to_skill"
},
{
"text": "Codify this quarterly release routine into a reusable skill package with scripts, references, and packaging checks.",
"family": "paraphrase_trigger"
},
{
"text": "Below is a noisy transcript plus release notes and prompt history. Ignore the chatter and turn this playbook into an agent capability package for the team library.",
"family": "long_context_trigger"
}
],
"should_not_trigger": [
@@ -31,6 +39,14 @@
{
"text": "Explain this release checklist in simpler language; do not package it.",
"family": "explain_only"
},
{
"text": "Keep this release workflow as documentation only for the wiki and produce a reference document, not for agent execution.",
"family": "long_context_document_only"
},
{
"text": "Translate this long runbook into Russian and keep it as documentation only with no skill package.",
"family": "translate_only"
}
],
"near_neighbor": [
@@ -45,6 +61,10 @@
{
"text": "Turn this into a document package for the wiki, not an agent skill.",
"family": "document_export_vs_agent_skill"
},
{
"text": "From the long workflow below, create a template only and a checklist for discussion only. No skill package.",
"family": "long_context_near_neighbor"
}
]
}
File diff suppressed because it is too large Load Diff
+48 -8
View File
@@ -19,7 +19,13 @@
"meta skill",
"meta-skill",
"turn this into a skill",
"turn all of that into one reusable skill package"
"turn all of that into one reusable skill package",
"codify this workflow",
"codify this routine",
"operationalize this workflow as a reusable skill",
"formalize this process into a reusable capability",
"turn this playbook into an agent capability package",
"convert this into a maintained skill"
]
},
"transform_workflow": {
@@ -33,7 +39,15 @@
"operations checklist",
"internal process note",
"existing skill draft",
"rough notes"
"rough notes",
"standard operating procedure",
"sop",
"transcript",
"workflow fragments",
"release notes",
"onboarding flow",
"support escalation routine",
"quarterly release routine"
]
},
"reuse_package": {
@@ -47,7 +61,13 @@
"share it with the team",
"team reuse",
"internal workflows",
"production-ready"
"production-ready",
"library entry",
"shared capability",
"team library",
"ops library",
"maintained",
"operationalize"
]
},
"eval_optimize": {
@@ -60,7 +80,12 @@
"benchmark",
"validation",
"validate",
"packaging checks"
"packaging checks",
"false positives",
"false negatives",
"stress test",
"route boundary",
"hardening"
]
},
"iterate_existing_skill": {
@@ -73,7 +98,11 @@
"optimize skill triggering",
"package this skill",
"sharing it with the team",
"before sharing it with the team"
"before sharing it with the team",
"tighten the boundary",
"revise this skill",
"draft skill",
"harden this skill for library reuse"
]
},
"multi_asset": {
@@ -172,7 +201,12 @@
"not a skill yet",
"plain note",
"keep it as a plain note",
"no packaging needed"
"no packaging needed",
"not for agent execution",
"no skill package",
"do not convert it into a reusable capability",
"keep it as documentation only",
"discussion only"
]
},
"document_only": {
@@ -187,7 +221,10 @@
"article",
"blog title",
"headline",
"landing page"
"landing page",
"reference document",
"knowledge base",
"template only"
]
},
"future_outline": {
@@ -197,7 +234,10 @@
"future skill",
"possible future skill",
"before we decide whether to build",
"outline for a possible future skill"
"outline for a possible future skill",
"maybe later",
"for later",
"future-ready outline"
]
}
}
+36
View File
@@ -41,6 +41,22 @@
{
"text": "Standardize this recurring workflow as a shareable skill package with references.",
"family": "package_for_team"
},
{
"text": "Codify this recurring support escalation workflow into a maintained skill for the ops library.",
"family": "paraphrase_trigger"
},
{
"text": "Operationalize this workflow as a reusable skill with validation notes and team library metadata.",
"family": "paraphrase_trigger"
},
{
"text": "Below are release notes, rough notes, and a transcript from the last handoff. Ignore the noise and turn this runbook into a reusable agent skill with references and scripts so the team can reuse it every quarter.",
"family": "long_context_trigger"
},
{
"text": "We have a standard operating procedure, checklist, and prompt history scattered across docs. Formalize this process into a reusable capability, add validation, and package it for team reuse.",
"family": "long_context_trigger"
}
],
"should_not_trigger": [
@@ -75,6 +91,18 @@
{
"text": "Explain the difference between a runbook and a workflow.",
"family": "explain_only"
},
{
"text": "Below is a long SOP, but I only want a summary for leadership and a recap of the main points. Do not build anything reusable.",
"family": "long_context_summary_only"
},
{
"text": "Keep this as documentation only for the knowledge base and rewrite it as a reference document.",
"family": "long_context_document_only"
},
{
"text": "Translate this long onboarding flow into French and keep it as documentation only with no skill package.",
"family": "translate_only"
}
],
"near_neighbor": [
@@ -101,6 +129,14 @@
{
"text": "Create a reusable-looking checklist, but keep it as a plain note instead of a skill.",
"family": "partial_scaffold_not_full_skill"
},
{
"text": "We may convert this workflow later, but for now keep it as documentation only and produce a reference document.",
"family": "long_context_near_neighbor"
},
{
"text": "Draft a template only from this process and stop short of making a full skill package.",
"family": "partial_scaffold_not_full_skill"
}
]
}
+84
View File
@@ -73,6 +73,42 @@
{
"text": "Turn this recurring onboarding process into a reusable skill library entry with validation notes.",
"family": "workflow_to_skill"
},
{
"text": "Codify this recurring support escalation workflow into a maintained skill for the ops library.",
"family": "paraphrase_trigger"
},
{
"text": "Operationalize this workflow as a reusable skill with validation notes and team library metadata.",
"family": "paraphrase_trigger"
},
{
"text": "Turn the SOP, transcript, and cleanup notes into a maintained skill for the team library.",
"family": "paraphrase_trigger"
},
{
"text": "Codify this workflow into a shared capability package and validate the route boundary before distribution.",
"family": "paraphrase_trigger"
},
{
"text": "Codify this quarterly release routine into a reusable skill package with scripts, references, and packaging checks.",
"family": "paraphrase_trigger"
},
{
"text": "Below are release notes, rough notes, and a transcript from the last handoff. Ignore the noise and turn this runbook into a reusable agent skill with references and scripts so the team can reuse it every quarter.",
"family": "long_context_trigger"
},
{
"text": "We have a standard operating procedure, checklist, and prompt history scattered across docs. Formalize this process into a reusable capability, add validation, and package it for team reuse.",
"family": "long_context_trigger"
},
{
"text": "After the long handoff notes below, build a skill package for team reuse. The materials include a transcript, rough notes, and an onboarding flow, and the result should include references, scripts, and validation.",
"family": "long_context_trigger"
},
{
"text": "Below is a noisy transcript plus release notes and prompt history. Ignore the chatter and turn this playbook into an agent capability package for the team library.",
"family": "long_context_trigger"
}
],
"should_not_trigger": [
@@ -135,6 +171,34 @@
{
"text": "Explain this release checklist in simpler language; do not package it.",
"family": "explain_only"
},
{
"text": "Below is a long SOP, but I only want a summary for leadership and a recap of the main points. Do not build anything reusable.",
"family": "long_context_summary_only"
},
{
"text": "Keep this as documentation only for the knowledge base and rewrite it as a reference document.",
"family": "long_context_document_only"
},
{
"text": "Translate this long onboarding flow into French and keep it as documentation only with no skill package.",
"family": "translate_only"
},
{
"text": "This is a long incident write-up. Keep it as documentation only, explain it in simpler language, and do not convert it into a reusable capability.",
"family": "long_context_document_only"
},
{
"text": "I pasted a very long process note. For now I only need a recap and a short summary, not a skill package.",
"family": "long_context_summary_only"
},
{
"text": "Keep this release workflow as documentation only for the wiki and produce a reference document, not for agent execution.",
"family": "long_context_document_only"
},
{
"text": "Translate this long runbook into Russian and keep it as documentation only with no skill package.",
"family": "translate_only"
}
],
"near_neighbor": [
@@ -193,6 +257,26 @@
{
"text": "Turn this into a document package for the wiki, not an agent skill.",
"family": "document_export_vs_agent_skill"
},
{
"text": "We may convert this workflow later, but for now keep it as documentation only and produce a reference document.",
"family": "long_context_near_neighbor"
},
{
"text": "Draft a template only from this process and stop short of making a full skill package.",
"family": "partial_scaffold_not_full_skill"
},
{
"text": "Turn the workflow below into a wiki-ready reference document and template only. We may build a skill later, but not now.",
"family": "long_context_near_neighbor"
},
{
"text": "Shape this idea for later and produce a future-ready outline, but do not build the skill package yet.",
"family": "future_outline_vs_build"
},
{
"text": "From the long workflow below, create a template only and a checklist for discussion only. No skill package.",
"family": "long_context_near_neighbor"
}
]
}
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
# Eval Family Summary
- total cases: `66`
- families: `21`
- false positives: `0`
- false negatives: `0`
- average precision: `1.0`
- average recall: `1.0`
## Family Results
| Family | Cases | Passed | FP | FN | Pass Rate |
| --- | ---: | ---: | ---: | ---: | ---: |
| `brainstorm_only` | 2 | 2 | 0 | 0 | 1.0 |
| `brainstorm_vs_build` | 1 | 1 | 0 | 0 | 1.0 |
| `complex_multi_asset` | 3 | 3 | 0 | 0 | 1.0 |
| `document_export_vs_agent_skill` | 4 | 4 | 0 | 0 | 1.0 |
| `document_only` | 3 | 3 | 0 | 0 | 1.0 |
| `explain_not_package` | 1 | 1 | 0 | 0 | 1.0 |
| `explain_only` | 5 | 5 | 0 | 0 | 1.0 |
| `future_outline_vs_build` | 4 | 4 | 0 | 0 | 1.0 |
| `iterate_existing_skill` | 5 | 5 | 0 | 0 | 1.0 |
| `long_context_document_only` | 3 | 3 | 0 | 0 | 1.0 |
| `long_context_near_neighbor` | 3 | 3 | 0 | 0 | 1.0 |
| `long_context_summary_only` | 2 | 2 | 0 | 0 | 1.0 |
| `long_context_trigger` | 4 | 4 | 0 | 0 | 1.0 |
| `meta_skill_creation` | 1 | 1 | 0 | 0 | 1.0 |
| `one_off_vs_reusable` | 2 | 2 | 0 | 0 | 1.0 |
| `package_for_team` | 2 | 2 | 0 | 0 | 1.0 |
| `paraphrase_trigger` | 5 | 5 | 0 | 0 | 1.0 |
| `partial_scaffold_not_full_skill` | 4 | 4 | 0 | 0 | 1.0 |
| `summary_only` | 3 | 3 | 0 | 0 | 1.0 |
| `translate_only` | 4 | 4 | 0 | 0 | 1.0 |
| `workflow_to_skill` | 5 | 5 | 0 | 0 | 1.0 |
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
import argparse
import json
import subprocess
import sys
from pathlib import Path
BEGIN_MARKER = "<!-- BEGIN:EVAL_RESULTS -->"
END_MARKER = "<!-- END:EVAL_RESULTS -->"
def run_eval_suite(args: argparse.Namespace) -> dict:
proc = subprocess.run(
[
sys.executable,
"scripts/run_eval_suite.py",
"--eval-dir",
args.eval_dir,
"--description-file",
args.description_file,
"--baseline-description-file",
args.baseline_description_file,
],
capture_output=True,
text=True,
check=True,
)
return json.loads(proc.stdout)
def render_readme_panel(report: dict) -> str:
summary = report["summary"]
suites = report["suites"]
families = report["family_summary"]
lines = [
BEGIN_MARKER,
f"- regression corpus: `{summary['total_cases']}` prompts across `{summary['family_count']}` families",
f"- aggregate result: `{summary['false_positives']}` false positives, `{summary['false_negatives']}` false negatives, average precision `{summary['average_precision']}`, average recall `{summary['average_recall']}`",
"- suite status:",
"",
"| Suite | Cases | FP | FN | Precision | Recall |",
"| --- | ---: | ---: | ---: | ---: | ---: |",
]
for suite_name in ("train", "dev", "holdout"):
suite = suites[suite_name]
cases = sum(bucket["total"] for bucket in suite["bucket_stats"].values())
lines.append(
f"| {suite_name} | {cases} | {suite['false_positives']} | {suite['false_negatives']} | {suite['precision']} | {suite['recall']} |"
)
lines.extend(
[
"",
"| Family | Cases | Pass Rate |",
"| --- | ---: | ---: |",
]
)
for family, stats in sorted(families.items()):
lines.append(f"| `{family}` | {stats['total']} | {stats['pass_rate']} |")
lines.extend(
[
"",
"Full reports: [reports/eval_suite.json](reports/eval_suite.json) and [reports/family_summary.md](reports/family_summary.md)",
END_MARKER,
]
)
return "\n".join(lines)
def render_family_summary(report: dict) -> str:
summary = report["summary"]
lines = [
"# Eval Family Summary",
"",
f"- total cases: `{summary['total_cases']}`",
f"- families: `{summary['family_count']}`",
f"- false positives: `{summary['false_positives']}`",
f"- false negatives: `{summary['false_negatives']}`",
f"- average precision: `{summary['average_precision']}`",
f"- average recall: `{summary['average_recall']}`",
"",
"## Family Results",
"",
"| Family | Cases | Passed | FP | FN | Pass Rate |",
"| --- | ---: | ---: | ---: | ---: | ---: |",
]
for family, stats in sorted(report["family_summary"].items()):
lines.append(
f"| `{family}` | {stats['total']} | {stats['passed']} | {stats['false_positives']} | {stats['false_negatives']} | {stats['pass_rate']} |"
)
return "\n".join(lines) + "\n"
def update_readme(readme_path: Path, panel: str) -> None:
text = readme_path.read_text(encoding="utf-8")
if BEGIN_MARKER not in text or END_MARKER not in text:
raise SystemExit(f"README markers not found in {readme_path}")
start = text.index(BEGIN_MARKER)
end = text.index(END_MARKER) + len(END_MARKER)
updated = text[:start] + panel + text[end:]
readme_path.write_text(updated, encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser(description="Render README-visible eval results and full family report.")
parser.add_argument("--eval-dir", default="evals")
parser.add_argument("--description-file", default="evals/improved_description.txt")
parser.add_argument("--baseline-description-file", default="evals/baseline_description.txt")
parser.add_argument("--output-dir", default="reports")
parser.add_argument("--readme", default="README.md")
args = parser.parse_args()
report = run_eval_suite(args)
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
(output_dir / "eval_suite.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
(output_dir / "family_summary.md").write_text(render_family_summary(report), encoding="utf-8")
update_readme(Path(args.readme), render_readme_panel(report))
if __name__ == "__main__":
main()
+8 -1
View File
@@ -33,6 +33,7 @@ def main() -> None:
parser.add_argument("--eval-dir", default="evals", help="Root eval directory")
parser.add_argument("--description-file", default="evals/improved_description.txt")
parser.add_argument("--baseline-description-file", default="evals/baseline_description.txt")
parser.add_argument("--output-file", help="Optional path to write the combined JSON report")
args = parser.parse_args()
root = Path(args.eval_dir).resolve()
@@ -65,15 +66,21 @@ def main() -> None:
for family, stats in family_summary.items():
stats["pass_rate"] = round(stats["passed"] / stats["total"], 3) if stats["total"] else None
total_cases = sum(stats["total"] for stats in family_summary.values())
summary = {
"suite_count": len(suites),
"total_cases": total_cases,
"family_count": len(family_summary),
"false_positives": aggregate["false_positives"],
"false_negatives": aggregate["false_negatives"],
"average_precision": round(sum(aggregate["precision"]) / len(aggregate["precision"]), 3) if aggregate["precision"] else None,
"average_recall": round(sum(aggregate["recall"]) / len(aggregate["recall"]), 3) if aggregate["recall"] else None,
}
output = {"summary": summary, "family_summary": family_summary, "suites": suites}
print(json.dumps(output, ensure_ascii=False, indent=2))
rendered = json.dumps(output, ensure_ascii=False, indent=2)
if args.output_file:
Path(args.output_file).write_text(rendered + "\n", encoding="utf-8")
print(rendered)
if summary["false_positives"] > 0 or summary["false_negatives"] > 0:
raise SystemExit(2)