1008 lines
35 KiB
Python
1008 lines
35 KiB
Python
from typing import List, Dict, Optional, Any, Union, Literal, TypeVar, Generic, Annotated
|
|
from pydantic import BaseModel, Field, EmailStr, HttpUrl, model_validator, field_validator # type: ignore
|
|
from pydantic.types import constr, conint # type: ignore
|
|
from datetime import datetime, date, UTC
|
|
from enum import Enum
|
|
import uuid
|
|
from auth_utils import (
|
|
AuthenticationManager,
|
|
validate_password_strength,
|
|
sanitize_login_input,
|
|
SecurityConfig
|
|
)
|
|
|
|
# Generic type variable
|
|
T = TypeVar('T')
|
|
|
|
# ============================
|
|
# Enums
|
|
# ============================
|
|
|
|
class UserType(str, Enum):
|
|
CANDIDATE = "candidate"
|
|
EMPLOYER = "employer"
|
|
GUEST = "guest"
|
|
|
|
class UserGender(str, Enum):
|
|
FEMALE = "female"
|
|
MALE = "male"
|
|
|
|
class UserStatus(str, Enum):
|
|
ACTIVE = "active"
|
|
INACTIVE = "inactive"
|
|
PENDING = "pending"
|
|
BANNED = "banned"
|
|
|
|
class SkillLevel(str, Enum):
|
|
BEGINNER = "beginner"
|
|
INTERMEDIATE = "intermediate"
|
|
ADVANCED = "advanced"
|
|
EXPERT = "expert"
|
|
|
|
class EmploymentType(str, Enum):
|
|
FULL_TIME = "full-time"
|
|
PART_TIME = "part-time"
|
|
CONTRACT = "contract"
|
|
INTERNSHIP = "internship"
|
|
FREELANCE = "freelance"
|
|
|
|
class InterviewType(str, Enum):
|
|
PHONE = "phone"
|
|
VIDEO = "video"
|
|
ONSITE = "onsite"
|
|
TECHNICAL = "technical"
|
|
BEHAVIORAL = "behavioral"
|
|
|
|
class ApplicationStatus(str, Enum):
|
|
APPLIED = "applied"
|
|
REVIEWING = "reviewing"
|
|
INTERVIEW = "interview"
|
|
OFFER = "offer"
|
|
REJECTED = "rejected"
|
|
ACCEPTED = "accepted"
|
|
WITHDRAWN = "withdrawn"
|
|
|
|
class InterviewRecommendation(str, Enum):
|
|
STRONG_HIRE = "strong_hire"
|
|
HIRE = "hire"
|
|
NO_HIRE = "no_hire"
|
|
STRONG_NO_HIRE = "strong_no_hire"
|
|
|
|
class ChatSenderType(str, Enum):
|
|
USER = "user"
|
|
ASSISTANT = "assistant"
|
|
SYSTEM = "system"
|
|
|
|
class ChatMessageType(str, Enum):
|
|
ERROR = "error"
|
|
GENERATING = "generating"
|
|
INFO = "info"
|
|
PREPARING = "preparing"
|
|
PROCESSING = "processing"
|
|
RESPONSE = "response"
|
|
SEARCHING = "searching"
|
|
RAG_RESULT = "rag_result"
|
|
SYSTEM = "system"
|
|
THINKING = "thinking"
|
|
TOOLING = "tooling"
|
|
USER = "user"
|
|
|
|
class ChatStatusType(str, Enum):
|
|
INITIALIZING = "initializing"
|
|
STREAMING = "streaming"
|
|
DONE = "done"
|
|
ERROR = "error"
|
|
|
|
class ChatContextType(str, Enum):
|
|
JOB_SEARCH = "job_search"
|
|
CANDIDATE_CHAT = "candidate_chat"
|
|
INTERVIEW_PREP = "interview_prep"
|
|
RESUME_REVIEW = "resume_review"
|
|
GENERAL = "general"
|
|
GENERATE_PERSONA = "generate_persona"
|
|
GENERATE_PROFILE = "generate_profile"
|
|
RAG_SEARCH = "rag_search"
|
|
|
|
class AIModelType(str, Enum):
|
|
QWEN2_5 = "qwen2.5"
|
|
FLUX_SCHNELL = "flux-schnell"
|
|
|
|
class MFAMethod(str, Enum):
|
|
APP = "app"
|
|
SMS = "sms"
|
|
EMAIL = "email"
|
|
|
|
class VectorStoreType(str, Enum):
|
|
CHROMA = "chroma",
|
|
# FAISS = "faiss",
|
|
# PINECONE = "pinecone"
|
|
# QDRANT = "qdrant"
|
|
# FAISS = "faiss"
|
|
# MILVUS = "milvus"
|
|
# WEAVIATE = "weaviate"
|
|
|
|
class DataSourceType(str, Enum):
|
|
DOCUMENT = "document"
|
|
WEBSITE = "website"
|
|
API = "api"
|
|
DATABASE = "database"
|
|
INTERNAL = "internal"
|
|
|
|
class ProcessingStepType(str, Enum):
|
|
EXTRACT = "extract"
|
|
TRANSFORM = "transform"
|
|
CHUNK = "chunk"
|
|
EMBED = "embed"
|
|
FILTER = "filter"
|
|
SUMMARIZE = "summarize"
|
|
|
|
class SearchType(str, Enum):
|
|
SIMILARITY = "similarity"
|
|
MMR = "mmr"
|
|
HYBRID = "hybrid"
|
|
KEYWORD = "keyword"
|
|
|
|
class ActivityType(str, Enum):
|
|
LOGIN = "login"
|
|
SEARCH = "search"
|
|
VIEW_JOB = "view_job"
|
|
APPLY_JOB = "apply_job"
|
|
MESSAGE = "message"
|
|
UPDATE_PROFILE = "update_profile"
|
|
CHAT = "chat"
|
|
|
|
class ThemePreference(str, Enum):
|
|
LIGHT = "light"
|
|
DARK = "dark"
|
|
SYSTEM = "system"
|
|
|
|
class NotificationType(str, Enum):
|
|
EMAIL = "email"
|
|
PUSH = "push"
|
|
IN_APP = "in_app"
|
|
|
|
class FontSize(str, Enum):
|
|
SMALL = "small"
|
|
MEDIUM = "medium"
|
|
LARGE = "large"
|
|
|
|
class SalaryPeriod(str, Enum):
|
|
HOUR = "hour"
|
|
DAY = "day"
|
|
MONTH = "month"
|
|
YEAR = "year"
|
|
|
|
class LanguageProficiency(str, Enum):
|
|
BASIC = "basic"
|
|
CONVERSATIONAL = "conversational"
|
|
FLUENT = "fluent"
|
|
NATIVE = "native"
|
|
|
|
class SocialPlatform(str, Enum):
|
|
LINKEDIN = "linkedin"
|
|
TWITTER = "twitter"
|
|
GITHUB = "github"
|
|
DRIBBBLE = "dribbble"
|
|
BEHANCE = "behance"
|
|
WEBSITE = "website"
|
|
OTHER = "other"
|
|
|
|
class ColorBlindMode(str, Enum):
|
|
PROTANOPIA = "protanopia"
|
|
DEUTERANOPIA = "deuteranopia"
|
|
TRITANOPIA = "tritanopia"
|
|
NONE = "none"
|
|
|
|
class SortOrder(str, Enum):
|
|
ASC = "asc"
|
|
DESC = "desc"
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
login: str # Can be email or username
|
|
password: str
|
|
|
|
@field_validator('login')
|
|
def sanitize_login(cls, v):
|
|
return sanitize_login_input(v)
|
|
|
|
@field_validator('password')
|
|
def validate_password_not_empty(cls, v):
|
|
if not v or not v.strip():
|
|
raise ValueError('Password cannot be empty')
|
|
return v
|
|
|
|
# ============================
|
|
# MFA Models
|
|
# ============================
|
|
|
|
class EmailVerificationRequest(BaseModel):
|
|
token: str
|
|
|
|
class MFARequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
device_id: str = Field(..., alias="deviceId")
|
|
device_name: str = Field(..., alias="deviceName")
|
|
model_config = {
|
|
"populate_by_name": True, # Allow both field names and aliases
|
|
}
|
|
|
|
class MFAVerifyRequest(BaseModel):
|
|
email: EmailStr
|
|
code: str
|
|
device_id: str = Field(..., alias="deviceId")
|
|
remember_device: bool = Field(False, alias="rememberDevice")
|
|
model_config = {
|
|
"populate_by_name": True, # Allow both field names and aliases
|
|
}
|
|
|
|
class MFAData(BaseModel):
|
|
message: str
|
|
device_id: str = Field(..., alias="deviceId")
|
|
device_name: str = Field(..., alias="deviceName")
|
|
code_sent: str = Field(..., alias="codeSent")
|
|
email: str
|
|
model_config = {
|
|
"populate_by_name": True, # Allow both field names and aliases
|
|
}
|
|
|
|
class MFARequestResponse(BaseModel):
|
|
mfa_required: bool = Field(..., alias="mfaRequired")
|
|
mfa_data: Optional[MFAData] = Field(None, alias="mfaData")
|
|
model_config = {
|
|
"populate_by_name": True, # Allow both field names and aliases
|
|
}
|
|
|
|
class ResendVerificationRequest(BaseModel):
|
|
email: EmailStr
|
|
|
|
# ============================
|
|
# Supporting Models
|
|
# ============================
|
|
|
|
class Tunables(BaseModel):
|
|
enable_rag: bool = Field(True, alias="enableRAG")
|
|
enable_tools: bool = Field(True, alias="enableTools")
|
|
enable_context: bool = Field(True, alias="enableContext")
|
|
|
|
class CandidateQuestion(BaseModel):
|
|
question: str
|
|
tunables: Optional[Tunables] = None
|
|
|
|
class Location(BaseModel):
|
|
city: str
|
|
state: Optional[str] = None
|
|
country: str
|
|
postal_code: Optional[str] = Field(None, alias="postalCode")
|
|
latitude: Optional[float] = None
|
|
longitude: Optional[float] = None
|
|
remote: Optional[bool] = None
|
|
hybrid_options: Optional[List[str]] = Field(None, alias="hybridOptions")
|
|
address: Optional[str] = None
|
|
|
|
class Skill(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
name: str
|
|
category: str
|
|
level: SkillLevel
|
|
years_of_experience: Optional[int] = Field(None, alias="yearsOfExperience")
|
|
|
|
class WorkExperience(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
company_name: str = Field(..., alias="companyName")
|
|
position: str
|
|
start_date: datetime = Field(..., alias="startDate")
|
|
end_date: Optional[datetime] = Field(None, alias="endDate")
|
|
is_current: bool = Field(..., alias="isCurrent")
|
|
description: str
|
|
skills: List[str]
|
|
location: Location
|
|
achievements: Optional[List[str]] = None
|
|
|
|
class Education(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
institution: str
|
|
degree: str
|
|
field_of_study: str = Field(..., alias="fieldOfStudy")
|
|
start_date: datetime = Field(..., alias="startDate")
|
|
end_date: Optional[datetime] = Field(None, alias="endDate")
|
|
is_current: bool = Field(..., alias="isCurrent")
|
|
gpa: Optional[float] = None
|
|
achievements: Optional[List[str]] = None
|
|
location: Optional[Location] = None
|
|
|
|
class Language(BaseModel):
|
|
language: str
|
|
proficiency: LanguageProficiency
|
|
|
|
class Certification(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
name: str
|
|
issuing_organization: str = Field(..., alias="issuingOrganization")
|
|
issue_date: datetime = Field(..., alias="issueDate")
|
|
expiration_date: Optional[datetime] = Field(None, alias="expirationDate")
|
|
credential_id: Optional[str] = Field(None, alias="credentialId")
|
|
credential_url: Optional[HttpUrl] = Field(None, alias="credentialUrl")
|
|
|
|
class SocialLink(BaseModel):
|
|
platform: SocialPlatform
|
|
url: HttpUrl
|
|
|
|
class DesiredSalary(BaseModel):
|
|
amount: float
|
|
currency: str
|
|
period: SalaryPeriod
|
|
|
|
class SalaryRange(BaseModel):
|
|
min: float
|
|
max: float
|
|
currency: str
|
|
period: SalaryPeriod
|
|
is_visible: bool = Field(..., alias="isVisible")
|
|
|
|
class PointOfContact(BaseModel):
|
|
name: str
|
|
position: str
|
|
email: EmailStr
|
|
phone: Optional[str] = None
|
|
|
|
class RefreshToken(BaseModel):
|
|
token: str
|
|
expires_at: datetime = Field(..., alias="expiresAt")
|
|
device: str
|
|
ip_address: str = Field(..., alias="ipAddress")
|
|
is_revoked: bool = Field(..., alias="isRevoked")
|
|
revoked_reason: Optional[str] = Field(None, alias="revokedReason")
|
|
|
|
class Attachment(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
file_name: str = Field(..., alias="fileName")
|
|
file_type: str = Field(..., alias="fileType")
|
|
file_size: int = Field(..., alias="fileSize")
|
|
file_url: str = Field(..., alias="fileUrl")
|
|
uploaded_at: datetime = Field(..., alias="uploadedAt")
|
|
is_processed: bool = Field(..., alias="isProcessed")
|
|
processing_result: Optional[Any] = Field(None, alias="processingResult")
|
|
thumbnail_url: Optional[str] = Field(None, alias="thumbnailUrl")
|
|
|
|
class MessageReaction(BaseModel):
|
|
user_id: str = Field(..., alias="userId")
|
|
reaction: str
|
|
timestamp: datetime
|
|
|
|
class EditHistory(BaseModel):
|
|
content: str
|
|
edited_at: datetime = Field(..., alias="editedAt")
|
|
edited_by: str = Field(..., alias="editedBy")
|
|
|
|
class CustomQuestion(BaseModel):
|
|
question: str
|
|
answer: str
|
|
|
|
class CandidateContact(BaseModel):
|
|
email: EmailStr
|
|
phone: Optional[str] = None
|
|
|
|
class ApplicationDecision(BaseModel):
|
|
status: Literal["accepted", "rejected"]
|
|
reason: Optional[str] = None
|
|
date: datetime
|
|
by: str
|
|
|
|
class SkillAssessment(BaseModel):
|
|
skill_name: str = Field(..., alias="skillName")
|
|
score: Annotated[float, Field(ge=0, le=10)]
|
|
comments: Optional[str] = None
|
|
|
|
class NotificationPreference(BaseModel):
|
|
type: NotificationType
|
|
events: List[str]
|
|
is_enabled: bool = Field(..., alias="isEnabled")
|
|
|
|
class AccessibilitySettings(BaseModel):
|
|
font_size: FontSize = Field(..., alias="fontSize")
|
|
high_contrast: bool = Field(..., alias="highContrast")
|
|
reduce_motion: bool = Field(..., alias="reduceMotion")
|
|
screen_reader: bool = Field(..., alias="screenReader")
|
|
color_blind_mode: Optional[ColorBlindMode] = Field(None, alias="colorBlindMode")
|
|
|
|
class ProcessingStep(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
type: ProcessingStepType
|
|
parameters: Dict[str, Any]
|
|
order: int
|
|
depends_on: Optional[List[str]] = Field(None, alias="dependsOn")
|
|
|
|
class RetrievalParameters(BaseModel):
|
|
search_type: SearchType = Field(..., alias="searchType")
|
|
top_k: int = Field(..., alias="topK")
|
|
similarity_threshold: Optional[float] = Field(None, alias="similarityThreshold")
|
|
reranker_model: Optional[str] = Field(None, alias="rerankerModel")
|
|
use_keyword_boost: bool = Field(..., alias="useKeywordBoost")
|
|
filter_options: Optional[Dict[str, Any]] = Field(None, alias="filterOptions")
|
|
context_window: int = Field(..., alias="contextWindow")
|
|
|
|
class ErrorDetail(BaseModel):
|
|
code: str
|
|
message: str
|
|
details: Optional[Any] = None
|
|
|
|
# ============================
|
|
# Main Models
|
|
# ============================
|
|
|
|
# Base user model without user_type field
|
|
class BaseUser(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
email: EmailStr
|
|
first_name: str = Field(..., alias="firstName")
|
|
last_name: str = Field(..., alias="lastName")
|
|
full_name: str = Field(..., alias="fullName")
|
|
phone: Optional[str] = None
|
|
location: Optional[Location] = None
|
|
created_at: datetime = Field(..., alias="createdAt")
|
|
updated_at: datetime = Field(..., alias="updatedAt")
|
|
last_login: Optional[datetime] = Field(None, alias="lastLogin")
|
|
profile_image: Optional[str] = Field(None, alias="profileImage")
|
|
status: UserStatus
|
|
is_admin: bool = Field(default=False, alias="isAdmin")
|
|
|
|
model_config = {
|
|
"populate_by_name": True, # Allow both field names and aliases
|
|
"use_enum_values": True # Use enum values instead of names
|
|
}
|
|
|
|
# Generic base user with user_type for API responses
|
|
class BaseUserWithType(BaseUser):
|
|
user_type: UserType = Field(..., alias="userType")
|
|
|
|
class RagEntry(BaseModel):
|
|
name: str
|
|
description: str = ""
|
|
enabled: bool = True
|
|
|
|
class RagContentMetadata(BaseModel):
|
|
source_file: str = Field(..., alias="sourceFile")
|
|
line_begin: int = Field(..., alias="lineBegin")
|
|
line_end: int = Field(..., alias="lineEnd")
|
|
lines: int
|
|
chunk_begin: Optional[int] = Field(None, alias="chunkBegin")
|
|
chunk_end: Optional[int] = Field(None, alias="chunkEnd")
|
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
model_config = {
|
|
"populate_by_name": True, # Allow both field names and aliases
|
|
}
|
|
|
|
class RagContentResponse(BaseModel):
|
|
id: str
|
|
content: str
|
|
metadata: RagContentMetadata
|
|
|
|
class DocumentType(str, Enum):
|
|
PDF = "pdf"
|
|
DOCX = "docx"
|
|
TXT = "txt"
|
|
MARKDOWN = "markdown"
|
|
IMAGE = "image"
|
|
|
|
class Document(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
owner_id: str = Field(..., alias="ownerId")
|
|
filename: str
|
|
originalName: str
|
|
type: DocumentType
|
|
size: int
|
|
upload_date: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="uploadDate")
|
|
include_in_RAG: bool = Field(default=True, alias="includeInRAG")
|
|
rag_chunks: Optional[int] = Field(default=0, alias="ragChunks")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class DocumentContentResponse(BaseModel):
|
|
document_id: str = Field(..., alias="documentId")
|
|
filename: str
|
|
type: DocumentType
|
|
content: str
|
|
size: int
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class DocumentListResponse(BaseModel):
|
|
documents: List[Document]
|
|
total: int
|
|
|
|
class DocumentUpdateRequest(BaseModel):
|
|
filename: Optional[str] = None
|
|
include_in_RAG: Optional[bool] = Field(None, alias="includeInRAG")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class Candidate(BaseUser):
|
|
user_type: Literal[UserType.CANDIDATE] = Field(UserType.CANDIDATE, alias="userType")
|
|
username: str
|
|
description: Optional[str] = None
|
|
resume: Optional[str] = None
|
|
skills: Optional[List[Skill]] = None
|
|
experience: Optional[List[WorkExperience]] = None
|
|
questions: Optional[List[CandidateQuestion]] = None
|
|
education: Optional[List[Education]] = None
|
|
preferred_job_types: Optional[List[EmploymentType]] = Field(None, alias="preferredJobTypes")
|
|
desired_salary: Optional[DesiredSalary] = Field(None, alias="desiredSalary")
|
|
availability_date: Optional[datetime] = Field(None, alias="availabilityDate")
|
|
summary: Optional[str] = None
|
|
languages: Optional[List[Language]] = None
|
|
certifications: Optional[List[Certification]] = None
|
|
job_applications: Optional[List["JobApplication"]] = Field(None, alias="jobApplications")
|
|
rags: List[RagEntry] = Field(default_factory=list)
|
|
rag_content_size : int = 0
|
|
|
|
class CandidateAI(Candidate):
|
|
user_type: Literal[UserType.CANDIDATE] = Field(UserType.CANDIDATE, alias="userType")
|
|
is_AI: bool = Field(True, alias="isAI")
|
|
age: Optional[int] = None
|
|
gender: Optional[UserGender] = None
|
|
ethnicity: Optional[str] = None
|
|
|
|
class Employer(BaseUser):
|
|
user_type: Literal[UserType.EMPLOYER] = Field(UserType.EMPLOYER, alias="userType")
|
|
company_name: str = Field(..., alias="companyName")
|
|
industry: str
|
|
description: Optional[str] = None
|
|
company_size: str = Field(..., alias="companySize")
|
|
company_description: str = Field(..., alias="companyDescription")
|
|
website_url: Optional[HttpUrl] = Field(None, alias="websiteUrl")
|
|
jobs: Optional[List["Job"]] = None
|
|
company_logo: Optional[str] = Field(None, alias="companyLogo")
|
|
social_links: Optional[List[SocialLink]] = Field(None, alias="socialLinks")
|
|
poc: Optional[PointOfContact] = None
|
|
|
|
class Guest(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
session_id: str = Field(..., alias="sessionId")
|
|
created_at: datetime = Field(..., alias="createdAt")
|
|
last_activity: datetime = Field(..., alias="lastActivity")
|
|
converted_to_user_id: Optional[str] = Field(None, alias="convertedToUserId")
|
|
ip_address: Optional[str] = Field(None, alias="ipAddress")
|
|
user_agent: Optional[str] = Field(None, alias="userAgent")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class Authentication(BaseModel):
|
|
user_id: str = Field(..., alias="userId")
|
|
password_hash: str = Field(..., alias="passwordHash")
|
|
salt: str
|
|
refresh_tokens: List[RefreshToken] = Field(..., alias="refreshTokens")
|
|
reset_password_token: Optional[str] = Field(None, alias="resetPasswordToken")
|
|
reset_password_expiry: Optional[datetime] = Field(None, alias="resetPasswordExpiry")
|
|
last_password_change: datetime = Field(..., alias="lastPasswordChange")
|
|
mfa_enabled: bool = Field(..., alias="mfaEnabled")
|
|
mfa_method: Optional[MFAMethod] = Field(None, alias="mfaMethod")
|
|
mfa_secret: Optional[str] = Field(None, alias="mfaSecret")
|
|
login_attempts: int = Field(..., alias="loginAttempts")
|
|
locked_until: Optional[datetime] = Field(None, alias="lockedUntil")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class AuthResponse(BaseModel):
|
|
access_token: str = Field(..., alias="accessToken")
|
|
refresh_token: str = Field(..., alias="refreshToken")
|
|
user: Candidate | Employer
|
|
expires_at: int = Field(..., alias="expiresAt")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class Job(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
title: str
|
|
description: str
|
|
responsibilities: List[str]
|
|
requirements: List[str]
|
|
preferred_skills: Optional[List[str]] = Field(None, alias="preferredSkills")
|
|
employer_id: str = Field(..., alias="employerId")
|
|
location: Location
|
|
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
|
|
employment_type: EmploymentType = Field(..., alias="employmentType")
|
|
date_posted: datetime = Field(..., alias="datePosted")
|
|
application_deadline: Optional[datetime] = Field(None, alias="applicationDeadline")
|
|
is_active: bool = Field(..., alias="isActive")
|
|
applicants: Optional[List["JobApplication"]] = None
|
|
department: Optional[str] = None
|
|
reports_to: Optional[str] = Field(None, alias="reportsTo")
|
|
benefits: Optional[List[str]] = None
|
|
visa_sponsorship: Optional[bool] = Field(None, alias="visaSponsorship")
|
|
featured_until: Optional[datetime] = Field(None, alias="featuredUntil")
|
|
views: int = 0
|
|
application_count: int = Field(0, alias="applicationCount")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class InterviewFeedback(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
interview_id: str = Field(..., alias="interviewId")
|
|
reviewer_id: str = Field(..., alias="reviewerId")
|
|
technical_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias="technicalScore")
|
|
cultural_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias="culturalScore")
|
|
overall_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias="overallScore")
|
|
strengths: List[str]
|
|
weaknesses: List[str]
|
|
recommendation: InterviewRecommendation
|
|
comments: str
|
|
created_at: datetime = Field(..., alias="createdAt")
|
|
updated_at: datetime = Field(..., alias="updatedAt")
|
|
is_visible: bool = Field(..., alias="isVisible")
|
|
skill_assessments: Optional[List[SkillAssessment]] = Field(None, alias="skillAssessments")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class InterviewSchedule(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
application_id: str = Field(..., alias="applicationId")
|
|
scheduled_date: datetime = Field(..., alias="scheduledDate")
|
|
end_date: datetime = Field(..., alias="endDate")
|
|
interview_type: InterviewType = Field(..., alias="interviewType")
|
|
interviewers: List[str]
|
|
location: Optional[Union[str, Location]] = None
|
|
notes: Optional[str] = None
|
|
feedback: Optional[InterviewFeedback] = None
|
|
status: Literal["scheduled", "completed", "cancelled", "rescheduled"]
|
|
meeting_link: Optional[HttpUrl] = Field(None, alias="meetingLink")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class JobApplication(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
job_id: str = Field(..., alias="jobId")
|
|
candidate_id: str = Field(..., alias="candidateId")
|
|
status: ApplicationStatus
|
|
applied_date: datetime = Field(..., alias="appliedDate")
|
|
updated_date: datetime = Field(..., alias="updatedDate")
|
|
resume_version: str = Field(..., alias="resumeVersion")
|
|
cover_letter: Optional[str] = Field(None, alias="coverLetter")
|
|
notes: Optional[str] = None
|
|
interview_schedules: Optional[List[InterviewSchedule]] = Field(None, alias="interviewSchedules")
|
|
custom_questions: Optional[List[CustomQuestion]] = Field(None, alias="customQuestions")
|
|
candidate_contact: Optional[CandidateContact] = Field(None, alias="candidateContact")
|
|
decision: Optional[ApplicationDecision] = None
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class ChromaDBGetResponse(BaseModel):
|
|
# Chroma fields
|
|
ids: List[str] = []
|
|
embeddings: List[List[float]] = []
|
|
documents: List[str] = []
|
|
metadatas: List[Dict[str, Any]] = []
|
|
distances: List[float] = []
|
|
# Additional fields
|
|
name: str = ""
|
|
size: int = 0
|
|
dimensions: int = 2 | 3
|
|
query: str = ""
|
|
query_embedding: Optional[List[float]] = Field(default=None, alias="queryEmbedding")
|
|
umap_embedding_2d: Optional[List[float]] = Field(default=None, alias="umapEmbedding2D")
|
|
umap_embedding_3d: Optional[List[float]] = Field(default=None, alias="umapEmbedding3D")
|
|
|
|
class ChatContext(BaseModel):
|
|
type: ChatContextType
|
|
related_entity_id: Optional[str] = Field(None, alias="relatedEntityId")
|
|
related_entity_type: Optional[Literal["job", "candidate", "employer"]] = Field(None, alias="relatedEntityType")
|
|
additional_context: Optional[Dict[str, Any]] = Field(None, alias="additionalContext")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class ChatOptions(BaseModel):
|
|
seed: Optional[int] = 8911
|
|
num_ctx: Optional[int] = Field(default=None, alias="numCtx") # Number of context tokens
|
|
temperature: Optional[float] = Field(default=0.7) # Higher temperature to encourage tool usage
|
|
|
|
class LLMMessage(BaseModel):
|
|
role: str = Field(default="")
|
|
content: str = Field(default="")
|
|
tool_calls: Optional[List[Dict]] = Field(default={}, exclude=True)
|
|
|
|
|
|
class ChatMessageBase(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
session_id: str = Field(..., alias="sessionId")
|
|
sender_id: Optional[str] = Field(None, alias="senderId")
|
|
status: ChatStatusType
|
|
type: ChatMessageType
|
|
sender: ChatSenderType
|
|
timestamp: datetime
|
|
tunables: Optional[Tunables] = None
|
|
content: str = ""
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class ChatMessageRagSearch(ChatMessageBase):
|
|
status: ChatStatusType = ChatStatusType.DONE
|
|
type: ChatMessageType = ChatMessageType.RAG_RESULT
|
|
sender: ChatSenderType = ChatSenderType.USER
|
|
dimensions: int = 2 | 3
|
|
|
|
class ChatMessageMetaData(BaseModel):
|
|
model: AIModelType = AIModelType.QWEN2_5
|
|
temperature: float = 0.7
|
|
max_tokens: int = Field(default=8092, alias="maxTokens")
|
|
top_p: float = Field(default=1, alias="topP")
|
|
frequency_penalty: Optional[float] = Field(None, alias="frequencyPenalty")
|
|
presence_penalty: Optional[float] = Field(None, alias="presencePenalty")
|
|
stop_sequences: Optional[List[str]] = Field(None, alias="stopSequences")
|
|
rag_results: List[ChromaDBGetResponse] = Field(default_factory=list, alias="ragResults")
|
|
llm_history: List[LLMMessage] = Field(default_factory=list, alias="llmHistory")
|
|
eval_count: int = 0
|
|
eval_duration: int = 0
|
|
prompt_eval_count: int = 0
|
|
prompt_eval_duration: int = 0
|
|
options: Optional[ChatOptions] = None
|
|
tools: Optional[Dict[str, Any]] = None
|
|
timers: Optional[Dict[str, float]] = None
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class ChatMessageUser(ChatMessageBase):
|
|
status: ChatStatusType = ChatStatusType.DONE
|
|
type: ChatMessageType = ChatMessageType.USER
|
|
sender: ChatSenderType = ChatSenderType.USER
|
|
|
|
class ChatMessage(ChatMessageBase):
|
|
#attachments: Optional[List[Attachment]] = None
|
|
#reactions: Optional[List[MessageReaction]] = None
|
|
#is_edited: bool = Field(False, alias="isEdited")
|
|
#edit_history: Optional[List[EditHistory]] = Field(None, alias="editHistory")
|
|
metadata: ChatMessageMetaData = Field(default_factory=ChatMessageMetaData)
|
|
|
|
class ChatSession(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
user_id: Optional[str] = Field(None, alias="userId")
|
|
guest_id: Optional[str] = Field(None, alias="guestId")
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
|
|
last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="lastActivity")
|
|
title: Optional[str] = None
|
|
context: ChatContext
|
|
# messages: Optional[List[ChatMessage]] = None
|
|
is_archived: bool = Field(False, alias="isArchived")
|
|
system_prompt: Optional[str] = Field(None, alias="systemPrompt")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
@model_validator(mode="after")
|
|
def check_user_or_guest(self) -> "ChatSession":
|
|
if not self.user_id and not self.guest_id:
|
|
raise ValueError("Either user_id or guest_id must be provided")
|
|
return self
|
|
|
|
class DataSourceConfiguration(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
rag_config_id: str = Field(..., alias="ragConfigId")
|
|
name: str
|
|
source_type: DataSourceType = Field(..., alias="sourceType")
|
|
connection_details: Dict[str, Any] = Field(..., alias="connectionDetails")
|
|
processing_pipeline: List[ProcessingStep] = Field(..., alias="processingPipeline")
|
|
refresh_schedule: Optional[str] = Field(None, alias="refreshSchedule")
|
|
last_refreshed: Optional[datetime] = Field(None, alias="lastRefreshed")
|
|
status: Literal["active", "pending", "error", "processing"]
|
|
error_details: Optional[str] = Field(None, alias="errorDetails")
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class RAGConfiguration(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
user_id: str = Field(..., alias="userId")
|
|
name: str
|
|
description: Optional[str] = None
|
|
data_source_configurations: List[DataSourceConfiguration] = Field(..., alias="dataSourceConfigurations")
|
|
embedding_model: str = Field(..., alias="embeddingModel")
|
|
vector_store_type: VectorStoreType = Field(..., alias="vectorStoreType")
|
|
retrieval_parameters: RetrievalParameters = Field(..., alias="retrievalParameters")
|
|
created_at: datetime = Field(..., alias="createdAt")
|
|
updated_at: datetime = Field(..., alias="updatedAt")
|
|
version: int
|
|
is_active: bool = Field(..., alias="isActive")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class UserActivity(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
user_id: Optional[str] = Field(None, alias="userId")
|
|
guest_id: Optional[str] = Field(None, alias="guestId")
|
|
activity_type: ActivityType = Field(..., alias="activityType")
|
|
timestamp: datetime
|
|
metadata: Dict[str, Any]
|
|
ip_address: Optional[str] = Field(None, alias="ipAddress")
|
|
user_agent: Optional[str] = Field(None, alias="userAgent")
|
|
session_id: Optional[str] = Field(None, alias="sessionId")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
@model_validator(mode="after")
|
|
def check_user_or_guest(self) -> "ChatSession":
|
|
if not self.user_id and not self.guest_id:
|
|
raise ValueError("Either user_id or guest_id must be provided")
|
|
return self
|
|
|
|
class Analytics(BaseModel):
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
entity_type: Literal["job", "candidate", "chat", "system", "employer"] = Field(..., alias="entityType")
|
|
entity_id: str = Field(..., alias="entityId")
|
|
metric_type: str = Field(..., alias="metricType")
|
|
value: float
|
|
timestamp: datetime
|
|
dimensions: Optional[Dict[str, Any]] = None
|
|
segment: Optional[str] = None
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class UserPreference(BaseModel):
|
|
user_id: str = Field(..., alias="userId")
|
|
theme: ThemePreference
|
|
notifications: List[NotificationPreference]
|
|
accessibility: AccessibilitySettings
|
|
dashboard_layout: Optional[Dict[str, Any]] = Field(None, alias="dashboardLayout")
|
|
language: str
|
|
timezone: str
|
|
email_frequency: Literal["immediate", "daily", "weekly", "never"] = Field(..., alias="emailFrequency")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
# ============================
|
|
# API Request/Response Models
|
|
# ============================
|
|
class CreateCandidateRequest(BaseModel):
|
|
email: EmailStr
|
|
username: str
|
|
password: str
|
|
first_name: str = Field(..., alias="firstName")
|
|
last_name: str = Field(..., alias="lastName")
|
|
# Add other required candidate fields as needed
|
|
phone: Optional[str] = None
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
@field_validator('username')
|
|
def validate_username(cls, v):
|
|
if not v or len(v.strip()) < 3:
|
|
raise ValueError('Username must be at least 3 characters long')
|
|
return v.strip().lower()
|
|
|
|
@field_validator('password')
|
|
def validate_password_strength(cls, v):
|
|
is_valid, issues = validate_password_strength(v)
|
|
if not is_valid:
|
|
raise ValueError('; '.join(issues))
|
|
return v
|
|
|
|
# Create Employer Endpoint (similar pattern)
|
|
class CreateEmployerRequest(BaseModel):
|
|
email: EmailStr
|
|
username: str
|
|
password: str
|
|
company_name: str = Field(..., alias="companyName")
|
|
industry: str
|
|
company_size: str = Field(..., alias="companySize")
|
|
company_description: str = Field(..., alias="companyDescription")
|
|
# Add other required employer fields
|
|
website_url: Optional[str] = Field(None, alias="websiteUrl")
|
|
phone: Optional[str] = None
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
@field_validator('username')
|
|
def validate_username(cls, v):
|
|
if not v or len(v.strip()) < 3:
|
|
raise ValueError('Username must be at least 3 characters long')
|
|
return v.strip().lower()
|
|
|
|
@field_validator('password')
|
|
def validate_password_strength(cls, v):
|
|
is_valid, issues = validate_password_strength(v)
|
|
if not is_valid:
|
|
raise ValueError('; '.join(issues))
|
|
return v
|
|
|
|
class ChatQuery(BaseModel):
|
|
prompt: str
|
|
tunables: Optional[Tunables] = None
|
|
agent_options: Optional[Dict[str, Any]] = Field(None, alias="agentOptions")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class PaginatedRequest(BaseModel):
|
|
page: Annotated[int, Field(ge=1)] = 1
|
|
limit: Annotated[int, Field(ge=1, le=100)] = 20
|
|
sort_by: Optional[str] = Field(None, alias="sortBy")
|
|
sort_order: Optional[SortOrder] = Field(None, alias="sortOrder")
|
|
filters: Optional[Dict[str, Any]] = None
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class SearchQuery(BaseModel):
|
|
query: str
|
|
filters: Optional[Dict[str, Any]] = None
|
|
page: Annotated[int, Field(ge=1)] = 1
|
|
limit: Annotated[int, Field(ge=1, le=100)] = 20
|
|
sort_by: Optional[str] = Field(None, alias="sortBy")
|
|
sort_order: Optional[SortOrder] = Field(None, alias="sortOrder")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class PaginatedResponse(BaseModel):
|
|
data: List[Any] # Will be typed specifically when used
|
|
total: int
|
|
page: int
|
|
limit: int
|
|
total_pages: int = Field(..., alias="totalPages")
|
|
has_more: bool = Field(..., alias="hasMore")
|
|
model_config = {
|
|
"populate_by_name": True # Allow both field names and aliases
|
|
}
|
|
|
|
class ApiResponse(BaseModel):
|
|
success: bool
|
|
data: Optional[Any] = None # Will be typed specifically when used
|
|
error: Optional[ErrorDetail] = None
|
|
meta: Optional[Dict[str, Any]] = None
|
|
|
|
# Specific typed response models for common use cases
|
|
class CandidateResponse(BaseModel):
|
|
success: bool
|
|
data: Optional[Candidate] = None
|
|
error: Optional[ErrorDetail] = None
|
|
meta: Optional[Dict[str, Any]] = None
|
|
|
|
class EmployerResponse(BaseModel):
|
|
success: bool
|
|
data: Optional[Employer] = None
|
|
error: Optional[ErrorDetail] = None
|
|
meta: Optional[Dict[str, Any]] = None
|
|
|
|
class JobResponse(BaseModel):
|
|
success: bool
|
|
data: Optional["Job"] = None
|
|
error: Optional[ErrorDetail] = None
|
|
meta: Optional[Dict[str, Any]] = None
|
|
|
|
class CandidateListResponse(BaseModel):
|
|
success: bool
|
|
data: Optional[List[Candidate]] = None
|
|
error: Optional[ErrorDetail] = None
|
|
meta: Optional[Dict[str, Any]] = None
|
|
|
|
class JobListResponse(BaseModel):
|
|
success: bool
|
|
data: Optional[List["Job"]] = None
|
|
error: Optional[ErrorDetail] = None
|
|
meta: Optional[Dict[str, Any]] = None
|
|
|
|
# Forward references resolution
|
|
Candidate.update_forward_refs()
|
|
Employer.update_forward_refs()
|
|
ChatSession.update_forward_refs()
|
|
JobApplication.update_forward_refs()
|
|
Job.update_forward_refs() |