generated from kgod/ai-review-template
feat: builder 简历生成 + 轻度优化 + 简历导入交付副本
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"""创建后丰富模块规格表(PRD §10.2 优先级队列)。
|
||||
|
||||
声明式表驱动:FSM 与路由层只读本表,不硬编码模块逻辑。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from .models import JobType
|
||||
|
||||
#: records 中合法的记录键
|
||||
RECORD_KEYS = (
|
||||
"education",
|
||||
"work_experience",
|
||||
"internship_experience",
|
||||
"project_experience",
|
||||
"campus_experience",
|
||||
"competition",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ModuleSpec:
|
||||
name: str # 队列元素唯一名
|
||||
kind: str # "record_chat" | "record_form" | "tags"
|
||||
record_type: str | None # records 目标键;None = 需用户先选类型或运行时决定
|
||||
multi: bool # 多段模块(确认后发 AddAnother)
|
||||
core_fields: tuple[str, ...] # 核心字段(缺一不进确认)
|
||||
optional_fields: tuple[str, ...]
|
||||
components: tuple[str, ...] # 须进 STAGE_COMPONENTS 白名单
|
||||
prompt: str # 模块开放式提问话术
|
||||
skippable: bool = True
|
||||
|
||||
|
||||
_CONFIRM = ("RecordFields", "ExperienceConfirmCard")
|
||||
_CONFIRM_MULTI = ("RecordFields", "ExperienceConfirmCard", "AddAnother")
|
||||
|
||||
|
||||
def _record(
|
||||
name: str,
|
||||
record_type: str | None,
|
||||
multi: bool,
|
||||
prompt: str,
|
||||
picker: bool = False,
|
||||
kind: str = "record_fields",
|
||||
) -> ModuleSpec:
|
||||
components = (("ChoiceChips",) if picker else ()) + (_CONFIRM_MULTI if multi else _CONFIRM)
|
||||
return ModuleSpec(
|
||||
name=name,
|
||||
kind=kind,
|
||||
record_type=record_type,
|
||||
multi=multi,
|
||||
core_fields=(),
|
||||
optional_fields=("description",),
|
||||
components=components,
|
||||
prompt=prompt,
|
||||
)
|
||||
|
||||
|
||||
ENRICHMENT_MODULES: dict[str, ModuleSpec] = {
|
||||
spec.name: spec
|
||||
for spec in (
|
||||
_record(
|
||||
"internship",
|
||||
"internship_experience",
|
||||
True,
|
||||
"补充一段实习经历,包括公司、职位和时间。",
|
||||
),
|
||||
_record(
|
||||
"more_work",
|
||||
"work_experience",
|
||||
True,
|
||||
"还有其他工作经历吗?填写公司、职位和时间。",
|
||||
),
|
||||
_record(
|
||||
"project",
|
||||
"project_experience",
|
||||
True,
|
||||
"填写一个你做过的项目,包括名称、你的角色和时间。",
|
||||
),
|
||||
_record(
|
||||
"education",
|
||||
"education",
|
||||
True,
|
||||
"补充一段教育经历,包括学校、专业、学历和时间。",
|
||||
),
|
||||
_record(
|
||||
"campus_experience",
|
||||
"campus_experience",
|
||||
True,
|
||||
"补充一段校园经历,包括组织、角色和时间。",
|
||||
),
|
||||
|
||||
ModuleSpec(
|
||||
name="competition",
|
||||
kind="record_form",
|
||||
record_type="competition",
|
||||
multi=True,
|
||||
core_fields=("name", "award", "date"),
|
||||
optional_fields=("description",),
|
||||
components=("CompetitionFields", "ExperienceConfirmCard", "AddAnother"),
|
||||
prompt="有竞赛获奖经历吗?填写竞赛名称、奖项和获奖月份。",
|
||||
),
|
||||
ModuleSpec(
|
||||
name="skills",
|
||||
kind="tags",
|
||||
record_type=None,
|
||||
multi=False,
|
||||
core_fields=(),
|
||||
optional_fields=("skills",),
|
||||
components=("TagsInput",),
|
||||
prompt="列一下你的技能,逐个添加,可以留空。",
|
||||
),
|
||||
ModuleSpec(
|
||||
name="certificates",
|
||||
kind="tags",
|
||||
record_type=None,
|
||||
multi=False,
|
||||
core_fields=(),
|
||||
optional_fields=("certificates",),
|
||||
components=("TagsInput",),
|
||||
prompt="列一下你的证书,可以留空。",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
ENRICHMENT_QUEUES: dict[JobType, tuple[str, ...]] = {
|
||||
JobType.CAMPUS: (
|
||||
"internship",
|
||||
"project",
|
||||
"competition",
|
||||
"skills",
|
||||
"certificates",
|
||||
),
|
||||
JobType.SOCIAL: (
|
||||
"more_work",
|
||||
"project",
|
||||
"education",
|
||||
"skills",
|
||||
"certificates",
|
||||
),
|
||||
JobType.INTERNSHIP: (
|
||||
"campus_experience",
|
||||
"project",
|
||||
"competition",
|
||||
"skills",
|
||||
"certificates",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def module_by_name(name: str) -> ModuleSpec:
|
||||
return ENRICHMENT_MODULES[name]
|
||||
|
||||
|
||||
PICKER_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = {}
|
||||
|
||||
TAG_SEQUENCE = ("skills", "certificates")
|
||||
TAG_TITLES = {"skills": "你的技能", "certificates": "你的证书"}
|
||||
|
||||
_DEFAULT_SKILLS = ("沟通协调", "问题解决", "团队协作")
|
||||
_SKILL_RULES = (
|
||||
(("前端", "frontend", "web"), ("HTML", "CSS", "JavaScript", "TypeScript", "Vue", "React", "Git")),
|
||||
(("java",), ("Java", "Spring Boot", "MySQL", "Redis", "Git")),
|
||||
(("后端", "backend"), ("Python", "Java", "MySQL", "Redis", "Docker", "Git")),
|
||||
(("数据", "算法", "ai", "人工智能"), ("Python", "SQL", "Pandas", "机器学习", "Git")),
|
||||
(("产品",), ("需求分析", "原型设计", "数据分析", "项目管理")),
|
||||
)
|
||||
|
||||
|
||||
_DESCRIPTION_SKILLS = (
|
||||
(("python",), "Python"),
|
||||
(("fastapi",), "FastAPI"),
|
||||
(("django",), "Django"),
|
||||
(("flask",), "Flask"),
|
||||
(("java",), "Java"),
|
||||
(("spring boot", "springboot", "spring"), "Spring Boot"),
|
||||
(("mysql",), "MySQL"),
|
||||
(("postgres", "postgresql"), "PostgreSQL"),
|
||||
(("redis",), "Redis"),
|
||||
(("docker",), "Docker"),
|
||||
(("kubernetes", "k8s"), "Kubernetes"),
|
||||
(("vue",), "Vue"),
|
||||
(("react",), "React"),
|
||||
(("typescript",), "TypeScript"),
|
||||
(("javascript",), "JavaScript"),
|
||||
(("sql",), "SQL"),
|
||||
(("pandas",), "Pandas"),
|
||||
(("机器学习", "machine learning"), "机器学习"),
|
||||
)
|
||||
|
||||
|
||||
def skill_suggestions(
|
||||
target_position: str | None, profile: dict[str, Any] | None = None
|
||||
) -> list[str]:
|
||||
"""Suggest skills from target role and facts already supplied by the user."""
|
||||
normalized = (target_position or "").strip().lower()
|
||||
base_suggestions: list[str] = []
|
||||
for keywords, rule_suggestions in _SKILL_RULES:
|
||||
if any(keyword in normalized for keyword in keywords):
|
||||
base_suggestions = list(rule_suggestions)
|
||||
break
|
||||
if not base_suggestions:
|
||||
base_suggestions = list(_DEFAULT_SKILLS)
|
||||
|
||||
profile = profile or {}
|
||||
supplied = _profile_text(profile).lower()
|
||||
for aliases, skill in _DESCRIPTION_SKILLS:
|
||||
if any(alias in supplied for alias in aliases):
|
||||
base_suggestions.append(skill)
|
||||
|
||||
existing = {
|
||||
str(skill).strip().casefold()
|
||||
for skill in ((profile.get("tags") or {}).get("skills") or [])
|
||||
if str(skill).strip()
|
||||
}
|
||||
result: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for skill in base_suggestions:
|
||||
key = skill.casefold()
|
||||
if key not in seen and key not in existing:
|
||||
result.append(skill)
|
||||
seen.add(key)
|
||||
return result[:8]
|
||||
|
||||
|
||||
def _profile_text(profile: dict[str, Any]) -> str:
|
||||
"""Collect user-entered descriptions only; never infer skills from resume examples."""
|
||||
values: list[str] = []
|
||||
entries: list[Any] = [profile.get("anchor")]
|
||||
entries.extend(profile.get("experiences") or [])
|
||||
for records in (profile.get("records") or {}).values():
|
||||
entries.extend(records or [])
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
description = entry.get("description")
|
||||
if description:
|
||||
values.append(str(description))
|
||||
values.extend(str(item) for item in (entry.get("highlights") or []) if item)
|
||||
return "\n".join(values)
|
||||
Reference in New Issue
Block a user