generated from kgod/ai-review-template
192 lines
9.4 KiB
Python
192 lines
9.4 KiB
Python
"""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)
|