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 ) import defines # 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" # Frontend can use this to set mock responses SYSTEM = "system" INFORMATION = "information" WARNING = "warning" ERROR = "error" class SkillStatus(str, Enum): PENDING = "pending" COMPLETE = "complete" WAITING = "waiting" ERROR = "error" class SkillStrength(str, Enum): STRONG = "strong" MODERATE = "moderate" WEAK = "weak" NONE = "none" class EvidenceDetail(BaseModel): source: str = Field(..., alias="source", description="The source of the evidence (e.g., resume section, position, project)") quote: str = Field(..., alias="quote", description="Exact text from the resume or other source showing evidence") context: str = Field(..., alias="context", description="Brief explanation of how this demonstrates the skill") 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 SkillAssessment(BaseModel): candidate_id: str = Field(..., alias='candidateId') skill: str = Field(..., alias="skill", description="The skill being assessed") skill_modified: Optional[str] = Field(default="", alias="skillModified", description="The skill rephrased by LLM during skill match") evidence_found: bool = Field(..., alias="evidenceFound", description="Whether evidence was found for the skill") evidence_strength: SkillStrength = Field(..., alias="evidenceStrength", description="Strength of evidence found for the skill") assessment: str = Field(..., alias="assessment", description="Short (one to two sentence) assessment of the candidate's proficiency with the skill") description: str = Field(..., alias="description", description="Short (two to three sentence) description of what the skill is, independent of whether the candidate has that skill or not") evidence_details: List[EvidenceDetail] = Field(default_factory=list, alias="evidenceDetails", description="List of evidence details supporting the skill assessment") created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias='createdAt') updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias='updatedAt') rag_results: List[ChromaDBGetResponse] = Field(default_factory=list, alias="ragResults") model_config = { "populate_by_name": True, # Allow both field names and aliases } class ApiMessageType(str, Enum): BINARY = "binary" TEXT = "text" JSON = "json" class ApiStatusType(str, Enum): STREAMING = "streaming" STATUS = "status" DONE = "done" ERROR = "error" class ChatContextType(str, Enum): JOB_SEARCH = "job_search" JOB_REQUIREMENTS = "job_requirements" CANDIDATE_CHAT = "candidate_chat" INTERVIEW_PREP = "interview_prep" RESUME_REVIEW = "resume_review" GENERAL = "general" GENERATE_PERSONA = "generate_persona" GENERATE_PROFILE = "generate_profile" GENERATE_RESUME = "generate_resume" GENERATE_IMAGE = "generate_image" RAG_SEARCH = "rag_search" SKILL_MATCH = "skill_match" 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") email: str = Field(..., alias="email") 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 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 # ============================ # Generic base user with user_type for API responses class BaseUserWithType(BaseModel): user_type: UserType = Field(..., alias="userType") id: str = Field(default_factory=lambda: str(uuid.uuid4())) last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="lastActivity") model_config = { "populate_by_name": True, # Allow both field names and aliases "use_enum_values": True # Use enum values instead of names } # Base user model without user_type field class BaseUser(BaseUserWithType): 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(default_factory=lambda: datetime.now(UTC), alias="createdAt") updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), 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 } 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 DocumentOptions(BaseModel): include_in_rag: bool = Field(default=True, alias="includeInRag") is_job_document: Optional[bool] = Field(default=False, alias="isJobDocument") overwrite: Optional[bool] = Field(default=False, alias="overwrite") model_config = { "populate_by_name": True # Allow both field names and aliases } 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") options: DocumentOptions = Field(default_factory=lambda: DocumentOptions(), alias="options") 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 model_config = { "populate_by_name": True # Allow both field names and aliases } class DocumentUpdateRequest(BaseModel): filename: Optional[str] = None options: Optional[DocumentOptions] = None model_config = { "populate_by_name": True # Allow both field names and aliases } class Candidate(BaseUser): user_type: UserType = 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: UserType = 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: UserType = 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(BaseUser): user_type: UserType = Field(UserType.GUEST, alias="userType") session_id: str = Field(..., alias="sessionId") username: str # Add username for consistency with other user types converted_to_user_id: Optional[str] = Field(None, alias="convertedToUserId") ip_address: Optional[str] = Field(None, alias="ipAddress") created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt") user_agent: Optional[str] = Field(None, alias="userAgent") rag_content_size: int = 0 model_config = { "populate_by_name": True, # Allow both field names and aliases "use_enum_values": True # Use enum values instead of names } 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: Union[Candidate, Employer, Guest] # Add Guest support expires_at: int = Field(..., alias="expiresAt") user_type: Optional[str] = Field(default=UserType.GUEST, alias="userType") # Explicit user type is_guest: Optional[bool] = Field(default=True, alias="isGuest") # Guest indicator model_config = { "populate_by_name": True } class GuestCleanupRequest(BaseModel): """Request to cleanup inactive guests""" inactive_hours: int = Field(24, alias="inactiveHours") model_config = { "populate_by_name": True } class Requirements(BaseModel): required: List[str] = Field(default_factory=list) preferred: List[str] = Field(default_factory=list) @model_validator(mode='before') def validate_requirements(cls, values): if not isinstance(values, dict): raise ValueError("Requirements must be a dictionary with 'required' and 'preferred' keys.") return values class JobRequirements(BaseModel): technical_skills: Requirements = Field(..., alias="technicalSkills") experience_requirements: Requirements = Field(..., alias="experienceRequirements") soft_skills: Optional[List[str]] = Field(default_factory=list, alias="softSkills") experience: Optional[List[str]] = [] education: Optional[List[str]] = [] certifications: Optional[List[str]] = [] preferred_attributes: Optional[List[str]] = Field(None, alias="preferredAttributes") model_config = { "populate_by_name": True # Allow both field names and aliases } class JobDetails(BaseModel): location: Location salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange") employment_type: EmploymentType = Field(..., alias="employmentType") date_posted: datetime = Field(default_factory=lambda: datetime.now(UTC), 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") class Job(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) owner_id: str = Field(..., alias="ownerId") owner_type: UserType = Field(..., alias="ownerType") owner: Optional[BaseUser] = None title: Optional[str] summary: Optional[str] company: Optional[str] description: str requirements: Optional[JobRequirements] created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt") updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt") details: Optional[JobDetails] = Field(None, alias="details") 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(default_factory=lambda: datetime.now(UTC), alias="createdAt") updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), 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 GuestSessionResponse(BaseModel): """Response for guest session creation""" access_token: str = Field(..., alias="accessToken") refresh_token: str = Field(..., alias="refreshToken") user: Guest expires_at: int = Field(..., alias="expiresAt") user_type: Literal["guest"] = Field("guest", alias="userType") is_guest: bool = Field(True, alias="isGuest") model_config = { "populate_by_name": True } 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({}, 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 model_config = { "populate_by_name": True # Allow both field names and aliases } # Add rate limiting configuration models class RateLimitConfig(BaseModel): """Rate limit configuration""" requests_per_minute: int = Field(..., alias="requestsPerMinute") requests_per_hour: int = Field(..., alias="requestsPerHour") requests_per_day: int = Field(..., alias="requestsPerDay") burst_limit: int = Field(..., alias="burstLimit") burst_window_seconds: int = Field(60, alias="burstWindowSeconds") model_config = { "populate_by_name": True } class RateLimitResult(BaseModel): """Result of rate limit check""" allowed: bool reason: Optional[str] = None retry_after_seconds: Optional[int] = Field(None, alias="retryAfterSeconds") remaining_requests: Dict[str, int] = Field(default_factory=dict, alias="remainingRequests") reset_times: Dict[str, datetime] = Field(default_factory=dict, alias="resetTimes") model_config = { "populate_by_name": True } class RateLimitStatus(BaseModel): """Rate limit status for a user""" user_id: str = Field(..., alias="userId") user_type: str = Field(..., alias="userType") is_admin: bool = Field(..., alias="isAdmin") current_usage: Dict[str, int] = Field(..., alias="currentUsage") limits: Dict[str, int] = Field(..., alias="limits") remaining: Dict[str, int] = Field(..., alias="remaining") reset_times: Dict[str, datetime] = Field(..., alias="resetTimes") config: RateLimitConfig model_config = { "populate_by_name": True } # Add guest conversion request models class GuestConversionRequest(BaseModel): """Request to convert guest to permanent user""" account_type: Literal["candidate", "employer"] = Field(..., alias="accountType") email: EmailStr username: str password: str first_name: str = Field(..., alias="firstName") last_name: str = Field(..., alias="lastName") phone: Optional[str] = None # Employer-specific fields (optional) company_name: Optional[str] = Field(None, alias="companyName") industry: Optional[str] = None company_size: Optional[str] = Field(None, alias="companySize") company_description: Optional[str] = Field(None, alias="companyDescription") website_url: Optional[HttpUrl] = Field(None, alias="websiteUrl") model_config = { "populate_by_name": True } @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): # Import here to avoid circular imports from auth_utils import validate_password_strength is_valid, issues = validate_password_strength(v) if not is_valid: raise ValueError('; '.join(issues)) return v # Add guest statistics response model class GuestStatistics(BaseModel): """Guest usage statistics""" total_guests: int = Field(..., alias="totalGuests") active_last_hour: int = Field(..., alias="activeLastHour") active_last_day: int = Field(..., alias="activeLastDay") converted_guests: int = Field(..., alias="convertedGuests") by_ip: Dict[str, int] = Field(..., alias="byIp") creation_timeline: Dict[str, int] = Field(..., alias="creationTimeline") model_config = { "populate_by_name": True } from llm_proxy import (LLMMessage) class ApiMessage(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: ApiStatusType type: ApiMessageType timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="timestamp") model_config = { "populate_by_name": True # Allow both field names and aliases } MOCK_UUID = str(uuid.uuid4()) class ChatMessageStreaming(ApiMessage): status: ApiStatusType = ApiStatusType.STREAMING type: ApiMessageType = ApiMessageType.TEXT content: str class ApiActivityType(str, Enum): SYSTEM = "system" # Used solely on frontend INFO = "info" # Used solely on frontend SEARCHING = "searching" # Used when generating RAG information THINKING = "thinking" # Used when determing if AI will use tools GENERATING = "generating" # Used when AI is generating a response CONVERTING = "converting" # Used when AI is generating a response GENERATING_IMAGE = "generating_image" # Used when AI is generating an image TOOLING = "tooling" # Used when AI is using tools HEARTBEAT = "heartbeat" # Used for periodic updates class ChatMessageStatus(ApiMessage): sender_id: Optional[str] = Field(default=MOCK_UUID, alias="senderId") status: ApiStatusType = ApiStatusType.STATUS type: ApiMessageType = ApiMessageType.TEXT activity: ApiActivityType content: Any class ChatMessageError(ApiMessage): sender_id: Optional[str] = Field(default=MOCK_UUID, alias="senderId") status: ApiStatusType = ApiStatusType.ERROR type: ApiMessageType = ApiMessageType.TEXT content: str class ChatMessageRagSearch(ApiMessage): type: ApiMessageType = ApiMessageType.JSON dimensions: int = 2 | 3 content: List[ChromaDBGetResponse] = [] class JobRequirementsMessage(ApiMessage): type: ApiMessageType = ApiMessageType.JSON job: Job = Field(..., alias="job") class DocumentMessage(ApiMessage): type: ApiMessageType = ApiMessageType.JSON sender_id: Optional[str] = Field(default=MOCK_UUID, alias="senderId") document: Document = Field(..., alias="document") content: Optional[str] = "" converted: bool = Field(False, alias="converted") model_config = { "populate_by_name": True # Allow both field names and aliases } 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: float = Field(default=0, alias="frequencyPenalty") presence_penalty: float = Field(default=0, alias="presencePenalty") stop_sequences: List[str] = Field(default=[], 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: Dict[str, Any] = Field(default_factory=dict) timers: Dict[str, float] = Field(default_factory=dict) model_config = { "populate_by_name": True # Allow both field names and aliases } class ChatMessageUser(ApiMessage): type: ApiMessageType = ApiMessageType.TEXT status: ApiStatusType = ApiStatusType.DONE role: ChatSenderType = ChatSenderType.USER content: str = "" tunables: Optional[Tunables] = None class ChatMessage(ChatMessageUser): role: ChatSenderType = ChatSenderType.ASSISTANT metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData()) #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") class ChatMessageSkillAssessment(ChatMessageUser): role: ChatSenderType = ChatSenderType.ASSISTANT metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData()) skill_assessment: SkillAssessment = Field(..., alias="skillAssessment") class ChatMessageResume(ChatMessageUser): role: ChatSenderType = ChatSenderType.ASSISTANT metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData()) resume: str = Field(..., alias="resume") system_prompt: Optional[str] = Field(None, alias="systemPrompt") prompt: Optional[str] = Field(None, alias="prompt") class GPUInfo(BaseModel): name: str memory: int discrete: bool class SystemInfo(BaseModel): installed_RAM: str = Field(..., alias="installedRAM") graphics_cards: List[GPUInfo] = Field(..., alias="graphicsCards") CPU: str llm_model: str = Field(default=defines.model, alias="llmModel") embedding_model: str = Field(default=defines.embedding_model, alias="embeddingModel") max_context_length: int = Field(default=defines.max_context, alias="maxContextLength") model_config = { "populate_by_name": True # Allow both field names and aliases } class ChatSession(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) user_id: Optional[str] = Field(None, alias="userId") 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(default_factory=lambda: datetime.now(UTC), alias="createdAt") updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), 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): 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 User = Union[Candidate, CandidateAI, Employer, Guest] # Forward references resolution Candidate.update_forward_refs() Employer.update_forward_refs() ChatSession.update_forward_refs() JobApplication.update_forward_refs() Job.update_forward_refs()