"""Layer 0/floor: lexical catalog matching. This wraps the existing pdfplumber-free matching logic from the MVP so the dispatcher can treat it uniformly with the other layers. Unlike the four ML layers, this one is always safe — it has no heavy deps and only touches the DB's Skill catalog. Kept separate from ``resume_parser`` so the dispatcher can invoke it as a layer alongside the others rather than having a special-case code path. """ from __future__ import annotations from . import NerLayer class LexicalLayer(NerLayer): name = "lexical" def predict(self, text: str) -> dict[str, float]: if not text.strip(): return {} # Reuse the existing implementation so behaviour stays identical. from apps.accounts.resume_parser import _candidate_matches from apps.skills.models import Skill skills = list(Skill.objects.all().only("id", "skill_name")) hits = _candidate_matches(text, skills) # _candidate_matches returns {skill_id: {confidence, proficiency, matched_span}} # — the dispatcher works with {name: confidence}, so remap. by_id = {s.id: s.skill_name for s in skills} out: dict[str, float] = {} for skill_id, data in hits.items(): name = by_id.get(skill_id) if name: out[name] = float(data["confidence"]) return out def available(self) -> bool: return True layer = LexicalLayer()