"""Builder 个人总结再生入口:finish 按钮与对话指令统一走"显式请求即重生成"。 agent 侧只在总结缺失或 stale 时才生成(避免自动覆盖用户手工文本);用户的显式 请求必须先把现有总结标记为 stale,让既有闸门放行。对话路径无法直接写简历 (add_message 不合并 resume_content),所以走"生成候选 → ChoiceChips 确认 → 组件事件写入"的既有 Builder 模式。 """ from __future__ import annotations import re from typing import Any from ..fsm import FSMError, Transition, assistant_turn, component from ..models import ComposerMode, Stage from ..resume_document import mark_profile_summary_stale, set_generated_profile_summary from .constants import u from .state import _set_stream_phases, ensure_builder_state SUMMARY_APPLY_MODULE = "builder_summary_apply" _REGEN_VERB = re.compile(r"(重新|再次|再来|重写|更新|刷新|换|再).{0,6}总结") _ASK_GENERATE = re.compile(r"(?:帮我|请|我要|我想|给我).{0,6}生成.{0,4}总结|^生成.{0,4}总结") def requests_summary_regen(content: str) -> bool: """"重新生成个人总结"类指令;提供总结原文或陈述事实的消息不算。""" normalized = re.sub(r"[\s,,。;;!!??]", "", content.casefold()) if "总结" not in normalized or "总结是" in normalized: return False return bool(_REGEN_VERB.search(normalized) or _ASK_GENERATE.search(normalized)) def finish_transition(profile: dict[str, Any], resume_content: dict[str, Any]) -> Transition: """"完成并生成个人总结":已有总结也强制重生成(先标记 stale 放行闸门)。""" _set_stream_phases(profile, "saving") return Transition( Stage.BUILDER_CONVERSATION, profile, assistant_turn( u("好的,已根据当前简历预览信息生成个人总结,内容仍可在右侧预览中编辑。"), [], mode=ComposerMode.CHAT, ), resume_content=mark_profile_summary_stale(resume_content), generate_profile_summary=True, ) def summary_regen_turn(agent: Any, profile: dict[str, Any], resume_content: dict[str, Any]) -> Transition: """对话"重新生成个人总结":立即生成候选文本,确认后经组件事件写入简历。""" try: proposal = agent.profile_summary_generator.generate(resume_content) except Exception: return Transition( Stage.BUILDER_CONVERSATION, profile, assistant_turn(u("个人总结生成失败,请稍后重试。"), [], mode=ComposerMode.CHAT), ) ensure_builder_state(profile)["pending_summary_proposal"] = proposal card = component( "ChoiceChips", module=SUMMARY_APPLY_MODULE, options=[ {"value": "apply", "label": u("写入简历")}, {"value": "dismiss", "label": u("暂不写入")}, ], ) return Transition( Stage.BUILDER_CONVERSATION, profile, assistant_turn(u("好的,已根据当前简历内容重新生成个人总结:\n") + proposal, [card], mode=ComposerMode.CHAT), ) def apply_summary_proposal( profile: dict[str, Any], action: str, payload: dict[str, Any], resume_content: dict[str, Any] ) -> Transition: """确认卡事件:apply 写入候选总结(随组件事件合并进简历),否则丢弃。""" if action != "select": raise FSMError("invalid_builder_choice", "Choose whether to apply the summary", status_code=422) proposal = str(ensure_builder_state(profile).pop("pending_summary_proposal", "") or "").strip() if str(payload.get("value") or "") != "apply" or not proposal: return Transition( Stage.BUILDER_CONVERSATION, profile, assistant_turn(u("好的,保留当前个人总结。"), [], mode=ComposerMode.CHAT), ) content = set_generated_profile_summary(mark_profile_summary_stale(resume_content), proposal, replace_stale=True) return Transition( Stage.BUILDER_CONVERSATION, profile, assistant_turn(u("已写入新的个人总结,仍可在右侧预览中编辑。"), [], mode=ComposerMode.CHAT), lifecycle="confirmed", resume_content=content, )