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