"""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, )