Files
skillfactor-pipeline/evals/skill-tiering/run_eval.py
skillfactor-pipeline 3b4a225594 feat(mapping): tiered relevance (core/adjacent) + source caps, eval 20/20
Deterministic tiers: core = token overlap with top-20 market hard skills or
essential ESCO competences; in-tier ranking by overlap score. Per-source cap
10 (overflow -> audit log, not the package). Source-domain allowlists stop
cross-contamination (marketing skill entering an engineering package via a
generic 'api' keyword). Flagship selection: 427 -> 73 entries; all 10 real
v1 flood negatives excluded, 10 must-have positives included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDKeXvpT6tENSvyQGLV1Uq
2026-07-09 06:45:38 +02:00

103 lines
3.8 KiB
Python

"""End-to-end eval for the tiered agent-skill mapping (phase 2c).
Runs the REAL p5 selection for the target occupation (catalog scan, rule +
auto-domain matching, tiering, per-source cap) and checks each labeled case:
include -> the skill must be in the final selection
exclude -> the skill must NOT be in the final selection
Ship criterion: >= 18/20.
Usage: python evals/skill-tiering/run_eval.py
"""
import json
import os
import sys
BASE = os.path.join(os.path.dirname(__file__), "..", "..")
sys.path.insert(0, os.path.join(BASE, "pipeline"))
import p5_enrich_ai_skills as p5 # noqa: E402
EVAL_SET = os.path.join(os.path.dirname(__file__), "eval-set.json")
def final_selection(slug):
"""Reproduce the p5 per-occupation selection (matching + tier + cap)."""
catalog = p5.load_catalog()
domain_index = {}
for (src_key, skill_key), entries in catalog.items():
if src_key not in p5.AUTO_SOURCES:
continue
text = (" ".join(e["name"] + " " + e["desc"] for e in entries).lower()
+ " " + skill_key.lower())
allowed = p5.SOURCE_DOMAINS.get(src_key)
for dom in p5.AUTO_DOMAINS:
if allowed is not None and dom["name"] not in allowed:
continue
if any(kw in text for kw in dom["skill_kw"]):
domain_index.setdefault(dom["name"], []).append((src_key, skill_key))
occ = p5.load_occupation(slug)
matched, seen = {}, set()
for rule in p5.RULES:
if not p5.rule_matches(rule, occ):
continue
for ref in rule["skills"]:
for entry in catalog.get(ref, []):
k = (ref[0], entry["name"])
if k not in seen:
seen.add(k)
matched.setdefault(ref[0], []).append(dict(entry))
for dom in p5.AUTO_DOMAINS:
if not p5.rule_matches(dom["occ"], occ):
continue
for (src_key, skill_key) in domain_index.get(dom["name"], ()):
for entry in catalog[(src_key, skill_key)]:
k = (src_key, entry["name"])
if k not in seen:
seen.add(k)
matched.setdefault(src_key, []).append(dict(entry))
core_signals = p5.load_core_signals(slug)
for src_key in list(matched):
entries = matched[src_key]
for e in entries:
e["tier"], e["score"] = p5.tier_for(e, core_signals)
entries.sort(key=lambda e: (0 if e["tier"] == "core" else 1,
-e["score"], e["name"]))
matched[src_key] = entries[:p5.MAX_PER_SOURCE]
return matched
def main():
data = json.load(open(EVAL_SET, encoding="utf-8"))
matched = final_selection(data["target_slug"])
included = {(src, e["name"].lower()) for src, es in matched.items() for e in es}
included_names = {}
for src, es in matched.items():
included_names[src] = [e["name"].lower() for e in es]
correct = 0
for case in data["cases"]:
src = case["source"]
if "skill" in case:
hit = (src, case["skill"].lower()) in included
else:
frag = case["skill_contains"].lower()
hit = any(frag in n for n in included_names.get(src, []))
want_included = case["expected"] == "include"
ok = (hit == want_included)
correct += ok
name = case.get("skill") or case["skill_contains"]
print(f"{'OK ' if ok else 'MISS'} {case['expected']:7} "
f"{'in-package' if hit else 'excluded '} {src}/{name}")
n = len(data["cases"])
print(f"\nscore: {correct}/{n} (ship threshold: {data['ship_threshold']})")
total = sum(len(v) for v in matched.values())
print(f"final selection size: {total} entries across {len(matched)} sources "
f"(v1 had 427)")
sys.exit(0 if correct >= data["ship_threshold"] else 1)
if __name__ == "__main__":
main()