generated from kgod/ai-review-template
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
230 lines
5.9 KiB
Python
230 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import datetime
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
|
|
from .resume_api_models import (
|
|
BusinessResume,
|
|
OptimizeEntryRequest,
|
|
OptimizeRequest,
|
|
ResumePatchOperation,
|
|
ResumePatchRequest,
|
|
)
|
|
|
|
|
|
PHONE_PATTERN = re.compile(r"^1[3-9]\d{9}$")
|
|
|
|
|
|
class Stage(StrEnum):
|
|
PRIVACY_CONSENT = "PRIVACY_CONSENT"
|
|
RESUME_SOURCE_SELECT = "RESUME_SOURCE_SELECT"
|
|
RESUME_IMPORT_UPLOAD = "RESUME_IMPORT_UPLOAD"
|
|
PHONE_SELECTION = "PHONE_SELECTION"
|
|
MANUAL_PHONE_INPUT = "MANUAL_PHONE_INPUT"
|
|
PERSONAL_INFO = "PERSONAL_INFO"
|
|
NAME_CAPTURE = "NAME_CAPTURE"
|
|
JOB_TYPE_SELECT = "JOB_TYPE_SELECT"
|
|
TARGET_POSITION = "TARGET_POSITION"
|
|
TARGET_POSITION_MAJOR = "TARGET_POSITION_MAJOR"
|
|
TARGET_POSITION_RECOMMENDATION = "TARGET_POSITION_RECOMMENDATION"
|
|
ANCHOR_TYPE_SELECT = "ANCHOR_TYPE_SELECT"
|
|
ANCHOR_COLLECTING = "ANCHOR_COLLECTING"
|
|
CONTENT_DISAMBIGUATION = "CONTENT_DISAMBIGUATION"
|
|
ANCHOR_CONFIRM = "ANCHOR_CONFIRM"
|
|
MINIMUM_READY = "MINIMUM_READY"
|
|
RESUME_CREATING = "RESUME_CREATING"
|
|
CREATE_FAILED = "CREATE_FAILED"
|
|
CONTENT_READY = "CONTENT_READY"
|
|
RESUME_ENRICHING = "RESUME_ENRICHING"
|
|
BUILDER_CONVERSATION = "BUILDER_CONVERSATION"
|
|
|
|
|
|
class JobType(StrEnum):
|
|
CAMPUS = "campus"
|
|
SOCIAL = "social"
|
|
INTERNSHIP = "internship"
|
|
|
|
|
|
class AnchorType(StrEnum):
|
|
EDUCATION = "education"
|
|
WORK_EXPERIENCE = "work_experience"
|
|
INTERNSHIP_EXPERIENCE = "internship_experience"
|
|
PROJECT_EXPERIENCE = "project_experience"
|
|
|
|
|
|
class TurnRole(StrEnum):
|
|
USER = "user"
|
|
ASSISTANT = "assistant"
|
|
SYSTEM = "system"
|
|
|
|
|
|
class ComposerMode(StrEnum):
|
|
UI_ONLY = "ui_only"
|
|
CHAT = "chat"
|
|
HYBRID = "hybrid"
|
|
|
|
|
|
class BlockType(StrEnum):
|
|
TEXT = "text"
|
|
COMPONENT = "component"
|
|
RESUME_PATCH = "resume_patch"
|
|
STATUS = "status"
|
|
ERROR = "error"
|
|
|
|
|
|
class ComponentLifecycle(StrEnum):
|
|
ACTIVE = "active"
|
|
SUBMITTED = "submitted"
|
|
CONFIRMED = "confirmed"
|
|
DISMISSED = "dismissed"
|
|
SUPERSEDED = "superseded"
|
|
FAILED = "failed"
|
|
|
|
|
|
class ComponentBlock(BaseModel):
|
|
id: str
|
|
type: BlockType
|
|
lifecycle: ComponentLifecycle
|
|
data: dict[str, Any] = Field(default_factory=dict)
|
|
version: int = 1
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class ConversationTurn(BaseModel):
|
|
id: str
|
|
sequence: int
|
|
role: TurnRole
|
|
content: str | None = None
|
|
composer_mode: ComposerMode
|
|
blocks: list[ComponentBlock] = Field(default_factory=list)
|
|
created_at: datetime
|
|
|
|
|
|
class SessionView(BaseModel):
|
|
id: str
|
|
stage: Stage
|
|
revision: int
|
|
job_type: JobType | None = None
|
|
anchor_type: AnchorType | None = None
|
|
masked_phone: str | None = None
|
|
phone_source: str | None = None
|
|
name: str | None = None
|
|
draft_id: str | None = None
|
|
resume_id: str | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class GateView(BaseModel):
|
|
allowed: bool
|
|
formal_content_ready: bool = False
|
|
anchor_type: AnchorType | None = None
|
|
required_fields: list[str] = Field(default_factory=list)
|
|
missing_fields: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class TimelineResponse(BaseModel):
|
|
session_id: str
|
|
session: SessionView
|
|
turns: list[ConversationTurn]
|
|
stage: Stage
|
|
revision: int
|
|
draft_id: str | None = None
|
|
resume_id: str | None = None
|
|
resume: BusinessResume | None = None
|
|
missing_fields: list[str] = Field(default_factory=list)
|
|
gate: GateView
|
|
trace_id: str
|
|
|
|
|
|
class ActionResponse(BaseModel):
|
|
session_id: str
|
|
stage: Stage
|
|
revision: int
|
|
turn: ConversationTurn | None = None
|
|
timeline: list[ConversationTurn] | None = None
|
|
draft_id: str | None = None
|
|
resume_id: str | None = None
|
|
resume: BusinessResume | None = None
|
|
missing_fields: list[str] = Field(default_factory=list)
|
|
builder_stream_phases: list[str] = Field(default_factory=list)
|
|
gate: GateView
|
|
trace_id: str
|
|
|
|
|
|
class CreateSessionRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
account_phone: str | None = None
|
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
@field_validator("account_phone")
|
|
@classmethod
|
|
def validate_account_phone(cls, value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
normalized = re.sub(r"[\s-]", "", value)
|
|
if normalized.startswith("+86"):
|
|
normalized = normalized[3:]
|
|
if not PHONE_PATTERN.fullmatch(normalized):
|
|
raise ValueError("phone must be a valid mainland China mobile number")
|
|
return normalized
|
|
|
|
|
|
class ComponentEventRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
component_id: str
|
|
event: str | None = None
|
|
event_type: str | None = None
|
|
payload: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
@model_validator(mode="after")
|
|
def require_event(self) -> "ComponentEventRequest":
|
|
if not (self.event or self.event_type):
|
|
raise ValueError("event is required")
|
|
return self
|
|
|
|
@property
|
|
def action(self) -> str:
|
|
return (self.event or self.event_type or "").strip().lower()
|
|
|
|
|
|
class MessageRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
content: str = Field(min_length=1, max_length=8_000)
|
|
|
|
@field_validator("content")
|
|
@classmethod
|
|
def strip_content(cls, value: str) -> str:
|
|
value = value.strip()
|
|
if not value:
|
|
raise ValueError("content cannot be blank")
|
|
return value
|
|
|
|
|
|
class CreateResumeRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
idempotency_key: str | None = Field(default=None, max_length=128)
|
|
|
|
|
|
|
|
class CreateResumeResponse(ActionResponse):
|
|
created: bool
|
|
resume: BusinessResume
|
|
|
|
|
|
class ErrorDetail(BaseModel):
|
|
code: str
|
|
message: str
|
|
stage: Stage | None = None
|
|
missing_fields: list[str] = Field(default_factory=list)
|
|
trace_id: str
|