talently-api / services /classifier.py
SantiCan's picture
fix: clasificación de área con texto compacto y label Web Development
9ce3067
from domain.interfaces import ClassifierInterface
from services.hf_client import hf_client
from core.config import settings
SENIORITY_LABELS = ["Junior", "Mid", "Senior", "Lead"]
# Umbrales de seniority por años de experiencia. Tupla (min, max) inclusiva en
# min y exclusiva en max. Se evalúan en orden: el primero que matchea gana.
# Centralizado acá para que ajustar la política sea cambiar UNA línea.
# Lead no entra en la regla determinística: ser Lead es por responsabilidades,
# no por años. Si no hay años, el modelo zero-shot puede igual devolver Lead.
SENIORITY_THRESHOLDS = [
("Junior", 0, 2),
("Mid", 2, 5),
("Senior", 5, 999),
]
AREA_LABELS = [
"Backend Development",
"Frontend Development",
"Full Stack Development",
"Web Development",
"Data Science",
"Machine Learning Engineering",
"DevOps",
"Mobile Development",
"QA / Testing",
"Product Management",
"UX/UI Design",
]
class ClassifierService(ClassifierInterface):
def classify_seniority(self, text: str, experience_years: int | None) -> dict:
"""
Clasifica seniority. Si tenemos años, regla determinística (confidence 1.0).
Si no, caemos al modelo zero-shot que evalúa el texto crudo.
"""
if experience_years is not None:
return self._seniority_by_years(experience_years)
return self._classify(text, SENIORITY_LABELS)
def classify_area(self, text: str) -> dict:
return self._classify(text, AREA_LABELS)
@staticmethod
def _seniority_by_years(years: int) -> dict:
"""Aplica la tabla de umbrales. Confidence 1.0 porque es determinístico."""
for label, lo, hi in SENIORITY_THRESHOLDS:
if lo <= years < hi:
return {"label": label, "confidence": 1.0}
# Fallback defensivo: no debería pasar con la tabla actual.
return {"label": "Senior", "confidence": 1.0}
def _classify(self, text: str, labels: list[str]) -> dict:
"""
Llama al modelo zero-shot y retorna la label con mayor score.
El modelo puede devolver dos formatos distintos:
- Dict: {"labels": [...], "scores": [...]}
- List: [{"label": "X", "score": 0.9}, ...]
"""
try:
result = hf_client.query(
settings.zero_shot_model,
{
"inputs": text[:2000],
"parameters": {"candidate_labels": labels},
},
)
except Exception:
return {"label": "Unknown", "confidence": 0.0}
# Formato list
if isinstance(result, list) and result:
top = max(result, key=lambda x: x.get("score", 0))
return {"label": top["label"], "confidence": float(top["score"])}
# Formato dict
if isinstance(result, dict) and "labels" in result:
return {"label": result["labels"][0], "confidence": float(result["scores"][0])}
return {"label": "Unknown", "confidence": 0.0}