Spaces:
Sleeping
Sleeping
File size: 4,849 Bytes
37b0787 | 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 | from __future__ import annotations
from difflib import SequenceMatcher
from typing import Any
from src.core.models import RequiredSkill, Skill, SkillImportance
SKILL_ALIASES: dict[str, list[str]] = {
"python": ["python3", "py"],
"javascript": ["js", "ecmascript", "es6"],
"typescript": ["ts"],
"react": ["reactjs", "react.js"],
"vue": ["vuejs", "vue.js"],
"angular": ["angularjs", "angular.js", "angular 2+"],
"node.js": ["nodejs", "node"],
"kubernetes": ["k8s"],
"docker": ["docker"],
"aws": ["amazon web services"],
"gcp": ["google cloud platform", "google cloud"],
"azure": ["microsoft azure"],
"machine learning": ["ml"],
"artificial intelligence": ["ai"],
"natural language processing": ["nlp"],
"ci/cd": ["ci", "cd", "continuous integration", "continuous deployment"],
"sql": ["mysql", "postgresql", "postgres", "pl/sql"],
"nosql": ["mongodb", "cassandra", "redis"],
"git": ["github", "gitlab", "bitbucket"],
"rest api": ["rest", "restful", "restful api"],
"html": ["html5"],
"css": ["css3"],
"tensorflow": ["tf"],
"pytorch": ["torch"],
"fastapi": ["fast api"],
"django": ["django"],
"flask": ["flask"],
"spring boot": ["spring", "spring framework"],
"data science": ["data science"],
"deep learning": ["dl"],
"computer vision": ["cv"],
"statistics": ["statistical analysis", "statistical modeling"],
"react native": ["react-native", "reactnative"],
}
class SkillMatcher:
def __init__(self, similarity_threshold: float = 0.7) -> None:
self.similarity_threshold = similarity_threshold
def match_skills(
self, required: list[RequiredSkill], candidate_skills: list[Skill],
) -> tuple[float, list[dict[str, Any]]]:
if not required:
return 1.0, []
total_weight = 0.0
weighted_score = 0.0
details: list[dict[str, Any]] = []
for req in required:
importance_weight = self._importance_weight(req.importance)
total_weight += importance_weight
match = self.find_best_match(req.name, candidate_skills)
if match is not None:
prof_score = self.compute_proficiency_match(
req.min_proficiency, match.proficiency,
)
skill_score = 0.5 + 0.5 * prof_score
found = True
else:
skill_score = 0.0
found = False
details.append({
"skill": req.name,
"importance": req.importance.value,
"found": found,
"score": skill_score,
})
weighted_score += importance_weight * skill_score
overall = weighted_score / total_weight if total_weight > 0 else 0.0
return overall, details
def find_best_match(self, required_name: str, candidate_skills: list[Skill], subskills: list[str] | None = None) -> Skill | None:
normalized_req = self._normalize(required_name)
aliases = [self._normalize(a) for a in SKILL_ALIASES.get(normalized_req, [])]
if subskills:
aliases.extend([self._normalize(s) for s in subskills])
best_score = 0.0
best_skill: Skill | None = None
for skill in candidate_skills:
normalized_skill = self._normalize(skill.name)
if normalized_req == normalized_skill:
return skill
if any(normalized_skill == alias for alias in aliases):
return skill
score = self._fuzzy_score(normalized_req, normalized_skill)
if score > best_score:
best_score = score
best_skill = skill
if best_score >= self.similarity_threshold:
return best_skill
return None
def _normalize(self, name: str) -> str:
return name.strip().lower()
def _fuzzy_score(self, a: str, b: str) -> float:
return SequenceMatcher(None, a, b).ratio()
def compute_proficiency_match(self, required: str | None, candidate: str | None) -> float:
levels = ["beginner", "intermediate", "advanced", "expert"]
if required is None or candidate is None:
return 1.0
req_idx = levels.index(required.lower()) if required.lower() in levels else 0
cand_idx = levels.index(candidate.lower()) if candidate.lower() in levels else 0
if cand_idx >= req_idx:
return 1.0
return max(0.0, 1.0 - (req_idx - cand_idx) * 0.25)
def _importance_weight(self, importance: SkillImportance) -> float:
weights = {
SkillImportance.REQUIRED: 1.0,
SkillImportance.PREFERRED: 0.6,
SkillImportance.NICE_TO_HAVE: 0.3,
}
return weights.get(importance, 0.5)
|