from __future__ import annotations from typing import List, Tuple, Set from .text import extract_keywords_from_text def allowed_keywords_from_profile(skills: List[str], experiences: List) -> Set[str]: allowed = set(s.lower() for s in skills) for e in experiences: for t in getattr(e, "technologies", []) or []: allowed.add(str(t).lower()) for a in getattr(e, "achievements", []) or []: for k in extract_keywords_from_text(a, top_k=5): allowed.add(k.lower()) return allowed def clamp_to_allowed_keywords(text: str, allowed: Set[str]) -> Tuple[str, List[str]]: used = [] # Retain only keywords that are allowed kws = extract_keywords_from_text(text, top_k=80) for k in kws: if k.lower() in allowed: used.append(k) return text, used def detect_contradictions(resume_text: str, letter_text: str, allowed: Set[str]) -> List[str]: # Simple heuristic: keywords in letter not in resume nor allowed -> potential contradiction resume_k = set(k.lower() for k in extract_keywords_from_text(resume_text, top_k=100)) letter_k = set(k.lower() for k in extract_keywords_from_text(letter_text, top_k=100)) issues = [] for k in letter_k: if k not in resume_k and k not in allowed: issues.append(k) return issues def coverage_score(text: str, target_keywords: List[str]) -> float: if not target_keywords: return 1.0 lower = text.lower() hits = sum(1 for k in target_keywords if k.lower() in lower) return hits / max(1, len(target_keywords)) def conciseness_score(text: str, max_chars: int) -> float: # 1.0 if within limit; decay if exceeded if len(text) <= max_chars: return 1.0 return max(0.0, 1.0 - (len(text) - max_chars) / (max_chars * 0.5))