Files
resume-agent/backend/app/builder_conversation/followups.py
T
hypandClaude ae2d9b128d feat: builder 简历生成 + 轻度优化 + 简历导入交付副本
自内部仓库剥离深度优化与 RAG 知识库后的交付版本:
- Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强)
- 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护
- 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏
- PostgreSQL 运行时 + Alembic 迁移链

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 11:08:30 +08:00

161 lines
6.4 KiB
Python

"""Continuation and revision turns for already-confirmed Builder entries."""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
from ..fsm import FIELD_LABELS, Transition, assistant_turn, component
from ..models import ComposerMode, Stage
from .candidate import _candidate_rewrite
from .constants import SECTION_HEADINGS, u
from .expander_provider import shared_expander
from .predicates import _entry_by_id
from .state import (
_highlights,
_merge_fact_text,
_public_entry,
_reset_gap_state,
_set_stream_phases,
ensure_builder_state,
)
def reconcile_last_confirmed_entry(profile: dict[str, Any], resume_content: dict[str, Any]) -> None:
"""Keep Builder's continuation pointer aligned after document ID reconciliation."""
state = ensure_builder_state(profile)
reference = state.get("last_confirmed_entry")
if not isinstance(reference, dict):
return
entry_id = str(reference.get("entry_id") or "")
if entry_id and _entry_by_id(resume_content, entry_id) is not None:
return
section_kind = str(reference.get("section") or "")
section = next(
(item for item in resume_content.get("sections") or [] if item.get("kind") == section_kind),
None,
)
entries = section.get("items") if isinstance(section, dict) else None
if isinstance(entries, list) and entries and isinstance(entries[-1], dict):
reference["entry_id"] = str(entries[-1].get("id") or "")
return
state["last_confirmed_entry"] = None
def _continue_recent_entry(
agent: Any, profile: dict[str, Any], content: str, resume_content: dict[str, Any]
) -> Transition | None:
state = ensure_builder_state(profile)
reference = state.get("last_confirmed_entry")
if not isinstance(reference, dict):
return None
entry_id = str(reference.get("entry_id") or "").strip()
target = _entry_by_id(resume_content, entry_id)
if not entry_id or target is None:
state["last_confirmed_entry"] = None
return None
section_data, saved_entry = target
section = str(section_data.get("kind") or reference.get("section") or "")
if section not in SECTION_HEADINGS:
return None
entry = _public_entry(saved_entry)
original_facts = str(reference.get("fact_description") or entry.get("description") or "").strip()
entry["description"] = _merge_fact_text(original_facts, content.strip())
entry["highlights"] = _highlights(str(entry["description"]))
entry["_proposal"] = _candidate_rewrite(agent, profile, entry, section)
state["active_section"] = section
state["identity_draft"] = _public_entry(saved_entry)
state["editing_base_entry"] = _public_entry(saved_entry)
state["editing_entry_id"] = entry_id
state["pending_entry"] = entry
state["selection_candidates"] = []
_reset_gap_state(state)
_set_stream_phases(profile, "structuring", "rewriting")
return Transition(
Stage.BUILDER_CONVERSATION,
profile,
assistant_turn(
"好的,已收到这条补充信息。我已基于原内容重新整理候选改写,尚未写入简历。请确认后再保存。",
[
component(
"ExperienceConfirmCard",
title="确认更新这段经历",
value=entry,
labels=FIELD_LABELS,
ai_proposal=entry["_proposal"],
)
],
mode=ComposerMode.CHAT,
),
)
def _regenerate_entry_candidate(
agent: Any,
profile: dict[str, Any],
section_data: dict[str, Any],
saved_entry: dict[str, Any],
instruction: str,
) -> Transition:
"""Re-run the light rewrite for a confirmed entry (e.g. "帮我重新优化这段")."""
state = ensure_builder_state(profile)
section = str(section_data.get("kind") or "")
entry = _public_entry(saved_entry)
entry["_proposal"] = _candidate_rewrite(agent, profile, entry, section, instruction=instruction)
state["active_section"] = section
state["identity_draft"] = _public_entry(saved_entry)
state["editing_base_entry"] = _public_entry(saved_entry)
state["editing_entry_id"] = saved_entry.get("id")
state["pending_entry"] = entry
state["selection_candidates"] = []
_reset_gap_state(state)
_set_stream_phases(profile, "structuring", "rewriting")
return Transition(
Stage.BUILDER_CONVERSATION,
profile,
assistant_turn(
"好的,已按你的要求重新生成候选改写,尚未写入简历。请在原始内容与候选稿之间选择。",
[
component(
"ExperienceConfirmCard",
title="确认更新这段经历",
value=entry,
labels=FIELD_LABELS,
ai_proposal=entry["_proposal"],
)
],
mode=ComposerMode.CHAT,
),
)
def _redisplay_revision_candidate(agent: Any, profile: dict[str, Any], instruction: str) -> Transition:
state = ensure_builder_state(profile)
section = str(state.get("active_section") or "education")
entry = _public_entry(dict(state.get("identity_draft") or {}))
entry["_proposal"] = _candidate_rewrite(agent, profile, entry, section, instruction=instruction, ensure_facts=True)
state["pending_entry"] = entry
state["revision_mode"] = False
_set_stream_phases(profile, "structuring", "rewriting")
return Transition(
Stage.BUILDER_CONVERSATION,
profile,
assistant_turn(
u("好的,已理解你的调整说明。我会保留这段已确认的事实,并重新展示候选改写供你确认。"),
[component("ExperienceConfirmCard", title=u("确认更新这段经历"), value=entry, labels=FIELD_LABELS, ai_proposal=entry["_proposal"])],
mode=ComposerMode.CHAT,
),
)
def _revise_pending_candidate(profile: dict[str, Any], pending: dict[str, Any], instruction: str) -> Transition:
"""Regenerate the pending proposal with guidance (confirm card's revise action).
Card events carry no agent reference, so the shared expander is used.
"""
state = ensure_builder_state(profile)
state["identity_draft"] = _public_entry(pending)
agent = SimpleNamespace(expander=shared_expander())
return _redisplay_revision_candidate(agent, profile, instruction)