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,191 @@
|
||||
"""Component-event handling for Builder cards (RecordFields, ChoiceChips, confirms)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from ..fsm import FSMError, Transition, anchor_field_specs, assistant_turn
|
||||
from ..models import ComposerMode, Stage
|
||||
from ..validators import record_entry_errors
|
||||
from .constants import SECTION_HEADINGS, u
|
||||
from .flow import _begin_edit
|
||||
from .followups import _redisplay_revision_candidate, _revise_pending_candidate
|
||||
from .predicates import _entry_by_id
|
||||
from .save import save_entry
|
||||
from .skills import _builder_skill_candidates, _process_skill_selection, _skill_choice_card
|
||||
from .summary_regen import SUMMARY_APPLY_MODULE, apply_summary_proposal, finish_transition
|
||||
from .state import (
|
||||
_clear_draft,
|
||||
_public_entry,
|
||||
_reset_gap_state,
|
||||
_set_stream_phases,
|
||||
ensure_builder_state,
|
||||
)
|
||||
from .turns import _fact_prompt, _next_step_turn, _record_card, recommended_section
|
||||
|
||||
|
||||
def process_component_event(
|
||||
profile: dict[str, Any],
|
||||
component_data: dict[str, Any],
|
||||
action: str,
|
||||
payload: dict[str, Any],
|
||||
resume_content: dict[str, Any],
|
||||
skill_suggester: Any | None = None,
|
||||
) -> Transition:
|
||||
updated = deepcopy(profile)
|
||||
state = ensure_builder_state(updated)
|
||||
name = str(component_data.get("component_name") or "")
|
||||
|
||||
if name == "ChoiceChips":
|
||||
module = str(component_data.get("module") or "")
|
||||
if module == "builder_entry_select":
|
||||
if action != "select":
|
||||
raise FSMError("invalid_builder_choice", "Choose an experience type", status_code=422)
|
||||
entry_id = str(payload.get("value") or "").strip()
|
||||
target = _entry_by_id(resume_content, entry_id)
|
||||
if target is None:
|
||||
raise FSMError("builder_entry_not_found", "The selected experience no longer exists", status_code=409)
|
||||
section, entry = target
|
||||
return _begin_edit(updated, section, entry)
|
||||
if module == "builder_skill_select":
|
||||
return _process_skill_selection(updated, state, action, payload, resume_content)
|
||||
if module == SUMMARY_APPLY_MODULE:
|
||||
return apply_summary_proposal(updated, action, payload, resume_content)
|
||||
if module == "builder_next_section":
|
||||
if action != "select":
|
||||
raise FSMError("invalid_builder_choice", "Choose the next Builder action", status_code=422)
|
||||
action_value = str(payload.get("value") or "").strip()
|
||||
if action_value == "builder_recommend_skills":
|
||||
candidates = _builder_skill_candidates(updated, resume_content, skill_suggester)
|
||||
state["pending_skill_candidates"] = candidates
|
||||
_set_stream_phases(updated, "suggesting_next", "structuring")
|
||||
if not candidates:
|
||||
return Transition(
|
||||
Stage.BUILDER_CONVERSATION,
|
||||
updated,
|
||||
_next_step_turn(
|
||||
recommended_section(updated, resume_content),
|
||||
prefix=u("暂时没有新的岗位技能建议。"),
|
||||
),
|
||||
)
|
||||
return Transition(
|
||||
Stage.BUILDER_CONVERSATION,
|
||||
updated,
|
||||
assistant_turn(
|
||||
u("结合你选择的目标岗位,整理出以下待确认技能。只有你勾选并确认后,才会写入简历。"),
|
||||
[_skill_choice_card(candidates)],
|
||||
mode=ComposerMode.CHAT,
|
||||
),
|
||||
)
|
||||
if action_value == "builder_finish":
|
||||
return finish_transition(updated, resume_content)
|
||||
section = action_value
|
||||
if section not in SECTION_HEADINGS:
|
||||
raise FSMError("invalid_builder_section", "Unsupported resume section", status_code=422)
|
||||
state["active_section"] = section
|
||||
_reset_gap_state(state)
|
||||
_set_stream_phases(updated, "suggesting_next", "structuring")
|
||||
return Transition(
|
||||
Stage.BUILDER_CONVERSATION,
|
||||
updated,
|
||||
assistant_turn(
|
||||
f"请先填写这段{SECTION_HEADINGS[section]}的基础信息。",
|
||||
[_record_card(section, title=f"填写{SECTION_HEADINGS[section]}", skippable=True)],
|
||||
mode=ComposerMode.CHAT,
|
||||
),
|
||||
)
|
||||
raise FSMError("invalid_builder_choice", "Choose an experience type", status_code=422)
|
||||
|
||||
if name == "RecordFields":
|
||||
section = str(component_data.get("record_type") or state.get("active_section") or "education")
|
||||
if section not in SECTION_HEADINGS:
|
||||
raise FSMError("invalid_builder_section", "Unsupported resume section", status_code=422)
|
||||
if action == "skip":
|
||||
_clear_draft(state)
|
||||
_set_stream_phases(updated, "suggesting_next")
|
||||
return Transition(Stage.BUILDER_CONVERSATION, updated, _next_step_turn(recommended_section(updated, resume_content)), lifecycle="dismissed")
|
||||
if action != "submit":
|
||||
raise FSMError("invalid_builder_identity", "Submit or skip the experience card", status_code=422)
|
||||
fields = anchor_field_specs(section)
|
||||
required = [field["key"] for field in fields]
|
||||
submitted = {field: str(payload.get(field) or "").strip() for field in required}
|
||||
errors = record_entry_errors(submitted, required)
|
||||
if errors:
|
||||
raise FSMError("invalid_builder_identity", "Please complete the required experience fields", status_code=422, missing_fields=errors)
|
||||
base = state.get("editing_base_entry")
|
||||
entry = {**dict(base or {}), **submitted} if isinstance(base, dict) else submitted
|
||||
state["active_section"] = section
|
||||
state["identity_draft"] = entry
|
||||
state["editing_entry_id"] = component_data.get("entry_id") or state.get("editing_entry_id") or None
|
||||
_reset_gap_state(state)
|
||||
_set_stream_phases(updated, "structuring")
|
||||
return Transition(Stage.BUILDER_CONVERSATION, updated, assistant_turn(_fact_prompt(section), [], mode=ComposerMode.CHAT))
|
||||
|
||||
if name == "ExperienceConfirmCard":
|
||||
pending = state.get("pending_entry")
|
||||
if not isinstance(pending, dict):
|
||||
raise FSMError("builder_proposal_missing", "The experience proposal is no longer available")
|
||||
if action == "edit":
|
||||
state["identity_draft"] = _public_entry(pending)
|
||||
state["pending_entry"] = None
|
||||
state["revision_mode"] = True
|
||||
_reset_gap_state(state)
|
||||
_set_stream_phases(updated, "structuring")
|
||||
return Transition(
|
||||
Stage.BUILDER_CONVERSATION,
|
||||
updated,
|
||||
assistant_turn("好的,请直接补充或指出要调整的事实;我会基于原内容重新生成候选改写。", [], mode=ComposerMode.CHAT),
|
||||
)
|
||||
if action == "revise":
|
||||
instruction = str(payload.get("instruction") or "").strip()
|
||||
if not instruction:
|
||||
raise FSMError("invalid_builder_confirmation", "Provide revision guidance", status_code=422)
|
||||
return _revise_pending_candidate(updated, pending, instruction)
|
||||
if action != "confirm":
|
||||
raise FSMError("invalid_builder_confirmation", "Confirm or revise the proposed experience", status_code=422)
|
||||
entry = _public_entry(pending)
|
||||
fact_description = str(entry.get("description") or "").strip()
|
||||
proposal = pending.get("_proposal")
|
||||
if payload.get("use_optimized") and isinstance(proposal, dict):
|
||||
optimized = str(proposal.get("optimized_description") or "").strip()
|
||||
if optimized:
|
||||
entry["description"] = optimized
|
||||
entry["provenance"] = proposal.get("source") or "ai_expanded"
|
||||
entry.setdefault("provenance", "user_provided")
|
||||
content = save_entry(
|
||||
resume_content,
|
||||
str(state.get("active_section") or "education"),
|
||||
entry,
|
||||
entry_id=str(state.get("editing_entry_id") or "") or None,
|
||||
)
|
||||
section = str(state.get("active_section") or "education")
|
||||
section_items = next(
|
||||
(item.get("items") for item in content.get("sections") or [] if item.get("kind") == section),
|
||||
[],
|
||||
)
|
||||
if not isinstance(section_items, list) or not section_items:
|
||||
raise FSMError("builder_entry_not_found", "The confirmed experience could not be saved", status_code=409)
|
||||
saved_entry = next(
|
||||
(item for item in section_items if isinstance(item, dict) and item.get("id") == entry.get("id")),
|
||||
section_items[-1],
|
||||
)
|
||||
# Keep the initial ID as a temporary lookup anchor. The persistence layer
|
||||
# reconciles it to the final ID after the transition is returned.
|
||||
state["last_confirmed_entry"] = {
|
||||
"entry_id": str(saved_entry.get("id") or entry.get("id") or ""),
|
||||
"section": section,
|
||||
"fact_description": fact_description,
|
||||
}
|
||||
_clear_draft(state)
|
||||
_set_stream_phases(updated, "saving", "suggesting_next")
|
||||
return Transition(
|
||||
Stage.BUILDER_CONVERSATION,
|
||||
updated,
|
||||
_next_step_turn(recommended_section(updated, content), prefix="已写入简历。"),
|
||||
lifecycle="confirmed",
|
||||
resume_content=content,
|
||||
)
|
||||
|
||||
raise FSMError("invalid_builder_component", "This card is no longer active", status_code=422)
|
||||
Reference in New Issue
Block a user