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)