Files
resume-agent/backend/app/fsm_enrichment.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

200 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""RESUME_ENRICHING 模块队列引擎(V1 全链路收集)。
组件事件分发见 enrichment_collectors.pyfsm.py 通过函数内 import 调用,避免循环依赖。
"""
from __future__ import annotations
from typing import Any
from .enrichment_modules import (
ENRICHMENT_MODULES,
ENRICHMENT_QUEUES,
PICKER_OPTIONS,
RECORD_KEYS,
TAG_TITLES,
ModuleSpec,
skill_suggestions,
)
from .fsm import Transition, assistant_turn, component
from .models import ComposerMode, JobType, Stage
from .record_card import record_card
def ensure_enrichment_state(profile: dict[str, Any]) -> dict[str, Any]:
"""惰性创建 records/tags/enrichment;旧会话现场建队列。"""
records = profile.setdefault("records", {})
for key in RECORD_KEYS:
records.setdefault(key, [])
profile.setdefault("tags", {"skills": [], "certificates": []})
enrichment = profile.setdefault("enrichment", {})
if "queue" not in enrichment:
try:
job_type = JobType(str(profile.get("job_type")))
except ValueError as exc:
raise ValueError("profile contains an unsupported job_type") from exc
enrichment["queue"] = list(ENRICHMENT_QUEUES[job_type])
enrichment.setdefault("index", 0)
enrichment.setdefault("current", None)
enrichment.setdefault("skipped", [])
enrichment.setdefault("completed", [])
enrichment.setdefault("module_draft", {})
enrichment.setdefault("custom_mode", False)
return profile
def current_module(profile: dict[str, Any]) -> ModuleSpec | None:
name = (profile.get("enrichment") or {}).get("current")
return ENRICHMENT_MODULES.get(name) if name else None
def enrichment_progress(profile: dict[str, Any]) -> dict[str, Any]:
enrichment = profile.get("enrichment") or {}
total = len(enrichment.get("queue") or [])
completed = len(enrichment.get("completed") or [])
# total 固定为队列长度;skip 只切换到下一模块,不推进度(PR 评审结论)
ratio = completed / total if total > 0 else 1.0
return {
"completed": completed,
"skipped": len(enrichment.get("skipped") or []),
"total": total,
"ratio": ratio,
}
def begin_enrichment(profile: dict[str, Any]) -> Transition:
ensure_enrichment_state(profile)
enrichment = profile["enrichment"]
queue = enrichment["queue"]
if enrichment["index"] >= len(queue):
from .enrichment_custom import custom_card_picker_transition
return custom_card_picker_transition(profile)
profile["enrichment_finished"] = False
spec = ENRICHMENT_MODULES[queue[enrichment["index"]]]
enrichment["current"] = spec.name
enrichment["module_draft"] = {}
return begin_module(profile, spec)
def progress_block(profile: dict[str, Any], spec: ModuleSpec) -> dict[str, Any]:
progress = enrichment_progress(profile)
return component(
"ProgressCard",
module=spec.name,
completed=progress["completed"],
skipped=progress["skipped"],
total=progress["total"],
percent=round(progress["ratio"] * 100),
actions=["defer"],
)
def begin_module(profile: dict[str, Any], spec: ModuleSpec) -> Transition:
blocks = [progress_block(profile, spec)]
if spec.name in PICKER_OPTIONS and not profile["enrichment"]["module_draft"].get("record_type"):
options = [{"value": v, "label": l} for v, l in PICKER_OPTIONS[spec.name]]
blocks.append(
component("ChoiceChips", module=spec.name, field="record_type", title=spec.prompt, options=options, skippable=True)
)
elif spec.kind in {"record_fields", "anchor_note"}:
blocks.append(record_card(profile, spec))
elif spec.kind == "record_form":
blocks.append(component("CompetitionFields", module=spec.name, title=spec.prompt))
elif spec.kind == "tags":
field = spec.optional_fields[0]
profile["enrichment"]["module_draft"] = {"field": field}
data = {
"module": spec.name,
"field": field,
"title": TAG_TITLES[field],
"description": spec.prompt,
}
if field == "skills":
data["suggestions"] = skill_suggestions(profile.get("target_position"), profile)
blocks.append(component("TagsInput", **data))
transition = Transition(
Stage.RESUME_ENRICHING,
profile,
assistant_turn(spec.prompt, blocks, mode=ComposerMode.UI_ONLY),
)
if spec.kind == "tags" and spec.optional_fields[0] == "skills":
transition.suggest_skills = True
return transition
def advance_or_finish(profile: dict[str, Any], *, refresh: bool = False) -> Transition:
enrichment = profile["enrichment"]
enrichment["index"] += 1
enrichment["module_draft"] = {}
if enrichment["index"] >= len(enrichment["queue"]):
from .enrichment_custom import custom_card_picker_transition
transition = custom_card_picker_transition(profile)
else:
spec = ENRICHMENT_MODULES[enrichment["queue"][enrichment["index"]]]
enrichment["current"] = spec.name
transition = begin_module(profile, spec)
transition.refresh_resume = refresh
return transition
def defer_enrichment(profile: dict[str, Any]) -> Transition:
profile["enrichment"]["current"] = None
profile["enrichment"]["module_draft"] = {}
profile["enrichment_finished"] = True
return Transition(
Stage.CONTENT_READY,
profile,
assistant_turn(
"好的,随时可以回来继续完善。",
[component("ContentReadyCard", can_continue=True)],
mode=ComposerMode.UI_ONLY,
),
lifecycle="confirmed",
)
def skip_module(profile: dict[str, Any], spec: ModuleSpec) -> Transition:
if profile["enrichment"].get("custom_mode"):
from .enrichment_custom import custom_card_picker_transition
return custom_card_picker_transition(profile)
skipped = profile["enrichment"]["skipped"]
if spec.name not in skipped:
skipped.append(spec.name)
return advance_or_finish(profile)
def mark_completed(profile: dict[str, Any], name: str) -> None:
completed = profile["enrichment"]["completed"]
if name not in completed:
completed.append(name)
def add_another_transition(profile: dict[str, Any], spec: ModuleSpec, *, refresh: bool = False) -> Transition:
"""multi 模块确认一段后的"再添加/下一项"卡片。"""
transition = Transition(
Stage.RESUME_ENRICHING,
profile,
assistant_turn(
"已写入简历。",
[
progress_block(profile, spec),
component(
"AddAnother",
module=spec.name,
title="还要再添加一段吗?",
options=[
{"value": "again", "label": "再添加一段"},
{"value": "next", "label": "进入下一项"},
],
),
],
mode=ComposerMode.UI_ONLY,
),
lifecycle="confirmed",
)
transition.refresh_resume = refresh
return transition