Spaces:
Runtime error
Runtime error
| """ | |
| lib/candidate_profile.py — V6 Canonical Candidate Profile | |
| Intermediate representation between raw JSON and features. | |
| Every downstream module reads this object instead of raw candidate dict. | |
| This makes the system: | |
| - Cleaner: one place to compute derived signals | |
| - Reusable: same profile works for any JD | |
| - Testable: profile creation is isolated from feature extraction | |
| - Consistent: no duplicate computation across features | |
| Structure: | |
| Experience: years, domain depth, pre-LLM experience | |
| Leadership: seniority trajectory, management signals | |
| Ownership: level, best verb, per-role ownership | |
| Production: readiness, categories, deployment evidence | |
| Scale: user scale, QPS, data scale | |
| Impact: quantified metrics, best strength | |
| Domain: skill map, expertise level, primary domain | |
| Career: coherence, trajectory, company tier trend | |
| Behaviour: availability, responsiveness, recency, trust | |
| Safety: honeypot, disqualifiers, penalties | |
| Education: tier, relevance | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Any | |
| from lib import schema, title_scoring, company_tier, honeypot | |
| from lib.career_narrative import analyze as analyze_narrative | |
| from lib.evidence import extract_all_evidence, get_evidence_summary, Evidence | |
| class ImpactMetric: | |
| """A single quantified impact metric.""" | |
| metric_type: str # ndcg, latency, recall, users, qps, etc. | |
| value: str # "15%", "200ms to 45ms", "50M" | |
| strength: float # 0-1 | |
| company: str = "" | |
| year_range: str = "" | |
| class RoleSnapshot: | |
| """Summary of a single role.""" | |
| company: str | |
| title: str | |
| seniority: float # 0-1 from title_scoring | |
| company_tier: float # 0-1 from company_tier | |
| ownership: float # 0-1 from evidence | |
| has_production: bool | |
| has_quantified: bool | |
| duration_months: float | |
| domain: str # broad domain classification | |
| start_year: int = 0 | |
| end_year: int = 0 | |
| class CanonicalProfile: | |
| """Universal Canonical Candidate Profile. | |
| This is the SINGLE intermediate representation that all features | |
| and scoring read from. No module should access raw JSON directly | |
| after this point (except for evidence extraction which needs | |
| raw text). | |
| """ | |
| # === Identity === | |
| candidate_id: str = "" | |
| current_title: str = "" | |
| current_company: str = "" | |
| headline: str = "" | |
| # === Experience === | |
| yoe: float = 0.0 | |
| yoe_in_domain: float = 0.0 # months in JD-relevant domain | |
| yoe_domain_ratio: float = 0.0 # fraction of career in domain | |
| pre_llm_months: float = 0.0 # pre-2022 IR experience (months) | |
| num_roles: int = 0 | |
| role_snapshots: list[RoleSnapshot] = field(default_factory=list) | |
| # === Leadership === | |
| max_seniority: float = 0.0 | |
| has_managed: bool = False | |
| management_signals: list[str] = field(default_factory=list) | |
| # === Ownership === | |
| ownership_level: float = 0.0 # 0-1 | |
| best_ownership_verb: str = "" | |
| ownership_by_role: list[tuple[str, float]] = field(default_factory=list) | |
| # === Production === | |
| production_readiness: float = 0.0 # 0-1 | |
| has_production_evidence: bool = False | |
| production_categories: list[str] = field(default_factory=list) | |
| # === Scale === | |
| max_user_scale: float = 0.0 | |
| max_qps: float = 0.0 | |
| data_scale: float = 0.0 | |
| # === Impact === | |
| has_quantified_impact: bool = False | |
| impact_metrics: list[ImpactMetric] = field(default_factory=list) | |
| best_impact_strength: float = 0.0 | |
| impact_count: int = 0 | |
| # === Domain === | |
| domain_skills: dict[str, float] = field(default_factory=dict) | |
| domain_expertise: float = 0.0 # 0-1 | |
| primary_domain: str = "unknown" | |
| # === Career === | |
| career_coherence: float = 0.5 | |
| career_trajectory: float = 0.5 | |
| company_tier_avg: float = 0.5 | |
| company_tier_trajectory: list[float] = field(default_factory=list) | |
| avg_tenure_months: float = 0.0 | |
| narrative_type: str = "stable" | |
| narrative_suspicious: list[str] = field(default_factory=list) | |
| # === Behaviour === | |
| availability: float = 0.5 | |
| responsiveness: float = 0.5 | |
| recency: float = 0.5 | |
| platform_trust: float = 0.5 | |
| market_demand: float = 0.0 | |
| github_activity: float = 0.0 | |
| interview_completion: float = 0.0 | |
| # === Resume Quality === | |
| quantified_outcomes: float = 0.0 | |
| truthiness: float = 0.5 | |
| keyword_stuffing_risk: float = 0.0 | |
| profile_completeness: float = 0.0 | |
| # === Safety === | |
| is_honeypot: bool = False | |
| honeypot_reasons: list[str] = field(default_factory=list) | |
| disqualifier_penalty: float = 1.0 | |
| disqualifier_reasons: list[str] = field(default_factory=list) | |
| # === Education === | |
| education_tier: str = "unknown" # tier_1, tier_2, unknown | |
| has_relevant_degree: bool = False | |
| education_field: str = "" | |
| # === Evidence (top-N for reasoning) === | |
| top_evidence: list[Evidence] = field(default_factory=list) | |
| evidence_summary: dict = field(default_factory=dict) | |
| # === Location === | |
| location: str = "" | |
| country: str = "" | |
| willing_to_relocate: bool = False | |
| preferred_work_mode: str = "" | |
| # === Raw refs (for reasoning, not for scoring) === | |
| _raw: dict = field(default_factory=dict) | |
| def build(c: dict) -> CanonicalProfile: | |
| """ | |
| Build a Canonical Candidate Profile from raw candidate JSON. | |
| This is the SINGLE place where raw JSON is accessed. | |
| All downstream modules read the CanonicalProfile. | |
| """ | |
| from lib.jd_parser import get_jd | |
| from lib.career_narrative import analyze as analyze_narrative | |
| jd = get_jd() | |
| p = schema.profile(c) | |
| ch = schema.career_history(c) | |
| sigs = schema.signals(c) | |
| edu = schema.education(c) | |
| text = schema.unified_text_blob(c) | |
| profile = CanonicalProfile() | |
| # === Identity === | |
| profile.candidate_id = c.get("candidate_id", "") | |
| profile.current_title = schema.current_title(c) | |
| profile.current_company = schema.current_company(c) | |
| profile.headline = p.get("headline", "") | |
| # === Experience === | |
| profile.yoe = schema.years_of_experience(c) | |
| profile.num_roles = len(ch) | |
| # Role snapshots | |
| relevant_kw = set() | |
| for skills in jd.required_skills.values(): | |
| relevant_kw.update(skills) | |
| for skills in jd.preferred_skills.values(): | |
| relevant_kw.update(skills) | |
| relevant_kw.update(jd.pre_llm_keywords) | |
| total_months = 0 | |
| relevant_months = 0 | |
| pre_llm_mos = 0.0 | |
| for role in ch: | |
| sd = schema.parse_date(role.get("start_date")) | |
| ed = schema.parse_date(role.get("end_date")) or __import__("lib.constants", fromlist=["REFERENCE_DATE"]).REFERENCE_DATE | |
| dur = role.get("duration_months") or 0 | |
| total_months += dur | |
| sr, _ = title_scoring.seniority_score(role.get("title", "")) | |
| ct = company_tier.company_quality_score(role.get("company", "")) | |
| role_text = f"{role.get('title','')} {role.get('description','')}".lower() | |
| is_relevant = any(kw in role_text for kw in relevant_kw) | |
| if is_relevant: | |
| relevant_months += dur | |
| # Pre-LLM | |
| if sd and sd.year < jd.pre_llm_cutoff_year: | |
| if not any(m in role_text for m in jd.post_llm_markers): | |
| hits = [kw for kw in jd.pre_llm_keywords if kw in role_text] | |
| if hits: | |
| pre_llm_mos += dur | |
| # Ownership for this role | |
| own_level = 0.0 | |
| from lib.evidence import _OWNERSHIP_RE, _OWNERSHIP_TIERS | |
| m = _OWNERSHIP_RE.search(role_text) | |
| if m: | |
| own_level = _OWNERSHIP_TIERS.get(m.group(1).lower(), 0.15) | |
| # Production evidence for this role | |
| prod_evidence = [p for p in (role.get("description") or "").lower().split() | |
| if p in ["production", "deployed", "shipped", "launched", "live"]] | |
| # Quantified impact for this role | |
| import re | |
| has_quant = bool(re.search( | |
| r'(\d+(?:\.\d+)?)\s*%|(\d+(?:\.\d+)?)\s*million|p99|ndcg|qps|rps', | |
| role_text, re.IGNORECASE | |
| )) | |
| from lib.career_narrative import _get_role_domain | |
| snap = RoleSnapshot( | |
| company=role.get("company", ""), | |
| title=role.get("title", ""), | |
| seniority=sr, | |
| company_tier=ct, | |
| ownership=own_level, | |
| has_production=bool(prod_evidence), | |
| has_quantified=has_quant, | |
| duration_months=dur, | |
| domain=_get_role_domain(role), | |
| start_year=sd.year if sd else 0, | |
| end_year=ed.year if ed else 0, | |
| ) | |
| profile.role_snapshots.append(snap) | |
| profile.ownership_by_role.append((role.get("company", ""), own_level)) | |
| profile.yoe_in_domain = relevant_months | |
| profile.yoe_domain_ratio = relevant_months / total_months if total_months > 0 else 0 | |
| profile.pre_llm_months = pre_llm_mos | |
| # === Leadership === | |
| profile.max_seniority = max( | |
| (s.seniority for s in profile.role_snapshots), default=0 | |
| ) | |
| profile.has_managed = bool( | |
| re.search(r"manag|lead\s+a\s+team|team\s+of|mentor|supervis", | |
| text, re.IGNORECASE) | |
| ) | |
| profile.management_signals = [ | |
| kw for kw in ["managed", "led a team", "mentored", "supervised"] | |
| if kw in text | |
| ] | |
| # === Ownership === | |
| if profile.ownership_by_role: | |
| profile.ownership_level = max(v for _, v in profile.ownership_by_role) | |
| best_role = max(profile.ownership_by_role, key=lambda x: x[1]) | |
| profile.best_ownership_verb = best_role[0] | |
| # === Production === | |
| prod_keywords = ["production", "deployed", "shipped", "launched", | |
| "live traffic", "at scale", "real users", "on-call"] | |
| prod_hits = [kw for kw in prod_keywords if kw in text] | |
| profile.production_categories = list(set(prod_hits)) | |
| profile.has_production_evidence = len(prod_hits) >= 2 | |
| profile.production_readiness = min(1.0, len(prod_hits) / 5.0) | |
| # === Scale === | |
| m_users = re.search(r'(\d+(?:\.\d+)?)\s*million\s+(?:daily\s+)?(?:active\s+)?users', text, re.IGNORECASE) | |
| if m_users: | |
| profile.max_user_scale = float(m_users.group(1)) | |
| m_qps = re.search(r'(\d+(?:\.\d+)?)\s*k?\s*(?:qps|rps|requests?\s*per\s*sec)', text, re.IGNORECASE) | |
| if m_qps: | |
| profile.max_qps = float(m_qps.group(1)) | |
| m_data = re.search(r'(\d+)\s*(?:tb|gb|pb)\s+of\s+data', text, re.IGNORECASE) | |
| if m_data: | |
| profile.data_scale = float(m_data.group(1)) | |
| # === Impact === | |
| all_evidence = extract_all_evidence(c) | |
| impact_ev = [e for e in all_evidence if e.type == "impact"] | |
| profile.impact_count = len(impact_ev) | |
| profile.has_quantified_impact = len(impact_ev) > 0 | |
| profile.best_impact_strength = impact_ev[0].score / 20.0 if impact_ev else 0 | |
| for ev in impact_ev: | |
| profile.impact_metrics.append(ImpactMetric( | |
| metric_type=ev.metric, value=ev.context, | |
| strength=ev.score / 20.0, company=ev.company, | |
| year_range=ev.year_range, | |
| )) | |
| # === Domain === | |
| from lib.domain import get_taxonomy | |
| tax = get_taxonomy() | |
| for skill, tier in tax.skill_tier.items(): | |
| weight = {1: 1.0, 2: 0.5, 3: 0.1}.get(tier, 0.05) | |
| if skill in text: | |
| profile.domain_skills[skill] = weight | |
| profile.domain_expertise = min(1.0, len(profile.domain_skills) / 5.0) if profile.domain_skills else 0 | |
| # Primary domain from most recent role | |
| if profile.role_snapshots: | |
| profile.primary_domain = profile.role_snapshots[0].domain | |
| # === Career === | |
| narrative = analyze_narrative(c) | |
| profile.career_coherence = narrative.coherence | |
| profile.narrative_type = narrative.trajectory_type | |
| profile.narrative_suspicious = narrative.suspicious_patterns | |
| profile.avg_tenure_months = total_months / len(ch) if ch else 0 | |
| # Company tier trajectory | |
| profile.company_tier_trajectory = [s.company_tier for s in profile.role_snapshots] | |
| if profile.company_tier_trajectory: | |
| profile.company_tier_avg = sum(profile.company_tier_trajectory) / len(profile.company_tier_trajectory) | |
| # Career trajectory (reuse from features logic) | |
| if len(profile.role_snapshots) >= 2: | |
| seniority_by_role = [s.seniority for s in reversed(profile.role_snapshots)] | |
| upward = sum(1 for i in range(1, len(seniority_by_role)) | |
| if seniority_by_role[i] > seniority_by_role[i-1] + 0.05) | |
| profile.career_trajectory = min(1.0, 0.3 + upward * 0.25) | |
| # === Behaviour === | |
| from lib.constants import REFERENCE_DATE | |
| # Availability | |
| notice = float(sigs.get("notice_period_days", 45) or 45) | |
| otw = bool(sigs.get("open_to_work_flag", False)) | |
| profile.availability = 0.0 | |
| if otw: | |
| profile.availability += 0.40 | |
| if notice <= 30: | |
| profile.availability += 0.30 | |
| elif notice <= 60: | |
| profile.availability += 0.15 | |
| else: | |
| profile.availability += 0.05 | |
| profile.availability = min(1.0, profile.availability + 0.10) | |
| # Responsiveness | |
| rr = float(sigs.get("recruiter_response_rate", 0.3) or 0.3) | |
| avg_resp = float(sigs.get("avg_response_time_hours", 48) or 48) | |
| profile.responsiveness = min(1.0, 0.4 * rr + 0.3 * max(0, 1 - avg_resp / 72) + 0.10) | |
| # Recency | |
| last_active = schema.parse_date(sigs.get("last_active_date")) | |
| if last_active: | |
| days = (REFERENCE_DATE - last_active).days | |
| if days <= 30: | |
| profile.recency = 1.0 | |
| elif days <= 60: | |
| profile.recency = 0.75 | |
| elif days <= 90: | |
| profile.recency = 0.50 | |
| elif days <= 180: | |
| profile.recency = 0.25 | |
| else: | |
| profile.recency = 0.10 | |
| # Platform trust | |
| verified = sigs.get("verified_email", False) and sigs.get("verified_phone", False) | |
| linkedin = sigs.get("linkedin_connected", False) | |
| profile.platform_trust = 0.0 | |
| if verified: | |
| profile.platform_trust += 0.30 | |
| if linkedin: | |
| profile.platform_trust += 0.20 | |
| profile.platform_trust += 0.10 # baseline | |
| # Market demand | |
| saves = min(float(sigs.get("saved_by_recruiters_30d") or 0), 20) / 20 | |
| appearances = min(float(sigs.get("search_appearance_30d") or 0), 200) / 200 | |
| profile.market_demand = 0.5 * saves + 0.3 * appearances + 0.2 * min(1.0, float(sigs.get("profile_views_received_30d", 0) or 0) / 50) | |
| # GitHub | |
| gh = float(sigs.get("github_activity_score", -1) or -1) | |
| profile.github_activity = max(0.0, min(1.0, gh / 100)) if gh >= 0 else 0.0 | |
| # Interview | |
| ic = float(sigs.get("interview_completion_rate", 0) or 0) | |
| profile.interview_completion = min(1.0, ic) | |
| # === Resume Quality === | |
| # Quantified outcomes | |
| quant_patterns = [r'\d+\s*%', r'\d+(?:\.\d+)?\s*million', r'p99', r'ndcg', r'\d+k?\s*qps'] | |
| quant_count = sum(1 for pat in quant_patterns if re.search(pat, text, re.IGNORECASE)) | |
| profile.quantified_outcomes = min(1.0, quant_count / 3.0) | |
| # Truthiness (simplified) | |
| listed = schema.listed_skill_names(c) | |
| context_matches = sum(1 for s in listed if s in text) | |
| profile.truthiness = min(1.0, context_matches / max(len(listed), 1)) | |
| # Keyword stuffing risk | |
| listed_jd_skills = sum(1 for s in listed | |
| if any(jd_sk in s for d in [jd.required_skills, jd.preferred_skills] | |
| for jd_sk in d.values() for jd_sk in d)) | |
| listed_non_jd = len(listed) - listed_jd_skills | |
| if listed_non_jd > 5 and profile.truthiness < 0.3: | |
| profile.keyword_stuffing_risk = min(1.0, listed_non_jd / 15.0) | |
| # Profile completeness | |
| profile.profile_completeness = (float(sigs.get("profile_completeness_score", 0) or 0) / 100.0) | |
| # === Safety === | |
| profile.is_honeypot, profile.honeypot_reasons = honeypot.is_honeypot(c) | |
| # Disqualifier (simplified - full logic in features.py) | |
| from lib.jd_requirements import BAD_TITLE_PATTERNS, CONSULTING_FIRMS, CONSULTING_INDUSTRIES | |
| profile.disqualifier_reasons = [] | |
| title_lower = profile.current_title.lower() | |
| if any(bp in title_lower for bp in BAD_TITLE_PATTERNS): | |
| profile.disqualifier_reasons.append("non_engineering_title") | |
| all_consulting = all( | |
| any(cf in (r.get("company") or "").lower() for cf in CONSULTING_FIRMS) | |
| or (r.get("industry") or "").lower() in CONSULTING_INDUSTRIES | |
| for r in ch | |
| ) | |
| if all_consulting and ch: | |
| profile.disqualifier_reasons.append("consulting_only_career") | |
| profile.disqualifier_penalty = 0.50 | |
| if profile.disqualifier_reasons: | |
| profile.disqualifier_penalty = min(profile.disqualifier_penalty, 0.65) | |
| # === Education === | |
| if edu: | |
| best_edu = max(edu, key=lambda e: e.get("end_year", 0) or 0) | |
| profile.education_field = (best_edu.get("field_of_study") or "").lower() | |
| profile.education_tier = (best_edu.get("tier") or "unknown").lower() | |
| profile.has_relevant_degree = any( | |
| kw in profile.education_field | |
| for kw in ["computer science", "cs", "artificial intelligence", | |
| "machine learning", "data science", "information technology", | |
| "mathematics", "statistics", "e&e", "electronics"] | |
| ) | |
| # === Location === | |
| profile.location = p.get("location", "") | |
| profile.country = p.get("country", "") | |
| profile.willing_to_relocate = bool(sigs.get("willing_to_relocate", False)) | |
| profile.preferred_work_mode = (sigs.get("preferred_work_mode") or "").lower() | |
| # === Evidence (top 5 for reasoning) === | |
| profile.top_evidence = all_evidence[:5] | |
| profile.evidence_summary = get_evidence_summary(c) | |
| # === Raw ref (for reasoning only) === | |
| profile._raw = c | |
| return profile |