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

99 lines
4.6 KiB
Python

"""Turn and card builders for the Builder conversation."""
from __future__ import annotations
from typing import Any
from ..fsm import anchor_field_specs, assistant_turn, component
from ..models import ComposerMode
from .constants import SECTION_HEADINGS, SECTION_PRIORITY, u
from .state import _completed_sections, _set_stream_phases, ensure_builder_state
def recommended_section(profile: dict[str, Any], resume_content: dict[str, Any] | None = None) -> str:
priorities = SECTION_PRIORITY.get(str(profile.get("job_type") or "campus"), SECTION_PRIORITY["campus"])
completed = _completed_sections(resume_content or {})
return next((section for section in priorities if section not in completed), priorities[0])
def welcome_turn(
profile: dict[str, Any],
resume_id: str | None,
*,
imported: bool = False,
resume_content: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], dict[str, Any]]:
state = ensure_builder_state(profile)
state["imported"] = imported
if imported:
turn = assistant_turn("简历已经导入。你想先修改哪一段经历,还是补充一段新的内容?", [], mode=ComposerMode.CHAT)
_set_stream_phases(profile, "suggesting_next")
return profile, turn
turn = _next_step_turn(recommended_section(profile, resume_content))
_set_stream_phases(profile, "suggesting_next")
return profile, turn
def _next_step_turn(section: str, *, prefix: str = "") -> dict[str, Any]:
lead = f"{prefix} " if prefix else ""
return assistant_turn(
f"{lead}接下来建议补充{SECTION_HEADINGS[section]}。你想继续这类经历,还是改选其他经历类型?",
[_section_choice_card(section)],
mode=ComposerMode.CHAT,
)
def _section_choice_card(recommended: str) -> dict[str, Any]:
return component(
"ChoiceChips",
module="builder_next_section",
title=u("选择下一步"),
description=f"{u('建议先补充')}{SECTION_HEADINGS[recommended]}{u(',也可以换一种经历、推荐岗位技能,或直接完成。')}",
options=[
*({"value": kind, "label": heading} for kind, heading in SECTION_HEADINGS.items()),
{"value": "builder_recommend_skills", "label": u("推荐岗位技能")},
{"value": "builder_finish", "label": u("完成并生成个人总结")},
],
value=recommended,
)
def _entry_choice_card(matches: list[tuple[dict[str, Any], dict[str, Any]]]) -> dict[str, Any]:
options = [{"value": str(entry.get("id") or ""), "label": _entry_label(entry, str(section.get("kind") or ""))} for section, entry in matches]
return component("ChoiceChips", module="builder_entry_select", title="选择要修改的经历", options=options)
def _record_card(section: str, *, title: str, value: dict[str, Any] | None = None, entry_id: Any = None, skippable: bool = False) -> dict[str, Any]:
props: dict[str, Any] = {
"module": "builder_identity",
"record_type": section,
"title": title,
"fields": anchor_field_specs(section),
"show_description": False,
"require_description": False,
"skippable": skippable,
"skip_label": "稍后补充",
}
if value:
props["value"] = value
if entry_id:
props["entry_id"] = entry_id
return component("RecordFields", **props)
def _fact_prompt(section: str) -> str:
prompts = {
"education": "请补充这段教育经历的真实信息,例如课程项目、竞赛、实践或学习成果;没有也可以直接说没有。",
"campus_experience": "请补充你实际承担的职责,或规模和结果中的一两项;没有也可以直接说没有。",
"project_experience": "请补充个人动作、方法或工具,以及交付物、规模或量化结果中的一两项;没有也可以直接说没有。",
"work_experience": "请补充个人动作、方法或工具,以及交付物、规模或量化结果中的一两项;没有也可以直接说没有。",
"internship_experience": "请补充个人动作、方法或工具,以及交付物、规模或量化结果中的一两项;没有也可以直接说没有。",
}
return prompts[section]
def _entry_label(entry: dict[str, Any], kind: str) -> str:
identity = next((str(entry.get(key) or "").strip() for key in ("school", "company", "project_name", "organization") if str(entry.get(key) or "").strip()), SECTION_HEADINGS.get(kind, "经历"))
role = next((str(entry.get(key) or "").strip() for key in ("major", "position", "project_role", "role") if str(entry.get(key) or "").strip()), "")
return f"{identity} · {role}" if role else identity