Files
resume-agent/backend/app/builder_conversation/skills.py
T
hypandClaude ae2d9b128d feat: builder 简历生成 + 轻度优化 + 简历导入交付副本
自内部仓库剥离深度优化与 RAG 知识库后的交付版本:
- Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强)
- 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护
- 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏
- PostgreSQL 运行时 + Alembic 迁移链

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 11:08:30 +08:00

122 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Skill-suggestion cards and selection handling for the Builder."""
from __future__ import annotations
from copy import deepcopy
from typing import Any
from ..fsm import FSMError, Transition, component
from ..models import Stage
from ..resume_skill_advisor import recommend_skill_candidates
from ..skill_groups import update_skill_groups
from .constants import u
from .state import _set_stream_phases
from .turns import _next_step_turn, recommended_section
def _skill_choice_card(candidates: list[dict[str, Any]]) -> dict[str, Any]:
return component(
"ChoiceChips",
module="builder_skill_select",
title=u("确认岗位技能"),
description=u("请选择你愿意确认加入简历的技能;未选择的候选不会写入。没有合适的也可以暂时跳过。"),
multiple=True,
skippable=True,
skip_label=u("暂不添加"),
options=[
{
"value": str(candidate["skill"]),
"label": f'{candidate["skill"]}{u("")}{candidate["category"]}{u("")}',
}
for candidate in candidates
],
)
def _builder_skill_candidates(
profile: dict[str, Any],
resume_content: dict[str, Any],
skill_suggester: Any | None,
) -> list[dict[str, Any]]:
if skill_suggester is None:
return []
existing = [
str(skill).strip()
for group in resume_content.get("skill_groups") or []
if isinstance(group, dict)
for skill in group.get("skills") or []
if str(skill).strip()
]
working_profile = deepcopy(profile)
working_profile["tags"] = {**dict(working_profile.get("tags") or {}), "skills": existing}
facts: list[dict[str, Any]] = []
for section in resume_content.get("sections") or []:
if not isinstance(section, dict):
continue
for entry in section.get("items") or []:
if isinstance(entry, dict):
facts.append(deepcopy(entry))
working_profile["experiences"] = facts
return recommend_skill_candidates(
working_profile,
existing,
u("根据我选择的目标岗位推荐可确认技能"),
skill_suggester,
)
def _process_skill_selection(
profile: dict[str, Any],
state: dict[str, Any],
action: str,
payload: dict[str, Any],
resume_content: dict[str, Any],
) -> Transition:
if action == "skip":
state["pending_skill_candidates"] = []
_set_stream_phases(profile, "suggesting_next")
return Transition(
Stage.BUILDER_CONVERSATION,
profile,
_next_step_turn(recommended_section(profile, resume_content), prefix=u("好的,先不添加技能。")),
lifecycle="dismissed",
)
if action != "select":
raise FSMError("invalid_builder_skill_selection", "Confirm or skip the skill suggestions", status_code=422)
selected = payload.get("values")
if not isinstance(selected, list):
selected = [payload.get("value")]
allowed = {
str(item.get("skill") or "").strip()
for item in state.get("pending_skill_candidates") or []
if isinstance(item, dict) and str(item.get("skill") or "").strip()
}
chosen = [str(value).strip() for value in selected if str(value or "").strip() in allowed]
if not chosen:
raise FSMError("invalid_builder_skill_selection", "Select at least one suggested skill or skip", status_code=422)
existing = [
str(skill).strip()
for group in resume_content.get("skill_groups") or []
if isinstance(group, dict)
for skill in group.get("skills") or []
if str(skill).strip()
]
preferred = {
str(item.get("skill") or "").strip(): str(item.get("category") or "").strip()
for item in state.get("pending_skill_candidates") or []
if isinstance(item, dict) and str(item.get("skill") or "").strip() and str(item.get("category") or "").strip()
}
content = update_skill_groups(resume_content, [*existing, *chosen], preferred_categories=preferred)
state["pending_skill_candidates"] = []
_set_stream_phases(profile, "saving", "suggesting_next")
return Transition(
Stage.BUILDER_CONVERSATION,
profile,
_next_step_turn(
recommended_section(profile, content),
prefix=u("已添加") + " " + u("、").join(chosen) + u("。"),
),
lifecycle="confirmed",
resume_content=content,
)