"""Deterministic display categories for confirmed resume skills.""" from __future__ import annotations from collections.abc import Iterable, Mapping _EXACT_CATEGORY_RULES: dict[str, str] = { "sql analysis": "产品、设计与分析", "data analysis": "产品、设计与分析", "user research": "产品、设计与分析", "数据分析": "产品、设计与分析", "用户研究": "产品、设计与分析", } _CATEGORY_RULES: tuple[tuple[str, tuple[str, ...]], ...] = ( ("编程语言与框架", ( "python", "java", "javascript", "typescript", "go", "golang", "c++", "c#", "fastapi", "django", "flask", "spring", "spring boot", "node.js", "nodejs", "react native", "pytorch", "tensorflow", "编程语言", "软件工程", )), ("前端", ( "vue", "react", "angular", "html", "css", "sass", "tailwind", "webpack", "vite", "前端", "小程序", )), ("后端与数据存储", ( "postgresql", "postgres", "mysql", "sqlite", "redis", "mongodb", "elasticsearch", "kafka", "rabbitmq", "sql", "clickhouse", "后端", "数据库", "缓存", "消息队列", )), ("AI 与数据智能", ( "langgraph", "langchain", "llamaindex", "rag", "pgvector", "bge-m3", "bge", "tei", "机器学习", "深度学习", "人工智能", "计算机视觉", "自然语言处理", "pandas", "numpy", )), ("云、DevOps 与工具", ( "docker", "kubernetes", "k8s", "git", "github actions", "gitlab ci", "jenkins", "linux", "terraform", "aws", "azure", "aliyun", "云原生", "容器", )), ("产品、设计与分析", ( "figma", "axure", "tableau", "power bi", "excel", "data analysis", "sql analysis", "product", "user research", "产品", "原型", "需求分析", "项目管理", )), ) def classify_skills( skills: Iterable[object], preferred: Mapping[str, str] | None = None ) -> list[dict[str, list[str] | str]]: """Group confirmed user skills without changing their display order. `preferred` maps skill -> category assigned by the recommender (LLM); it wins over the keyword rules, which remain the fallback for manual edits. """ preferred_normalized = { " ".join(str(skill).casefold().split()): str(category).strip() for skill, category in (preferred or {}).items() if str(category).strip() } grouped: dict[str, list[str]] = {category: [] for category, _ in _CATEGORY_RULES} grouped["其他技能"] = [] seen: set[str] = set() for value in skills: skill = str(value or "").strip() normalized = " ".join(skill.casefold().split()) if not normalized or normalized in seen: continue seen.add(normalized) category = preferred_normalized.get(normalized) or _EXACT_CATEGORY_RULES.get(normalized) or next( (name for name, keywords in _CATEGORY_RULES if any(keyword in normalized for keyword in keywords)), "其他技能", ) grouped.setdefault(category, []).append(skill) return [ {"category": category, "skills": values} for category, values in grouped.items() if values ]