generated from kgod/ai-review-template
200 lines
7.1 KiB
Python
200 lines
7.1 KiB
Python
"""RESUME_ENRICHING 模块队列引擎(V1 全链路收集)。
|
||
|
||
组件事件分发见 enrichment_collectors.py;fsm.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
|