generated from kgod/ai-review-template
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
"""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,
|
||
)
|