Spaces:
Sleeping
Sleeping
File size: 7,782 Bytes
9bf4a3d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | """
Provider evaluation harness (spec #6, #7).
Reusable functions to run the EXACT production generation logic
(`ResumeCustomizer._generate_resume_v4`) once per provider on a shared set of
JDs, then collect comparable metrics. The scripts in scripts/ format the tables.
Model-independent: works with any provider (Claude / Kimi / NVIDIA) and with the
deterministic StubProvider for offline smoke runs.
"""
from __future__ import annotations
import os
import re
import json
import time
import statistics
from typing import List, Optional
from .resume_model import Resume
# ββ loaders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def load_base_resume() -> Optional[Resume]:
"""Load the candidate base resume from the parsed cache or the real PDF."""
cache = os.path.join("data", "resume", "_parsed.json")
if os.path.exists(cache):
try:
with open(cache, encoding="utf-8") as f:
data = json.load(f)
return Resume.from_dict(data.get("resume", data))
except Exception:
pass
pdf = os.path.join("data", "resume", "resume.pdf")
if os.path.exists(pdf):
from .resume_parser_v2 import parse_resume_pdf_cached
return parse_resume_pdf_cached(pdf)
return None
def load_jd_fixtures(limit: int = None) -> List[dict]:
"""Load JD fixtures as job dicts from tests/fixtures/jds/*.txt."""
jd_dir = os.path.join("tests", "fixtures", "jds")
jobs: List[dict] = []
if not os.path.isdir(jd_dir):
return jobs
for fn in sorted(os.listdir(jd_dir)):
if not fn.endswith(".txt") or fn.lower() == "readme.txt":
continue
path = os.path.join(jd_dir, fn)
try:
with open(path, encoding="utf-8") as f:
text = f.read()
except Exception:
continue
slug = os.path.splitext(fn)[0]
title, company = _title_company_from_slug(slug, text)
jobs.append({"title": title, "company": company, "description": text,
"ats_keywords": "", "_raw_assessment": {}, "_jd_slug": slug})
if limit:
jobs = jobs[:limit]
return jobs
def _title_company_from_slug(slug: str, text: str) -> tuple:
first = (text.strip().splitlines() or [""])[0][:60]
pretty = slug.replace("_", " ").title()
return (f"Product Manager ({pretty})", pretty)
# ββ single evaluation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _make_customizer(llm, base_text: str, output_dir: str):
from .resume_customizer import ResumeCustomizer
c = ResumeCustomizer.__new__(ResumeCustomizer)
c.llm = llm
c.resume_text = base_text
c.fast_model_cfg = None
c.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
return c
def evaluate_provider_on_job(provider, base_resume: Resume, job: dict,
output_dir: str, llm=None) -> dict:
"""Run one provider on one job through the production v4 path; return a row."""
c = _make_customizer(llm, base_resume.to_flat_text(), output_dir)
safe = re.sub(r'[\\/*?:"<>|]', "", f"{provider.name}_{job.get('_jd_slug', job.get('company',''))}")[:90]
filepath = os.path.join(output_dir, safe + ".docx")
row = {"provider": getattr(provider, "name", "?"),
"job": job.get("_jd_slug", job.get("company", "")),
"internal": 0, "independent": 0, "readability": 0,
"status": "", "repair_attempts": 0, "risk_terms": 0,
"schema_errors": 0, "provider_errors": 0, "download_allowed": False,
"runtime_s": 0.0}
t0 = time.time()
try:
path = c._generate_resume_v4(job, cfg=None, filepath=filepath,
provider=provider, base_resume_override=base_resume)
except Exception as e:
row["provider_errors"] = 1
row["status"] = f"ERROR:{str(e)[:40]}"
row["runtime_s"] = round(time.time() - t0, 1)
return row
row["runtime_s"] = round(time.time() - t0, 1)
report = job.get("_v2_report", {}) or {}
est = report.get("estimated_scores", {}) or {}
row["internal"] = est.get("jd_match", 0)
row["independent"] = report.get("independent_jd_match", 0)
row["readability"] = est.get("ats_readability", 0)
row["status"] = report.get("status", job.get("_v2_status", ""))
row["repair_attempts"] = len(report.get("repair_attempts", []) or [])
row["risk_terms"] = (len(report.get("review_terms_for_user_review", []) or [])
+ len(report.get("high_risk_terms_for_confirmation", []) or []))
row["download_allowed"] = bool(report.get("download_allowed"))
pq = report.get("provider_response_quality", "ok")
row["schema_errors"] = 0 if pq == "ok" else 1
return row
def run_provider_matrix(providers: List, base_resume: Resume, jobs: List[dict],
output_root: str, llm=None) -> List[dict]:
rows: List[dict] = []
for provider in providers:
out = os.path.join(output_root, getattr(provider, "name", "provider"))
for job in jobs:
rows.append(evaluate_provider_on_job(provider, base_resume, job, out, llm))
return rows
def summarize(rows: List[dict]) -> List[dict]:
"""Per-provider summary table (spec #6)."""
by_provider: dict = {}
for r in rows:
by_provider.setdefault(r["provider"], []).append(r)
out: List[dict] = []
for prov, rs in by_provider.items():
n = len(rs) or 1
ready = sum(1 for r in rs if r.get("download_allowed"))
out.append({
"provider": prov,
"ready_rate": round(100 * ready / n),
"avg_independent": round(statistics.mean(r["independent"] for r in rs), 1),
"avg_internal": round(statistics.mean(r["internal"] for r in rs), 1),
"avg_repairs": round(statistics.mean(r["repair_attempts"] for r in rs), 1),
"schema_error_rate": round(100 * sum(r["schema_errors"] for r in rs) / n),
"provider_error_rate": round(100 * sum(r["provider_errors"] for r in rs) / n),
"risk_overuse": round(statistics.mean(r["risk_terms"] for r in rs), 1),
"avg_runtime_s": round(statistics.mean(r["runtime_s"] for r in rs), 1),
"recommendation": "preferred" if (ready / n) >= 0.8 else (
"usable" if (ready / n) >= 0.5 else "deprioritize"),
})
out.sort(key=lambda x: (x["ready_rate"], x["avg_independent"]), reverse=True)
return out
# ββ consistency (spec #7) ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def consistency_runs(provider, base_resume: Resume, job: dict,
output_dir: str, n: int = 3, llm=None) -> dict:
"""Run the same provider on the same JD n times; report variance + stability."""
scores: List[int] = []
for i in range(n):
out = os.path.join(output_dir, f"run{i+1}")
row = evaluate_provider_on_job(provider, base_resume, dict(job), out, llm)
scores.append(row["independent"])
variance = round(statistics.pvariance(scores), 1) if len(scores) > 1 else 0.0
spread = max(scores) - min(scores) if scores else 0
return {"provider": getattr(provider, "name", "?"),
"job": job.get("_jd_slug", job.get("company", "")),
"scores": scores, "variance": variance, "spread": spread,
"stable": spread <= 5}
|