Spaces:
Sleeping
Sleeping
| 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) | |