from typing import List, Dict, Optional, Any, Union, Literal, TypeVar, Generic, Annotated from pydantic import BaseModel, Field, EmailStr, HttpUrl, model_validator, field_validator, ConfigDict from pydantic.types import constr, conint 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=str("source"), description="The source of the evidence (e.g., resume section, position, project)") quote: str = Field(..., alias=str("quote"), description="Exact text from the resume or other source showing evidence") context: str = Field(..., alias=str("context"), description="Brief explanation of how this demonstrates the skill") model_config = ConfigDict(populate_by_name=True) 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=str("queryEmbedding")) umap_embedding_2d: Optional[List[float]] = Field(default=None, alias=str("umapEmbedding2D")) umap_embedding_3d: Optional[List[float]] = Field(default=None, alias=str("umapEmbedding3D")) class SkillAssessment(BaseModel): candidate_id: str = Field(..., alias=str('candidateId')) skill: str = Field(..., alias=str("skill"), description="The skill being assessed") skill_modified: Optional[str] = Field(default="", alias=str("skillModified"), description="The skill rephrased by LLM during skill match") evidence_found: bool = Field(..., alias=str("evidenceFound"), description="Whether evidence was found for the skill") evidence_strength: SkillStrength = Field(..., alias=str("evidenceStrength"), description="Strength of evidence found for the skill") assessment: str = Field(..., alias=str("assessment"), description="Short (one to two sentence) assessment of the candidate's proficiency with the skill") description: str = Field(..., alias=str("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=str("evidenceDetails"), description="List of evidence details supporting the skill assessment") created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("createdAt")) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("updatedAt")) rag_results: List[ChromaDBGetResponse] = Field(default_factory=list, alias=str("ragResults")) model_config = ConfigDict(populate_by_name=True) 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=str("deviceId")) device_name: str = Field(..., alias=str("deviceName")) email: str = Field(..., alias=str("email")) model_config = ConfigDict(populate_by_name=True) class MFAVerifyRequest(BaseModel): email: EmailStr code: str device_id: str = Field(..., alias=str("deviceId")) remember_device: bool = Field(False, alias=str("rememberDevice")) model_config = ConfigDict(populate_by_name=True) class MFAData(BaseModel): message: str device_id: str = Field(..., alias=str("deviceId")) device_name: str = Field(..., alias=str("deviceName")) code_sent: str = Field(..., alias=str("codeSent")) email: str model_config = ConfigDict(populate_by_name=True) class MFARequestResponse(BaseModel): mfa_required: bool = Field(..., alias=str("mfaRequired")) mfa_data: Optional[MFAData] = Field(None, alias=str("mfaData")) model_config = ConfigDict(populate_by_name=True) class ResendVerificationRequest(BaseModel): email: EmailStr # ============================ # Supporting Models # ============================ class Tunables(BaseModel): enable_rag: bool = Field(default=True, alias=str("enableRAG")) enable_tools: bool = Field(default=True, alias=str("enableTools")) enable_context: bool = Field(default=True, alias=str("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=str("postalCode")) latitude: Optional[float] = None longitude: Optional[float] = None remote: Optional[bool] = None hybrid_options: Optional[List[str]] = Field(None, alias=str("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=str("yearsOfExperience")) class WorkExperience(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) company_name: str = Field(..., alias=str("companyName")) position: str start_date: datetime = Field(..., alias=str("startDate")) end_date: Optional[datetime] = Field(None, alias=str("endDate")) is_current: bool = Field(..., alias=str("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=str("fieldOfStudy")) start_date: datetime = Field(..., alias=str("startDate")) end_date: Optional[datetime] = Field(None, alias=str("endDate")) is_current: bool = Field(..., alias=str("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=str("issuingOrganization")) issue_date: datetime = Field(..., alias=str("issueDate")) expiration_date: Optional[datetime] = Field(None, alias=str("expirationDate")) credential_id: Optional[str] = Field(None, alias=str("credentialId")) credential_url: Optional[HttpUrl] = Field(None, alias=str("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=str("isVisible")) class PointOfContact(BaseModel): name: str position: str email: EmailStr phone: Optional[str] = None class RefreshToken(BaseModel): token: str expires_at: datetime = Field(..., alias=str("expiresAt")) device: str ip_address: str = Field(..., alias=str("ipAddress")) is_revoked: bool = Field(..., alias=str("isRevoked")) revoked_reason: Optional[str] = Field(None, alias=str("revokedReason")) class Attachment(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) file_name: str = Field(..., alias=str("fileName")) file_type: str = Field(..., alias=str("fileType")) file_size: int = Field(..., alias=str("fileSize")) file_url: str = Field(..., alias=str("fileUrl")) uploaded_at: datetime = Field(..., alias=str("uploadedAt")) is_processed: bool = Field(..., alias=str("isProcessed")) processing_result: Optional[Any] = Field(None, alias=str("processingResult")) thumbnail_url: Optional[str] = Field(None, alias=str("thumbnailUrl")) class MessageReaction(BaseModel): user_id: str = Field(..., alias=str("userId")) reaction: str timestamp: datetime class EditHistory(BaseModel): content: str edited_at: datetime = Field(..., alias=str("editedAt")) edited_by: str = Field(..., alias=str("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=str("isEnabled")) class AccessibilitySettings(BaseModel): font_size: FontSize = Field(..., alias=str("fontSize")) high_contrast: bool = Field(..., alias=str("highContrast")) reduce_motion: bool = Field(..., alias=str("reduceMotion")) screen_reader: bool = Field(..., alias=str("screenReader")) color_blind_mode: Optional[ColorBlindMode] = Field(None, alias=str("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=str("dependsOn")) class RetrievalParameters(BaseModel): search_type: SearchType = Field(..., alias=str("searchType")) top_k: int = Field(..., alias=str("topK")) similarity_threshold: Optional[float] = Field(None, alias=str("similarityThreshold")) reranker_model: Optional[str] = Field(None, alias=str("rerankerModel")) use_keyword_boost: bool = Field(..., alias=str("useKeywordBoost")) filter_options: Optional[Dict[str, Any]] = Field(None, alias=str("filterOptions")) context_window: int = Field(..., alias=str("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=str("userType")) id: str = Field(default_factory=lambda: str(uuid.uuid4())) last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("lastActivity")) model_config = ConfigDict(populate_by_name=True, use_enum_values=True) # Base user model without user_type field class BaseUser(BaseUserWithType): email: EmailStr first_name: str = Field(..., alias=str("firstName")) last_name: str = Field(..., alias=str("lastName")) full_name: str = Field(..., alias=str("fullName")) phone: Optional[str] = None location: Optional[Location] = None created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("createdAt")) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("updatedAt")) last_login: Optional[datetime] = Field(None, alias=str("lastLogin")) profile_image: Optional[str] = Field(None, alias=str("profileImage")) status: UserStatus is_admin: bool = Field(default=False, alias=str("isAdmin")) model_config = ConfigDict(populate_by_name=True, use_enum_values=True) class RagEntry(BaseModel): name: str description: str = "" enabled: bool = True class RagContentMetadata(BaseModel): source_file: str = Field(..., alias=str("sourceFile")) line_begin: int = Field(..., alias=str("lineBegin")) line_end: int = Field(..., alias=str("lineEnd")) lines: int chunk_begin: Optional[int] = Field(None, alias=str("chunkBegin")) chunk_end: Optional[int] = Field(None, alias=str("chunkEnd")) metadata: Dict[str, Any] = Field(default_factory=dict) model_config = ConfigDict(populate_by_name=True) 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=str("includeInRag")) is_job_document: Optional[bool] = Field(default=False, alias=str("isJobDocument")) overwrite: Optional[bool] = Field(default=False, alias=str("overwrite")) model_config = ConfigDict(populate_by_name=True) class RAGDocumentRequest(BaseModel): """Request model for RAG document content""" id: str class Document(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) owner_id: str = Field(..., alias=str("ownerId")) filename: str original_name: str = Field(..., alias=str("originalName")) type: DocumentType size: int upload_date: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("uploadDate")) options: DocumentOptions = Field(default_factory=lambda: DocumentOptions(), alias=str("options")) rag_chunks: Optional[int] = Field(default=0, alias=str("ragChunks")) model_config = ConfigDict(populate_by_name=True) class DocumentContentResponse(BaseModel): document_id: str = Field(..., alias=str("documentId")) filename: str type: DocumentType content: str size: int model_config = ConfigDict(populate_by_name=True) class DocumentListResponse(BaseModel): documents: List[Document] total: int model_config = ConfigDict(populate_by_name=True) class DocumentUpdateRequest(BaseModel): filename: Optional[str] = None options: Optional[DocumentOptions] = None model_config = ConfigDict(populate_by_name=True) class Candidate(BaseUser): user_type: UserType = Field(UserType.CANDIDATE, alias=str("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=str("preferredJobTypes")) desired_salary: Optional[DesiredSalary] = Field(None, alias=str("desiredSalary")) availability_date: Optional[datetime] = Field(None, alias=str("availabilityDate")) summary: Optional[str] = None languages: Optional[List[Language]] = None certifications: Optional[List[Certification]] = None job_applications: Optional[List["JobApplication"]] = Field(None, alias=str("jobApplications")) rags: List[RagEntry] = Field(default_factory=list) rag_content_size : int = 0 is_public: bool = Field(default=True, alias=str("isPublic")) class CandidateAI(Candidate): user_type: UserType = Field(UserType.CANDIDATE, alias=str("userType")) is_AI: bool = Field(True, alias=str("isAI")) age: Optional[int] = None gender: Optional[UserGender] = None ethnicity: Optional[str] = None class Employer(BaseUser): user_type: UserType = Field(UserType.EMPLOYER, alias=str("userType")) company_name: str = Field(..., alias=str("companyName")) industry: str description: Optional[str] = None company_size: str = Field(..., alias=str("companySize")) company_description: str = Field(..., alias=str("companyDescription")) website_url: Optional[HttpUrl] = Field(None, alias=str("websiteUrl")) jobs: Optional[List["Job"]] = None company_logo: Optional[str] = Field(None, alias=str("companyLogo")) social_links: Optional[List[SocialLink]] = Field(None, alias=str("socialLinks")) poc: Optional[PointOfContact] = None class Guest(BaseUser): user_type: UserType = Field(UserType.GUEST, alias=str("userType")) session_id: str = Field(..., alias=str("sessionId")) username: str # Add username for consistency with other user types converted_to_user_id: Optional[str] = Field(None, alias=str("convertedToUserId")) ip_address: Optional[str] = Field(None, alias=str("ipAddress")) created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("createdAt")) user_agent: Optional[str] = Field(None, alias=str("userAgent")) rag_content_size: int = 0 is_public: bool = Field(default=False, alias=str("isPublic")) model_config = ConfigDict(populate_by_name=True, use_enum_values=True) class Authentication(BaseModel): user_id: str = Field(..., alias=str("userId")) password_hash: str = Field(..., alias=str("passwordHash")) salt: str refresh_tokens: List[RefreshToken] = Field(..., alias=str("refreshTokens")) reset_password_token: Optional[str] = Field(None, alias=str("resetPasswordToken")) reset_password_expiry: Optional[datetime] = Field(None, alias=str("resetPasswordExpiry")) last_password_change: datetime = Field(..., alias=str("lastPasswordChange")) mfa_enabled: bool = Field(..., alias=str("mfaEnabled")) mfa_method: Optional[MFAMethod] = Field(None, alias=str("mfaMethod")) mfa_secret: Optional[str] = Field(None, alias=str("mfaSecret")) login_attempts: int = Field(..., alias=str("loginAttempts")) locked_until: Optional[datetime] = Field(None, alias=str("lockedUntil")) model_config = ConfigDict(populate_by_name=True) class AuthResponse(BaseModel): access_token: str = Field(..., alias=str("accessToken")) refresh_token: str = Field(..., alias=str("refreshToken")) user: Union[Candidate, Employer, Guest] # Add Guest support expires_at: int = Field(..., alias=str("expiresAt")) user_type: Optional[str] = Field(default=UserType.GUEST, alias=str("userType")) # Explicit user type is_guest: Optional[bool] = Field(default=True, alias=str("isGuest")) # Guest indicator model_config = ConfigDict(populate_by_name=True) class GuestCleanupRequest(BaseModel): """Request to cleanup inactive guests""" inactive_hours: int = Field(24, alias=str("inactiveHours")) model_config = ConfigDict(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=str("technicalSkills")) experience_requirements: Requirements = Field(..., alias=str("experienceRequirements")) soft_skills: Optional[List[str]] = Field(default_factory=list, alias=str("softSkills")) experience: Optional[List[str]] = [] education: Optional[List[str]] = [] certifications: Optional[List[str]] = [] preferred_attributes: Optional[List[str]] = Field(None, alias=str("preferredAttributes")) company_values: Optional[List[str]] = Field(None, alias=str("companyValues")) model_config = ConfigDict(populate_by_name=True) class JobDetails(BaseModel): location: Location salary_range: Optional[SalaryRange] = Field(None, alias=str("salaryRange")) employment_type: EmploymentType = Field(..., alias=str("employmentType")) date_posted: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("datePosted")) application_deadline: Optional[datetime] = Field(None, alias=str("applicationDeadline")) is_active: bool = Field(..., alias=str("isActive")) applicants: Optional[List["JobApplication"]] = None department: Optional[str] = None reports_to: Optional[str] = Field(None, alias=str("reportsTo")) benefits: Optional[List[str]] = None visa_sponsorship: Optional[bool] = Field(None, alias=str("visaSponsorship")) featured_until: Optional[datetime] = Field(None, alias=str("featuredUntil")) views: int = 0 application_count: int = Field(0, alias=str("applicationCount")) class Job(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) owner_id: str = Field(..., alias=str("ownerId")) owner_type: UserType = Field(..., alias=str("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=str("createdAt")) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("updatedAt")) details: Optional[JobDetails] = None model_config = ConfigDict(populate_by_name=True) class InterviewFeedback(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) interview_id: str = Field(..., alias=str("interviewId")) reviewer_id: str = Field(..., alias=str("reviewerId")) technical_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias=str("technicalScore")) cultural_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias=str("culturalScore")) overall_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias=str("overallScore")) strengths: List[str] weaknesses: List[str] recommendation: InterviewRecommendation comments: str created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("createdAt")) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("updatedAt")) is_visible: bool = Field(..., alias=str("isVisible")) skill_assessments: Optional[List[SkillAssessment]] = Field(None, alias=str("skillAssessments")) model_config = ConfigDict(populate_by_name=True) class InterviewSchedule(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) application_id: str = Field(..., alias=str("applicationId")) scheduled_date: datetime = Field(..., alias=str("scheduledDate")) end_date: datetime = Field(..., alias=str("endDate")) interview_type: InterviewType = Field(..., alias=str("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=str("meetingLink")) model_config = ConfigDict(populate_by_name=True) class JobApplication(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) job_id: str = Field(..., alias=str("jobId")) candidate_id: str = Field(..., alias=str("candidateId")) status: ApplicationStatus applied_date: datetime = Field(..., alias=str("appliedDate")) updated_date: datetime = Field(..., alias=str("updatedDate")) resume_version: str = Field(..., alias=str("resumeVersion")) cover_letter: Optional[str] = Field(None, alias=str("coverLetter")) notes: Optional[str] = None interview_schedules: Optional[List[InterviewSchedule]] = Field(None, alias=str("interviewSchedules")) custom_questions: Optional[List[CustomQuestion]] = Field(None, alias=str("customQuestions")) candidate_contact: Optional[CandidateContact] = Field(None, alias=str("candidateContact")) decision: Optional[ApplicationDecision] = None model_config = ConfigDict(populate_by_name=True) class GuestSessionResponse(BaseModel): """Response for guest session creation""" access_token: str = Field(..., alias=str("accessToken")) refresh_token: str = Field(..., alias=str("refreshToken")) user: Guest expires_at: int = Field(..., alias=str("expiresAt")) user_type: Literal["guest"] = Field("guest", alias=str("userType")) is_guest: bool = Field(True, alias=str("isGuest")) model_config = ConfigDict(populate_by_name=True) class ChatContext(BaseModel): type: ChatContextType related_entity_id: Optional[str] = Field(None, alias=str("relatedEntityId")) related_entity_type: Optional[Literal["job", "candidate", "employer"]] = Field(None, alias=str("relatedEntityType")) additional_context: Optional[Dict[str, Any]] = Field({}, alias=str("additionalContext")) model_config = ConfigDict(populate_by_name=True) class ChatOptions(BaseModel): seed: Optional[int] = 8911 num_ctx: Optional[int] = Field(default=None, alias=str("numCtx")) # Number of context tokens temperature: Optional[float] = Field(default=0.7) # Higher temperature to encourage tool usage model_config = ConfigDict(populate_by_name=True) # Add rate limiting configuration models class RateLimitConfig(BaseModel): """Rate limit configuration""" requests_per_minute: int = Field(..., alias=str("requestsPerMinute")) requests_per_hour: int = Field(..., alias=str("requestsPerHour")) requests_per_day: int = Field(..., alias=str("requestsPerDay")) burst_limit: int = Field(..., alias=str("burstLimit")) burst_window_seconds: int = Field(60, alias=str("burstWindowSeconds")) model_config = ConfigDict(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=str("retryAfterSeconds")) remaining_requests: Dict[str, int] = Field(default_factory=dict, alias=str("remainingRequests")) reset_times: Dict[str, datetime] = Field(default_factory=dict, alias=str("resetTimes")) model_config = ConfigDict(populate_by_name=True) class RateLimitStatus(BaseModel): """Rate limit status for a user""" user_id: str = Field(..., alias=str("userId")) user_type: str = Field(..., alias=str("userType")) is_admin: bool = Field(..., alias=str("isAdmin")) current_usage: Dict[str, int] = Field(..., alias=str("currentUsage")) limits: Dict[str, int] = Field(..., alias=str("limits")) remaining: Dict[str, int] = Field(..., alias=str("remaining")) reset_times: Dict[str, datetime] = Field(..., alias=str("resetTimes")) config: RateLimitConfig model_config = ConfigDict(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=str("accountType")) email: EmailStr username: str password: str first_name: str = Field(..., alias=str("firstName")) last_name: str = Field(..., alias=str("lastName")) phone: Optional[str] = None # Employer-specific fields (optional) company_name: Optional[str] = Field(None, alias=str("companyName")) industry: Optional[str] = None company_size: Optional[str] = Field(None, alias=str("companySize")) company_description: Optional[str] = Field(None, alias=str("companyDescription")) website_url: Optional[HttpUrl] = Field(None, alias=str("websiteUrl")) model_config = ConfigDict(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=str("totalGuests")) active_last_hour: int = Field(..., alias=str("activeLastHour")) active_last_day: int = Field(..., alias=str("activeLastDay")) converted_guests: int = Field(..., alias=str("convertedGuests")) by_ip: Dict[str, int] = Field(..., alias=str("byIp")) creation_timeline: Dict[str, int] = Field(..., alias=str("creationTimeline")) model_config = ConfigDict(populate_by_name=True) from utils.llm_proxy import (LLMMessage) class ApiMessage(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) session_id: str = Field(..., alias=str("sessionId")) sender_id: Optional[str] = Field(default=None, alias=str("senderId")) status: ApiStatusType type: ApiMessageType timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("timestamp")) model_config = ConfigDict(populate_by_name=True) 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=str("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=str("senderId")) status: ApiStatusType = ApiStatusType.ERROR type: ApiMessageType = ApiMessageType.TEXT content: str class ChatMessageRagSearch(ApiMessage): type: ApiMessageType = Field(default=ApiMessageType.JSON) dimensions: int = 2 | 3 content: List[ChromaDBGetResponse] = [] class JobRequirementsMessage(ApiMessage): type: ApiMessageType = ApiMessageType.JSON job: Job = Field(..., alias=str("job")) class DocumentMessage(ApiMessage): type: ApiMessageType = ApiMessageType.JSON sender_id: Optional[str] = Field(default=MOCK_UUID, alias=str("senderId")) document: Document = Field(..., alias=str("document")) content: Optional[str] = "" converted: bool = Field(False, alias=str("converted")) model_config = ConfigDict(populate_by_name=True) class ChatMessageMetaData(BaseModel): model: AIModelType = AIModelType.QWEN2_5 temperature: float = 0.7 max_tokens: int = Field(default=8092, alias=str("maxTokens")) top_p: float = Field(default=1, alias=str("topP")) frequency_penalty: float = Field(default=0, alias=str("frequencyPenalty")) presence_penalty: float = Field(default=0, alias=str("presencePenalty")) stop_sequences: List[str] = Field(default=[], alias=str("stopSequences")) rag_results: List[ChromaDBGetResponse] = Field(default_factory=list, alias=str("ragResults")) llm_history: List[LLMMessage] = Field(default_factory=list, alias=str("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 = ConfigDict(populate_by_name=True) 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=str("isEdited")) #edit_history: Optional[List[EditHistory]] = Field(None, alias=str("editHistory")) class ChatMessageSkillAssessment(ChatMessageUser): role: ChatSenderType = ChatSenderType.ASSISTANT metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData()) skill_assessment: SkillAssessment = Field(..., alias=str("skillAssessment")) class ChatMessageResume(ChatMessageUser): role: ChatSenderType = ChatSenderType.ASSISTANT metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData()) resume: str = Field(..., alias=str("resume")) system_prompt: Optional[str] = Field(None, alias=str("systemPrompt")) prompt: Optional[str] = Field(None, alias=str("prompt")) model_config = ConfigDict(populate_by_name=True) class Resume(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) job_id: str = Field(..., alias=str("jobId")) candidate_id: str = Field(..., alias=str("candidateId")) resume: str = Field(..., alias=str("resume")) created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("createdAt")) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("updatedAt")) job: Optional[Job] = None candidate: Optional[Candidate] = None model_config = ConfigDict(populate_by_name=True) class ResumeMessage(ChatMessageUser): role: ChatSenderType = ChatSenderType.ASSISTANT resume: Resume model_config = ConfigDict(populate_by_name=True) class GPUInfo(BaseModel): name: str memory: int discrete: bool class SystemInfo(BaseModel): installed_RAM: str = Field(..., alias=str("installedRAM")) graphics_cards: List[GPUInfo] = Field(..., alias=str("graphicsCards")) CPU: str llm_model: str = Field(default=defines.model, alias=str("llmModel")) embedding_model: str = Field(default=defines.embedding_model, alias=str("embeddingModel")) max_context_length: int = Field(default=defines.max_context, alias=str("maxContextLength")) model_config = ConfigDict(populate_by_name=True) class ChatSession(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) user_id: Optional[str] = Field(None, alias=str("userId")) guest_id: Optional[str] = Field(None, alias=str("guestId")) created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("createdAt")) last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("lastActivity")) title: Optional[str] = None context: ChatContext # messages: Optional[List[ChatMessage]] = None is_archived: bool = Field(False, alias=str("isArchived")) system_prompt: Optional[str] = Field(None, alias=str("systemPrompt")) model_config = ConfigDict(populate_by_name=True) @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=str("ragConfigId")) name: str source_type: DataSourceType = Field(..., alias=str("sourceType")) connection_details: Dict[str, Any] = Field(..., alias=str("connectionDetails")) processing_pipeline: List[ProcessingStep] = Field(..., alias=str("processingPipeline")) refresh_schedule: Optional[str] = Field(None, alias=str("refreshSchedule")) last_refreshed: Optional[datetime] = Field(None, alias=str("lastRefreshed")) status: Literal["active", "pending", "error", "processing"] error_details: Optional[str] = Field(None, alias=str("errorDetails")) metadata: Optional[Dict[str, Any]] = None model_config = ConfigDict(populate_by_name=True) class RAGConfiguration(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) user_id: str = Field(..., alias=str("userId")) name: str description: Optional[str] = None data_source_configurations: List[DataSourceConfiguration] = Field(..., alias=str("dataSourceConfigurations")) embedding_model: str = Field(..., alias=str("embeddingModel")) vector_store_type: VectorStoreType = Field(..., alias=str("vectorStoreType")) retrieval_parameters: RetrievalParameters = Field(..., alias=str("retrievalParameters")) created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("createdAt")) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("updatedAt")) version: int is_active: bool = Field(..., alias=str("isActive")) model_config = ConfigDict(populate_by_name=True) class UserActivity(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) user_id: Optional[str] = Field(None, alias=str("userId")) guest_id: Optional[str] = Field(None, alias=str("guestId")) activity_type: ActivityType = Field(..., alias=str("activityType")) timestamp: datetime metadata: Dict[str, Any] ip_address: Optional[str] = Field(None, alias=str("ipAddress")) user_agent: Optional[str] = Field(None, alias=str("userAgent")) session_id: Optional[str] = Field(None, alias=str("sessionId")) model_config = ConfigDict(populate_by_name=True) @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=str("entityType")) entity_id: str = Field(..., alias=str("entityId")) metric_type: str = Field(..., alias=str("metricType")) value: float timestamp: datetime dimensions: Optional[Dict[str, Any]] = None segment: Optional[str] = None model_config = ConfigDict(populate_by_name=True) class UserPreference(BaseModel): user_id: str = Field(..., alias=str("userId")) theme: ThemePreference notifications: List[NotificationPreference] accessibility: AccessibilitySettings dashboard_layout: Optional[Dict[str, Any]] = Field(None, alias=str("dashboardLayout")) language: str timezone: str email_frequency: Literal["immediate", "daily", "weekly", "never"] = Field(..., alias=str("emailFrequency")) model_config = ConfigDict(populate_by_name=True) # ============================ # API Request/Response Models # ============================ class CreateCandidateRequest(BaseModel): email: EmailStr username: str password: str first_name: str = Field(..., alias=str("firstName")) last_name: str = Field(..., alias=str("lastName")) # Add other required candidate fields as needed phone: Optional[str] = None model_config = ConfigDict(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): 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=str("companyName")) industry: str company_size: str = Field(..., alias=str("companySize")) company_description: str = Field(..., alias=str("companyDescription")) # Add other required employer fields website_url: Optional[str] = Field(None, alias=str("websiteUrl")) phone: Optional[str] = None model_config = ConfigDict(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): 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=str("agentOptions")) model_config = ConfigDict(populate_by_name=True) 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=str("sortBy")) sort_order: Optional[SortOrder] = Field(None, alias=str("sortOrder")) filters: Optional[Dict[str, Any]] = None model_config = ConfigDict(populate_by_name=True) 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=str("sortBy")) sort_order: Optional[SortOrder] = Field(None, alias=str("sortOrder")) model_config = ConfigDict(populate_by_name=True) class PaginatedResponse(BaseModel): data: List[Any] # Will be typed specifically when used total: int page: int limit: int total_pages: int = Field(..., alias=str("totalPages")) has_more: bool = Field(..., alias=str("hasMore")) model_config = ConfigDict(populate_by_name=True) 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()