Files
resume-agent/backend/app/fsm_basics.py
T

152 lines
5.5 KiB
Python

"""基本信息阶段的卡片与校验:个人信息(PERSONAL_INFO)与意向职位(TARGET_POSITION)。
独立成模块以控制 fsm.py 行数;fsm.py 通过函数内 import 调用,避免循环依赖。
"""
from __future__ import annotations
from typing import Any
from .fsm import FSMError, Transition, assistant_turn, component
from .models import ComposerMode, Stage
from .validators import valid_email, valid_url
PERSONAL_INFO_FIELDS: list[dict[str, Any]] = [
{"key": "name", "label": "姓名", "kind": "text", "required": True},
{"key": "email", "label": "邮箱", "kind": "text", "required": True},
{"key": "city", "label": "所在地", "kind": "text", "required": False},
{"key": "portfolio_url", "label": "作品集链接", "kind": "text", "required": False},
]
TARGET_POSITION_FIELDS: list[dict[str, Any]] = [
{"key": "target_position", "label": "意向职位", "kind": "text", "required": True},
]
TARGET_POSITION_MAJOR_FIELDS: list[dict[str, Any]] = [
{"key": "major", "label": "专业", "kind": "text", "required": True},
{"key": "interests", "label": "感兴趣的方向", "kind": "text", "required": False},
]
def personal_info_transition(profile: dict[str, Any]) -> Transition:
return Transition(
Stage.PERSONAL_INFO,
profile,
assistant_turn(
"先填写你的个人信息,姓名和邮箱将用于简历抬头。",
[
component(
"RecordFields",
title="填写个人信息",
fields=PERSONAL_INFO_FIELDS,
show_description=False,
skippable=False,
)
],
mode=ComposerMode.UI_ONLY,
),
)
def validate_personal_info(payload: dict[str, Any]) -> dict[str, Any]:
"""姓名和邮箱必填;所在地和作品集链接选填。"""
name = str(payload.get("name") or "").strip()
email = str(payload.get("email") or "").strip()
city = str(payload.get("city") or "").strip()
portfolio_url = str(payload.get("portfolio_url") or "").strip()
missing: list[str] = []
if not name or len(name) > 64:
missing.append("name")
if not valid_email(email):
missing.append("email")
if portfolio_url and not valid_url(portfolio_url):
missing.append("portfolio_url")
if missing:
raise FSMError(
"invalid_personal_info",
"请填写姓名、有效邮箱,并确认作品集链接以 http(s):// 开头",
status_code=422,
missing_fields=missing,
)
return {
"name": name,
"email": email,
"city": city or None,
"portfolio_url": portfolio_url or None,
}
def target_position_transition(profile: dict[str, Any]) -> Transition:
return Transition(
Stage.TARGET_POSITION,
profile,
assistant_turn(
"先确认你是否已有明确的意向职位。",
[
component(
"ChoiceChips",
title="意向职位",
description="已有方向可直接填写;暂不确定时,先根据专业获得职位建议。",
options=[
{"value": "known", "label": "我有明确意向职位"},
{"value": "explore", "label": "暂不确定,想先获取建议"},
],
skippable=False,
)
],
mode=ComposerMode.UI_ONLY,
),
)
def target_position_input_transition(profile: dict[str, Any]) -> Transition:
return Transition(
Stage.TARGET_POSITION,
profile,
assistant_turn(
"填写一个意向职位,后续的优化和技能推荐会参考它。",
[component("RecordFields", title="填写意向职位", fields=TARGET_POSITION_FIELDS, show_description=False)],
mode=ComposerMode.UI_ONLY,
),
)
def target_position_major_transition(profile: dict[str, Any]) -> Transition:
return Transition(
Stage.TARGET_POSITION_MAJOR,
profile,
assistant_turn(
"填写专业和可选兴趣方向,我会给出可选择的职位建议。",
[component("RecordFields", title="专业与方向", fields=TARGET_POSITION_MAJOR_FIELDS, show_description=False)],
mode=ComposerMode.UI_ONLY,
),
)
def target_position_recommendation_transition(profile: dict[str, Any]) -> Transition:
suggestions = profile.get("target_position_suggestions") or []
options = [
{"value": item["title"], "label": item["title"], "description": item.get("reason")}
for item in suggestions if isinstance(item, dict) and item.get("title")
]
options.append({"value": "manual", "label": "手动填写职位"})
return Transition(
Stage.TARGET_POSITION_RECOMMENDATION,
profile,
assistant_turn(
"以下职位仅供探索参考,选择后仍可在简历预览中修改。",
[component("ChoiceChips", title="职位建议", options=options, skippable=False)],
mode=ComposerMode.UI_ONLY,
),
)
def validate_target_position(payload: dict[str, Any]) -> str:
value = str(payload.get("target_position") or "").strip()
if not value or len(value) > 32:
raise FSMError(
"invalid_target_position",
"请填写意向职位(32 字以内),或点击「暂时跳过」。",
status_code=422,
missing_fields=["target_position"],
)
return value