generated from kgod/ai-review-template
feat: add resume agent MVP
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
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
|
||||
|
||||
|
||||
PHONE_PATTERN = re.compile(r"^1[3-9]\d{9}$")
|
||||
|
||||
|
||||
class Stage(StrEnum):
|
||||
PRIVACY_CONSENT = "PRIVACY_CONSENT"
|
||||
PHONE_SELECTION = "PHONE_SELECTION"
|
||||
MANUAL_PHONE_INPUT = "MANUAL_PHONE_INPUT"
|
||||
NAME_CAPTURE = "NAME_CAPTURE"
|
||||
JOB_TYPE_SELECT = "JOB_TYPE_SELECT"
|
||||
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"
|
||||
|
||||
|
||||
class JobType(StrEnum):
|
||||
CAMPUS = "campus"
|
||||
SOCIAL = "social"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
missing_fields: 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 BusinessResume(BaseModel):
|
||||
id: str
|
||||
session_id: str
|
||||
revision: int
|
||||
content: dict[str, Any]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user