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,154 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from .fsm import FSMError, Transition, assistant_turn, component
|
||||
from .fsm_enrichment import (
|
||||
add_another_transition,
|
||||
advance_or_finish,
|
||||
begin_module,
|
||||
current_module,
|
||||
mark_completed,
|
||||
)
|
||||
from .models import ComposerMode, Stage
|
||||
from .services import ExtractedExperience
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RewriteConfirmationTransition:
|
||||
stage: Stage
|
||||
profile: dict[str, Any]
|
||||
turn: dict[str, Any]
|
||||
lifecycle: str = "submitted"
|
||||
create_draft: bool = False
|
||||
resume_content: dict[str, Any] | None = None
|
||||
refresh_resume: bool = False
|
||||
|
||||
|
||||
def prepare_rewrite_confirmation(
|
||||
profile: dict[str, Any],
|
||||
extraction: ExtractedExperience,
|
||||
proposal: dict[str, Any],
|
||||
*,
|
||||
module: str | None = None,
|
||||
record_type: str | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
updated = deepcopy(profile)
|
||||
pending = extraction.to_dict()
|
||||
if module is not None:
|
||||
pending["module"] = module
|
||||
if record_type is not None:
|
||||
pending["record_type"] = record_type
|
||||
pending["description"] = extraction.raw_text
|
||||
optimized = str(proposal.get("optimized_description") or "").strip()
|
||||
if optimized and optimized != extraction.raw_text.strip():
|
||||
pending_proposal = {
|
||||
"optimized_description": optimized,
|
||||
"changes": proposal.get("changes") or [],
|
||||
"source": proposal.get("source", "ai_expanded"),
|
||||
}
|
||||
for key in ("generation_source", "fallback_reason"):
|
||||
if proposal.get(key):
|
||||
pending_proposal[key] = proposal[key]
|
||||
pending["pending_proposal"] = pending_proposal
|
||||
updated["pending_experience"] = pending
|
||||
summary = _confirmation_summary(extraction)
|
||||
turn = assistant_turn(
|
||||
"我已把这段事实整理成正式简历语言,请确认后再写入简历。",
|
||||
[
|
||||
component(
|
||||
"ExperienceConfirmCard",
|
||||
title="确认 AI 改写",
|
||||
description="只在内容准确时加入简历;需要调整可返回继续描述。",
|
||||
value=summary,
|
||||
ai_proposal=pending.get("pending_proposal"),
|
||||
confirmation_kind="rewrite",
|
||||
)
|
||||
],
|
||||
mode=ComposerMode.UI_ONLY,
|
||||
)
|
||||
return updated, turn
|
||||
|
||||
|
||||
def process_rewrite_confirmation(
|
||||
profile: dict[str, Any], action: str, payload: dict[str, Any] | None = None
|
||||
) -> Transition | RewriteConfirmationTransition:
|
||||
updated = deepcopy(profile)
|
||||
normalized = action.strip().lower()
|
||||
payload = payload or {}
|
||||
pending = updated.get("pending_experience")
|
||||
module = pending.get("module") if isinstance(pending, dict) else None
|
||||
if normalized in {"edit", "revise", "edit_anchor"}:
|
||||
updated.pop("pending_experience", None)
|
||||
spec = _module_spec(updated, module)
|
||||
if spec is not None:
|
||||
return begin_module(updated, spec)
|
||||
return RewriteConfirmationTransition(
|
||||
Stage.RESUME_ENRICHING,
|
||||
updated,
|
||||
assistant_turn(
|
||||
"这版暂不写入。请补充或纠正事实,我会重新整理。",
|
||||
[],
|
||||
mode=ComposerMode.CHAT,
|
||||
),
|
||||
)
|
||||
if normalized not in {"confirm", "confirm_anchor", "confirm_rewrite"}:
|
||||
raise FSMError("invalid_action", "Confirm or revise the proposed rewrite")
|
||||
experience = updated.pop("pending_experience", None)
|
||||
if not isinstance(experience, dict):
|
||||
raise FSMError("rewrite_not_pending", "No proposed rewrite is waiting for confirmation")
|
||||
proposal = experience.pop("pending_proposal", None)
|
||||
if isinstance(proposal, dict) and payload.get("use_optimized") is True:
|
||||
experience["description"] = str(proposal.get("optimized_description") or "").strip()
|
||||
experience["provenance"] = proposal.get("source", "ai_expanded")
|
||||
record_type = experience.get("record_type")
|
||||
if record_type:
|
||||
record = {**experience, "confirmed": True, "rewrite_confirmed": True}
|
||||
updated.setdefault("records", {}).setdefault(record_type, []).append(record)
|
||||
else:
|
||||
updated.setdefault("experiences", []).append(experience)
|
||||
updated["ai_rewrites_confirmed"] = True
|
||||
spec = _module_spec(updated, module)
|
||||
if spec is not None:
|
||||
if spec.multi:
|
||||
transition = add_another_transition(updated, spec)
|
||||
else:
|
||||
mark_completed(updated, spec.name)
|
||||
transition = advance_or_finish(updated)
|
||||
transition.refresh_resume = True
|
||||
return transition
|
||||
return RewriteConfirmationTransition(
|
||||
Stage.CONTENT_READY,
|
||||
updated,
|
||||
assistant_turn(
|
||||
"已确认并写入简历。",
|
||||
[
|
||||
component(
|
||||
"ContentReadyCard",
|
||||
formal_content_ready=True,
|
||||
actions=["continue_enriching", "finish_enrichment"],
|
||||
)
|
||||
],
|
||||
mode=ComposerMode.HYBRID,
|
||||
),
|
||||
lifecycle="confirmed",
|
||||
refresh_resume=True,
|
||||
)
|
||||
|
||||
|
||||
def _module_spec(profile: dict[str, Any], module: Any):
|
||||
if not module:
|
||||
return None
|
||||
spec = current_module(profile)
|
||||
return spec if spec is not None and spec.name == module else None
|
||||
|
||||
|
||||
def _confirmation_summary(extraction: ExtractedExperience) -> dict[str, Any]:
|
||||
return {
|
||||
"title": extraction.title,
|
||||
"organization": extraction.organization,
|
||||
"role": extraction.role,
|
||||
"description": extraction.raw_text,
|
||||
}
|
||||
Reference in New Issue
Block a user