redrob-ranker / src /ingestion /normalizer.py
Venkat7123's picture
Deploy candidate ranker
37b0787
Raw
History Blame Contribute Delete
10.2 kB
from __future__ import annotations
import re
import uuid
from typing import Any
from src.core.models import (
Education,
Location,
PersonalInfo,
ProfessionalInfo,
ProficiencyLevel,
Profile,
ProfileMetadata,
ProfileSource,
Signals,
Skill,
WorkExperience,
)
from src.extraction.pipeline import FieldExtractorPipeline
_PROFICIENCY_MAP = {
"beginner": ProficiencyLevel.BEGINNER,
"intermediate": ProficiencyLevel.INTERMEDIATE,
"advanced": ProficiencyLevel.ADVANCED,
"expert": ProficiencyLevel.EXPERT,
}
def _parse_grade(grade: str | None) -> float | None:
if not grade:
return None
grade = grade.strip()
m = re.search(r"([\d.]+)", grade)
if m:
val = float(m.group(1))
if "%" in grade:
val = val / 100.0 * 10.0
return round(val, 2)
return None
def normalize_redrob(raw: dict[str, Any], source: str = "redrob") -> Profile:
prof = raw.get("profile", {})
city_state = prof.get("location", "") or ""
city_parts = city_state.split(",") if city_state else []
city = city_parts[0].strip() if city_parts else None
state = city_parts[1].strip() if len(city_parts) > 1 else None
preferred_work_mode = raw.get("redrob_signals", {}).get("preferred_work_mode", "")
is_remote_ok = preferred_work_mode in ("remote", "flexible", "hybrid")
location = Location(
city=city or None,
state=state or None,
country=prof.get("country", "India"),
is_remote_ok=is_remote_ok,
)
personal = PersonalInfo(
name=prof.get("anonymized_name", ""),
location=location,
languages_spoken=[],
native_language=None,
)
extracted = FieldExtractorPipeline().extract(raw)
professional = ProfessionalInfo(
current_title=extracted.current_title.value or prof.get("current_title"),
current_company=extracted.current_company.value or prof.get("current_company"),
total_experience_years=(
extracted.total_experience_years.value or prof.get("years_of_experience")
),
industry=extracted.industry.value or prof.get("current_industry"),
employment_type=None,
seniority_level=extracted.seniority_level.value,
)
skills = [
Skill(
name=s["name"],
category=_infer_skill_category(s["name"]),
proficiency=_PROFICIENCY_MAP.get(s.get("proficiency", "")),
years_used=(
round(s.get("duration_months", 0) / 12, 1)
if s.get("duration_months")
else None
),
evidence=None,
confidence=min(1.0, s.get("endorsements", 0) / 50),
)
for s in raw.get("skills", [])
]
experience = [
WorkExperience(
title=entry["title"],
company=entry["company"],
start_date=entry.get("start_date"),
end_date=entry.get("end_date"),
is_current=entry.get("is_current", False),
description=entry.get("description", ""),
location=entry.get("location"),
)
for entry in raw.get("career_history", [])
]
education = [
Education(
institution=edu["institution"],
degree=edu.get("degree"),
field=edu.get("field_of_study"),
start_date=str(edu.get("start_year", "")) if edu.get("start_year") else None,
end_date=str(edu.get("end_year", "")) if edu.get("end_year") else None,
gpa=_parse_grade(edu.get("grade")),
)
for edu in raw.get("education", [])
]
certs = [c["name"] for c in raw.get("certifications", [])]
languages = [
lang["language"] for lang in raw.get("languages", [])
]
native_langs = [
lang["language"]
for lang in raw.get("languages", [])
if lang.get("proficiency") == "native"
]
signals = _build_signals(raw.get("redrob_signals", {}), certs)
raw_text = _build_raw_text(prof, experience, skills, education, certs, languages)
existing_skill_names = {s.name.lower() for s in skills}
skills = _enrich_skills_from_text(skills, raw_text, existing_skill_names)
if native_langs:
personal.native_language = native_langs[0]
personal.languages_spoken = languages
return Profile(
profile_id=raw.get("candidate_id") or str(uuid.uuid4()),
source=ProfileSource.REDROB,
raw_text=raw_text,
personal=personal,
professional=professional,
skills=skills,
experience=experience,
education=education,
signals=signals,
metadata=ProfileMetadata(
language_detected="en",
data_quality_score=0.0,
),
)
def _enrich_skills_from_text(
skills: list[Skill],
raw_text: str,
existing_skill_names: set[str],
) -> list[Skill]:
"""Append skill aliases found in raw text but missing from structured skills."""
from src.matching.skill_matcher import SKILL_ALIASES as _KNOWN_ALIASES
enriched = list(skills)
raw_lower = raw_text.lower()
for canon, _aliases in _KNOWN_ALIASES.items():
if canon not in existing_skill_names and canon in raw_lower:
enriched.append(
Skill(
name=canon.title(),
category=_infer_skill_category(canon),
evidence="Extracted from profile text",
confidence=0.6,
)
)
existing_skill_names.add(canon)
return enriched
def _infer_skill_category(name: str) -> Any:
from src.core.models import SkillCategory
name_lower = name.lower()
lang_keywords = [
"python", "java", "javascript", "typescript", "go", "rust",
"c++", "c#", "ruby", "php", "swift", "kotlin", "scala",
"r", "sql", "html", "css", "bash",
]
framework_keywords = [
"react", "angular", "vue", "django", "flask", "spring",
"pytorch", "tensorflow", "keras", "spark", "hadoop",
"node.js", "express", "next.js", "tailwind", "redux",
]
tool_keywords = [
"docker", "kubernetes", "aws", "gcp", "azure", "terraform",
"airflow", "kafka", "git", "jenkins", "ansible",
"snowflake", "bigquery", "mongodb", "redis", "postgresql",
]
if any(kw in name_lower for kw in lang_keywords):
return SkillCategory.PROGRAMMING_LANGUAGE
if any(kw in name_lower for kw in framework_keywords):
return SkillCategory.FRAMEWORK
if any(kw in name_lower for kw in tool_keywords):
return SkillCategory.TOOL
domain_keywords = [
"machine learning", "deep learning", "nlp", "computer vision",
"data science", "data engineering", "mlops",
"natural language processing", "llm", "rag",
"statistical", "analytics",
]
if any(kw in name_lower for kw in domain_keywords):
return SkillCategory.DOMAIN_KNOWLEDGE
return SkillCategory.TOOL
def _build_raw_text(
prof: dict[str, Any],
experience: list[WorkExperience],
skills: list[Skill],
education: list[Education],
certs: list[str],
languages: list[str],
) -> str:
name = prof.get("anonymized_name", "")
title = prof.get("current_title", "")
company = prof.get("current_company", "")
summary = prof.get("summary", "")
skill_names = ", ".join(s.name for s in skills)
exp_strs = [
f"{e.title} at {e.company} ({e.start_date or ''}-{e.end_date or ''}): {e.description}"
for e in experience
]
exp_text = "; ".join(exp_strs) if exp_strs else ""
edu_strs = [
f"{e.degree or ''} in {e.field or ''} at {e.institution}"
for e in education
]
edu_text = "; ".join(edu_strs) if edu_strs else ""
certs_text = ", ".join(certs) if certs else ""
langs_text = ", ".join(languages) if languages else ""
parts = [
f"Name: {name}.",
f"Title: {title}." if title else "",
f"Company: {company}." if company else "",
f"Summary: {summary}." if summary else "",
f"Skills: {skill_names}." if skill_names else "",
f"Experience: {exp_text}." if exp_text else "",
f"Education: {edu_text}." if edu_text else "",
f"Certifications: {certs_text}." if certs_text else "",
f"Languages: {langs_text}." if langs_text else "",
]
return " ".join(p for p in parts if p)
def _build_signals(rs: dict[str, Any], certs: list[str]) -> Signals:
salary_range = rs.get("expected_salary_range_inr_lpa", {}) or {}
return Signals(
is_passive=not rs.get("open_to_work_flag", True),
last_active_date=rs.get("last_active_date"),
open_to_work=rs.get("open_to_work_flag"),
github_activity_score=rs.get("github_activity_score"),
certifications=certs,
has_portfolio=rs.get("linkedin_connected", False),
# Full platform signals
profile_completeness_score=rs.get("profile_completeness_score"),
recruiter_response_rate=rs.get("recruiter_response_rate"),
avg_response_time_hours=rs.get("avg_response_time_hours"),
saved_by_recruiters_30d=rs.get("saved_by_recruiters_30d"),
profile_views_received_30d=rs.get("profile_views_received_30d"),
applications_submitted_30d=rs.get("applications_submitted_30d"),
connection_count=rs.get("connection_count"),
endorsements_received=rs.get("endorsements_received"),
search_appearance_30d=rs.get("search_appearance_30d"),
interview_completion_rate=rs.get("interview_completion_rate"),
offer_acceptance_rate=rs.get("offer_acceptance_rate"),
notice_period_days=rs.get("notice_period_days"),
preferred_work_mode=rs.get("preferred_work_mode"),
willing_to_relocate=rs.get("willing_to_relocate"),
verified_email=rs.get("verified_email"),
verified_phone=rs.get("verified_phone"),
expected_salary_min=salary_range.get("min"),
expected_salary_max=salary_range.get("max"),
linkedin_connected=rs.get("linkedin_connected"),
skill_assessment_scores=rs.get("skill_assessment_scores", {}),
)