feat: strengthen eval coverage and parsing reliability

This commit is contained in:
yaojingang
2026-03-31 20:54:06 +08:00
parent b6cc9ca8b9
commit 170c1ad70c
9 changed files with 210 additions and 72 deletions
+31 -48
View File
@@ -4,53 +4,23 @@ import json
import shutil
import zipfile
from pathlib import Path
import yaml
def read_simple_yaml(path: Path) -> dict:
lines = path.read_text(encoding="utf-8").splitlines()
data: dict = {}
stack: list[tuple[int, dict]] = [(0, data)]
for raw_line in lines:
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip(" "))
line = raw_line.strip()
while len(stack) > 1 and indent <= stack[-1][0]:
stack.pop()
parent = stack[-1][1]
if line.startswith("- "):
item = line[2:].strip().strip("'\"")
existing = parent.setdefault("__list__", [])
existing.append(item)
continue
if ":" not in line:
continue
key, value = line.split(":", 1)
key = key.strip()
value = value.strip()
if value == "":
child: dict = {}
parent[key] = child
stack.append((indent, child))
else:
parent[key] = value.strip("'\"")
return data
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
def read_frontmatter(skill_md: Path) -> dict:
if not skill_md.exists():
raise FileNotFoundError(f"Missing required file: {skill_md}")
text = skill_md.read_text(encoding="utf-8")
if not text.startswith("---"):
return {}
parts = text.split("---", 2)
if len(parts) < 3:
return {}
data = {}
for line in parts[1].splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip().strip("'\"")
return data
return yaml.safe_load(parts[1]) or {}
def read_interface(skill_dir: Path) -> dict:
@@ -58,18 +28,22 @@ def read_interface(skill_dir: Path) -> dict:
if not path.exists():
return {}
raw = read_simple_yaml(path)
compatibility = raw.get("compatibility", {})
targets = compatibility.get("adapter_targets", {})
if isinstance(targets, dict) and "__list__" in targets:
compatibility["adapter_targets"] = targets["__list__"]
raw["compatibility"] = compatibility
return raw
def require_fields(payload: dict, fields: list[str], label: str) -> None:
missing = [field for field in fields if not payload.get(field)]
if missing:
raise ValueError(f"Missing required {label} fields: {', '.join(missing)}")
def build_manifest(skill_dir: Path, platform: str) -> dict:
frontmatter = read_frontmatter(skill_dir / "SKILL.md")
interface = read_interface(skill_dir).get("interface", {})
compatibility = read_interface(skill_dir).get("compatibility", {})
interface_doc = read_interface(skill_dir)
interface = interface_doc.get("interface", {})
compatibility = interface_doc.get("compatibility", {})
require_fields(frontmatter, ["name", "description"], "frontmatter")
require_fields(interface, ["display_name", "short_description", "default_prompt"], "interface")
return {
"name": frontmatter.get("name", skill_dir.name),
"description": frontmatter.get("description", ""),
@@ -127,6 +101,8 @@ def write_yaml_like(path: Path, payload: dict) -> None:
def write_adapter(skill_dir: Path, out_dir: Path, platform: str) -> Path:
target_dir = out_dir / "targets" / platform
target_dir.mkdir(parents=True, exist_ok=True)
if platform not in PLATFORM_CONTRACTS:
raise ValueError(f"Unsupported platform: {platform}")
payload = build_manifest(skill_dir, platform)
if platform == "openai":
meta_dir = target_dir / "agents"
@@ -223,13 +199,17 @@ def main() -> None:
manifest = copy_manifest(skill_dir, out_dir)
generated = [str(manifest)]
for platform in (args.platform or ["generic"]):
generated.append(str(write_adapter(skill_dir, out_dir, platform)))
if args.zip:
generated.append(str(make_zip(skill_dir, out_dir)))
failures = []
try:
for platform in (args.platform or ["generic"]):
generated.append(str(write_adapter(skill_dir, out_dir, platform)))
if args.zip:
generated.append(str(make_zip(skill_dir, out_dir)))
except (FileNotFoundError, ValueError, yaml.YAMLError) as exc:
failures.append(str(exc))
expectations = load_expectations(Path(args.expectations).resolve()) if args.expectations else {}
validation = validate_exports(out_dir, expectations) if expectations else None
validation = validate_exports(out_dir, expectations) if expectations and not failures else None
report = {
"output_dir": str(out_dir),
"generated": generated,
@@ -238,10 +218,13 @@ def main() -> None:
"failure_handling": {
"missing_required_file": "exit with code 2 when expectations are provided and validation fails",
"missing_required_field": "exit with code 2 when expectations are provided and validation fails",
"invalid_yaml_or_frontmatter": "exit with code 2 when parsing fails",
"unsupported_platform": "exit with code 2 when the platform is not defined in PLATFORM_CONTRACTS",
},
"failures": failures,
}
print(json.dumps(report, ensure_ascii=False, indent=2))
if validation and not validation["ok"]:
if failures or (validation and not validation["ok"]):
raise SystemExit(2)
+64 -5
View File
@@ -3,6 +3,7 @@ import argparse
import json
import re
from pathlib import Path
from collections import Counter
WORD_RE = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_-]*")
@@ -37,12 +38,67 @@ def score_prompt(description_words: set[str], prompt: str) -> float:
return len(overlap) / len(prompt_words)
def token_frequencies(cases: dict, buckets: tuple[str, ...]) -> Counter:
freq: Counter = Counter()
for bucket in buckets:
for prompt in cases.get(bucket, []):
freq.update(words(prompt))
return freq
def compile_negative_patterns(cases: dict) -> list[re.Pattern[str]]:
return [re.compile(pattern, re.IGNORECASE) for pattern in cases.get("negative_patterns", [])]
def score_prompt_weighted(description_words: set[str], prompt: str, positive_freq: Counter, negative_freq: Counter, negative_patterns: list[re.Pattern[str]]) -> tuple[float, dict]:
prompt_words = words(prompt)
if not prompt_words:
return 0.0, {"matched_positive_tokens": [], "matched_negative_tokens": [], "matched_negative_patterns": []}
overlap = description_words & prompt_words
base_score = len(overlap) / len(prompt_words)
weighted_bonus = 0.0
matched_positive_tokens = []
matched_negative_tokens = []
for token in overlap:
pos = positive_freq.get(token, 0)
neg = negative_freq.get(token, 0)
if pos > neg:
weighted_bonus += 0.06
matched_positive_tokens.append(token)
weighted_penalty = 0.0
for token in prompt_words:
neg = negative_freq.get(token, 0)
pos = positive_freq.get(token, 0)
if neg > pos and token not in overlap:
weighted_penalty += 0.04
matched_negative_tokens.append(token)
matched_negative_patterns = [pattern.pattern for pattern in negative_patterns if pattern.search(prompt)]
pattern_penalty = 0.18 * len(matched_negative_patterns)
score = max(0.0, min(1.0, base_score + weighted_bonus - weighted_penalty - pattern_penalty))
return score, {
"matched_positive_tokens": sorted(set(matched_positive_tokens)),
"matched_negative_tokens": sorted(set(matched_negative_tokens)),
"matched_negative_patterns": matched_negative_patterns,
"base_score": round(base_score, 3),
"weighted_bonus": round(weighted_bonus, 3),
"weighted_penalty": round(weighted_penalty + pattern_penalty, 3),
}
def classify_bucket(bucket: str) -> bool:
return bucket == "should_trigger"
def evaluate(description: str, cases: dict, threshold: float) -> dict:
desc_words = words(description)
positive_freq = token_frequencies(cases, ("should_trigger",))
negative_freq = token_frequencies(cases, ("should_not_trigger", "near_neighbor"))
negative_patterns = compile_negative_patterns(cases)
results = {"should_trigger": [], "should_not_trigger": [], "near_neighbor": []}
fp = 0
fn = 0
@@ -54,7 +110,7 @@ def evaluate(description: str, cases: dict, threshold: float) -> dict:
total = 0
passed_count = 0
for prompt in cases.get(bucket, []):
score = score_prompt(desc_words, prompt)
score, score_detail = score_prompt_weighted(desc_words, prompt, positive_freq, negative_freq, negative_patterns)
predicted = score >= threshold
passed = predicted == expected
total += 1
@@ -70,6 +126,7 @@ def evaluate(description: str, cases: dict, threshold: float) -> dict:
"predicted_trigger": predicted,
"expected_trigger": expected,
"passed": passed,
"score_detail": score_detail,
}
if 0.75 * threshold <= score <= 1.25 * threshold:
record["boundary_case"] = True
@@ -81,6 +138,7 @@ def evaluate(description: str, cases: dict, threshold: float) -> dict:
"prompt": prompt,
"score": round(score, 3),
"reason": "false_negative" if expected else "false_positive",
"matched_negative_patterns": score_detail["matched_negative_patterns"],
}
)
bucket_stats[bucket] = {
@@ -95,7 +153,7 @@ def evaluate(description: str, cases: dict, threshold: float) -> dict:
return {
"threshold": threshold,
"threshold_explanation": "Prompts at or above the threshold are treated as trigger matches. Scores near the threshold should be reviewed as boundary cases.",
"threshold_explanation": "Prompts at or above the threshold are treated as trigger matches. Final scores combine token overlap, positive-token bonuses, negative-token penalties, and explicit negative-pattern penalties. Scores near the threshold should be reviewed as boundary cases.",
"false_positives": fp,
"false_negatives": fn,
"precision": round(precision, 3) if precision is not None else None,
@@ -128,7 +186,7 @@ def main() -> None:
parser.add_argument("--baseline-description", help="Baseline description string to compare against")
parser.add_argument("--baseline-description-file", help="Read baseline description from file")
parser.add_argument("--cases", required=True, help="JSON file with should_trigger and should_not_trigger arrays")
parser.add_argument("--threshold", type=float, default=0.18, help="Token overlap threshold")
parser.add_argument("--threshold", type=float, default=None, help="Trigger threshold override")
args = parser.parse_args()
description = args.description
@@ -138,13 +196,14 @@ def main() -> None:
raise SystemExit("Provide --description or --description-file")
cases = load_cases(Path(args.cases))
report = evaluate(description, cases, args.threshold)
threshold = args.threshold if args.threshold is not None else cases.get("recommended_threshold", 0.35)
report = evaluate(description, cases, threshold)
baseline = args.baseline_description
if args.baseline_description_file:
baseline = extract_description(Path(args.baseline_description_file).read_text(encoding="utf-8"))
if baseline:
report["comparison"] = compare_reports(evaluate(baseline, cases, args.threshold), report)
report["comparison"] = compare_reports(evaluate(baseline, cases, threshold), report)
print(json.dumps(report, ensure_ascii=False, indent=2))
if report["false_positives"] > 2: