backstory/src/backend/models.py

1478 lines
53 KiB
Python

from typing import List, Dict, Optional, Any, Union, Literal, TypeVar, Annotated
from pydantic import BaseModel, Field, EmailStr, HttpUrl, model_validator, field_validator, ConfigDict
from datetime import datetime, UTC
from enum import Enum
import os
import uuid
from utils.auth_utils import (
validate_password_strength,
sanitize_login_input,
)
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"
UNKNOWN = "unknown"
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"))
match_score: float = Field(default=0.0, alias=str("matchScore"))
model_config = ConfigDict(populate_by_name=True)
class JobAnalysis(BaseModel):
job_id: str = Field(..., alias=str("jobId"))
candidate_id: str = Field(..., alias=str("candidateId"))
skills: List[SkillAssessment] = Field(...)
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_CHAT = "resume_chat"
RESUME_REVIEW = "resume_review"
EDIT_RESUME = "edit_resume"
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(default=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"))
temperature: float = Field(default=0.7, alias=str("temperature"))
top_k: int = Field(default=5, alias=str("topK"))
threshold: Optional[float] = Field(default=None, alias=str("threshold"))
class CandidateQuestion(BaseModel):
question: str
tunables: Optional[Tunables] = None
class Location(BaseModel):
text: str
city: Optional[str] = None
state: Optional[str] = None
country: Optional[str] = None
postal_code: Optional[str] = Field(default=None, alias=str("postalCode"))
latitude: Optional[float] = None
longitude: Optional[float] = None
remote: Optional[bool] = None
hybrid_options: Optional[List[str]] = Field(default=None, alias=str("hybridOptions"))
address: Optional[str] = None
model_config = ConfigDict(populate_by_name=True, use_enum_values=True)
@model_validator(mode="before")
@classmethod
def set_text_from_location(cls, data: Any) -> Any:
if isinstance(data, dict):
# Check if text is missing, None, or empty string
if not data.get("text"):
city = data.get("city", "")
country = data.get("country", "")
# Build text from available location components
location_parts = []
if city:
location_parts.append(city)
if country:
location_parts.append(country)
# Set text to combined location or a default if both are empty
data["text"] = ", ".join(location_parts) if location_parts else "Unknown Location"
return data
class Skill(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
category: str
level: SkillLevel
years_of_experience: Optional[int] = Field(default=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(default=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(default=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(default=None, alias=str("expirationDate"))
credential_id: Optional[str] = Field(default=None, alias=str("credentialId"))
credential_url: Optional[HttpUrl] = Field(default=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(default=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(default=None, alias=str("processingResult"))
thumbnail_url: Optional[str] = Field(default=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(default=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(default=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(default=None, alias=str("similarityThreshold"))
reranker_model: Optional[str] = Field(default=None, alias=str("rerankerModel"))
use_keyword_boost: bool = Field(..., alias=str("useKeywordBoost"))
filter_options: Optional[Dict[str, Any]] = Field(default=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(default=None, alias=str("lastLogin"))
profile_image: Optional[str] = Field(default=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(default=None, alias=str("chunkBegin"))
chunk_end: Optional[int] = Field(default=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(default=None, alias=str("preferredJobTypes"))
desired_salary: Optional[DesiredSalary] = Field(default=None, alias=str("desiredSalary"))
availability_date: Optional[datetime] = Field(default=None, alias=str("availabilityDate"))
summary: Optional[str] = None
languages: Optional[List[Language]] = None
certifications: Optional[List[Certification]] = None
job_applications: Optional[List["JobApplication"]] = Field(default=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(default=None, alias=str("websiteUrl"))
jobs: Optional[List["Job"]] = None
company_logo: Optional[str] = Field(default=None, alias=str("companyLogo"))
social_links: Optional[List[SocialLink]] = Field(default=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(default=None, alias=str("convertedToUserId"))
ip_address: Optional[str] = Field(default=None, alias=str("ipAddress"))
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias=str("createdAt"))
user_agent: Optional[str] = Field(default=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(default=None, alias=str("resetPasswordToken"))
reset_password_expiry: Optional[datetime] = Field(default=None, alias=str("resetPasswordExpiry"))
last_password_change: datetime = Field(..., alias=str("lastPasswordChange"))
mfa_enabled: bool = Field(..., alias=str("mfaEnabled"))
mfa_method: Optional[MFAMethod] = Field(default=None, alias=str("mfaMethod"))
mfa_secret: Optional[str] = Field(default=None, alias=str("mfaSecret"))
login_attempts: int = Field(..., alias=str("loginAttempts"))
locked_until: Optional[datetime] = Field(default=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(default=None, alias=str("preferredAttributes"))
company_values: Optional[List[str]] = Field(default=None, alias=str("companyValues"))
model_config = ConfigDict(populate_by_name=True)
class JobDetails(BaseModel):
location: Location
salary_range: Optional[SalaryRange] = Field(default=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(default=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(default=None, alias=str("reportsTo"))
benefits: Optional[List[str]] = None
visa_sponsorship: Optional[bool] = Field(default=None, alias=str("visaSponsorship"))
featured_until: Optional[datetime] = Field(default=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]
pay_minimum: Optional[str] = Field(default=None, alias=str("payMinimum"))
pay_maximum: Optional[str] = Field(default=None, alias=str("payMaximum"))
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(default=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(default=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(default=None, alias=str("coverLetter"))
notes: Optional[str] = None
interview_schedules: Optional[List[InterviewSchedule]] = Field(default=None, alias=str("interviewSchedules"))
custom_questions: Optional[List[CustomQuestion]] = Field(default=None, alias=str("customQuestions"))
candidate_contact: Optional[CandidateContact] = Field(default=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(default=None, alias=str("relatedEntityId"))
related_entity_type: Optional[Literal["job", "candidate", "employer"]] = Field(
default=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)
# Enhanced usage statistics model
class UsageStats(BaseModel):
"""Comprehensive usage statistics across all providers"""
# Token counts (standardized across providers)
prompt_tokens: Optional[int] = Field(default=None, description="Number of tokens in the prompt")
completion_tokens: Optional[int] = Field(default=None, description="Number of tokens in the completion")
total_tokens: Optional[int] = Field(default=None, description="Total number of tokens used")
# Ollama-specific detailed stats
prompt_eval_count: Optional[int] = Field(default=None, description="Number of tokens evaluated in prompt")
prompt_eval_duration: Optional[int] = Field(default=None, description="Time spent evaluating prompt (nanoseconds)")
eval_count: Optional[int] = Field(default=None, description="Number of tokens generated")
eval_duration: Optional[int] = Field(default=None, description="Time spent generating tokens (nanoseconds)")
total_duration: Optional[int] = Field(default=None, description="Total request duration (nanoseconds)")
# Performance metrics
tokens_per_second: Optional[float] = Field(default=None, description="Generation speed in tokens/second")
prompt_tokens_per_second: Optional[float] = Field(default=None, description="Prompt processing speed")
# Additional provider-specific stats
extra_stats: Optional[Dict[str, Any]] = Field(
default_factory=dict, description="Provider-specific additional statistics"
)
def calculate_derived_stats(self) -> None:
"""Calculate derived statistics where possible"""
# Calculate tokens per second for Ollama
if self.eval_count and self.eval_duration and self.eval_duration > 0:
# Convert nanoseconds to seconds and calculate tokens/sec
duration_seconds = self.eval_duration / 1_000_000_000
self.tokens_per_second = self.eval_count / duration_seconds
# Calculate prompt processing speed for Ollama
if self.prompt_eval_count and self.prompt_eval_duration and self.prompt_eval_duration > 0:
duration_seconds = self.prompt_eval_duration / 1_000_000_000
self.prompt_tokens_per_second = self.prompt_eval_count / duration_seconds
# Standardize token counts across providers
if not self.total_tokens and self.prompt_tokens and self.completion_tokens:
self.total_tokens = self.prompt_tokens + self.completion_tokens
# Map Ollama counts to standard format if not already set
if not self.prompt_tokens and self.prompt_eval_count:
self.prompt_tokens = self.prompt_eval_count
if not self.completion_tokens and self.eval_count:
self.completion_tokens = self.eval_count
class ChatResponse(BaseModel):
content: str
model: str
finish_reason: Optional[str] = Field(default="")
usage: Optional[UsageStats] = Field(default=None)
# Keep legacy usage field for backward compatibility
usage_legacy: Optional[Dict[str, int]] = Field(default=None, alias="usage_dict")
def get_usage_dict(self) -> Dict[str, Any]:
"""Get usage statistics as dictionary for backward compatibility"""
if self.usage:
return self.usage.model_dump(exclude_none=True)
return self.usage_legacy or {}
# 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(default=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(default=None, alias=str("companyName"))
industry: Optional[str] = None
company_size: Optional[str] = Field(default=None, alias=str("companySize"))
company_description: Optional[str] = Field(default=None, alias=str("companyDescription"))
website_url: Optional[HttpUrl] = Field(default=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
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 ToolCall(BaseModel):
name: str
content: str
model_config = ConfigDict(populate_by_name=True)
class Tool(BaseModel):
# generate-types doesn't support nested-nested
tool_calls: List[dict[str, str]] = Field(default=[], alias=str("toolCalls"))
messages: List[LLMMessage] = Field(default_factory=list)
available: List[str] = Field(default_factory=list)
tool_name: str = Field(..., alias=str("toolName"))
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"))
usage: UsageStats = Field(default_factory=UsageStats)
options: Optional[ChatOptions] = None
tools: Optional[Tool] = None
timers: Dict[str, float] = Field(default_factory=dict)
model_config = ConfigDict(populate_by_name=True)
class SkillMatchRequest(BaseModel):
"""Request model for skill match"""
skill: str
regenerate: bool = Field(default=False, description="Whether to regenerate the skill match even if cached")
class ExtraChatContext(BaseModel):
"""Extra context for chat messages"""
job_id: Optional[str] = Field(default=None, alias=str("jobId"))
candidate_id: Optional[str] = Field(default=None, alias=str("candidateId"))
resume_id: Optional[str] = Field(default=None, alias=str("resumeId"))
resume: Optional[str] = Field(default=None)
is_answer: Optional[bool] = Field(default=None, alias=str("isAnswer"))
model_config = ConfigDict(populate_by_name=True)
class ChatMessageUser(ApiMessage):
type: ApiMessageType = ApiMessageType.TEXT
status: ApiStatusType = ApiStatusType.DONE
role: ChatSenderType = ChatSenderType.USER
content: str = ""
extra_context: Optional[ExtraChatContext] = Field(default=None, alias=str("extraContext"))
tunables: Optional[Tunables] = None
class ChatMessage(ChatMessageUser):
role: ChatSenderType = ChatSenderType.ASSISTANT
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
class ChatMessageSkillAssessment(ChatMessageUser):
role: ChatSenderType = ChatSenderType.ASSISTANT
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
skill_assessment: SkillAssessment = Field(..., alias=str("skillAssessment"))
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
system_prompt: Optional[str] = Field(default=None)
prompt: Optional[str] = Field(default=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"))
job: Optional[Job] = None
candidate: Optional[Candidate] = None
model_config = ConfigDict(populate_by_name=True)
class ChatMessageResume(ChatMessageUser):
role: ChatSenderType = ChatSenderType.ASSISTANT
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
resume: Resume
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_backend: str = Field(default=os.getenv("DEFAULT_LLM_PROVIDER", "openai"), alias=str("llmBackend"))
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(default=None, alias=str("userId"))
guest_id: Optional[str] = Field(default=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
is_archived: bool = Field(False, alias=str("isArchived"))
system_prompt: Optional[str] = Field(default=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(default=None, alias=str("refreshSchedule"))
last_refreshed: Optional[datetime] = Field(default=None, alias=str("lastRefreshed"))
status: Literal["active", "pending", "error", "processing"]
error_details: Optional[str] = Field(default=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(default=None, alias=str("userId"))
guest_id: Optional[str] = Field(default=None, alias=str("guestId"))
activity_type: ActivityType = Field(..., alias=str("activityType"))
timestamp: datetime
metadata: Dict[str, Any]
ip_address: Optional[str] = Field(default=None, alias=str("ipAddress"))
user_agent: Optional[str] = Field(default=None, alias=str("userAgent"))
session_id: Optional[str] = Field(default=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(default=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(default=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(default=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(default=None, alias=str("sortBy"))
sort_order: Optional[SortOrder] = Field(default=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(default=None, alias=str("sortBy"))
sort_order: Optional[SortOrder] = Field(default=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()