backstory/src/backend/models.py

1277 lines
47 KiB
Python

from typing import List, Dict, Optional, Any, Union, Literal, TypeVar, Generic, Annotated
from pydantic import BaseModel, Field, EmailStr, HttpUrl, model_validator, field_validator # type: ignore
from pydantic.types import constr, conint # type: ignore
from datetime import datetime, date, UTC
from enum import Enum
import uuid
from auth_utils import (
AuthenticationManager,
validate_password_strength,
sanitize_login_input,
SecurityConfig
)
import defines
# Generic type variable
T = TypeVar('T')
# ============================
# Enums
# ============================
class UserType(str, Enum):
CANDIDATE = "candidate"
EMPLOYER = "employer"
GUEST = "guest"
class UserGender(str, Enum):
FEMALE = "female"
MALE = "male"
class UserStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
PENDING = "pending"
BANNED = "banned"
class SkillLevel(str, Enum):
BEGINNER = "beginner"
INTERMEDIATE = "intermediate"
ADVANCED = "advanced"
EXPERT = "expert"
class EmploymentType(str, Enum):
FULL_TIME = "full-time"
PART_TIME = "part-time"
CONTRACT = "contract"
INTERNSHIP = "internship"
FREELANCE = "freelance"
class InterviewType(str, Enum):
PHONE = "phone"
VIDEO = "video"
ONSITE = "onsite"
TECHNICAL = "technical"
BEHAVIORAL = "behavioral"
class ApplicationStatus(str, Enum):
APPLIED = "applied"
REVIEWING = "reviewing"
INTERVIEW = "interview"
OFFER = "offer"
REJECTED = "rejected"
ACCEPTED = "accepted"
WITHDRAWN = "withdrawn"
class InterviewRecommendation(str, Enum):
STRONG_HIRE = "strong_hire"
HIRE = "hire"
NO_HIRE = "no_hire"
STRONG_NO_HIRE = "strong_no_hire"
class ChatSenderType(str, Enum):
USER = "user"
ASSISTANT = "assistant"
# Frontend can use this to set mock responses
SYSTEM = "system"
INFORMATION = "information"
WARNING = "warning"
ERROR = "error"
class SkillStatus(str, Enum):
PENDING = "pending"
COMPLETE = "complete"
WAITING = "waiting"
ERROR = "error"
class SkillStrength(str, Enum):
STRONG = "strong"
MODERATE = "moderate"
WEAK = "weak"
NONE = "none"
class EvidenceDetail(BaseModel):
source: str = Field(..., alias="source", description="The source of the evidence (e.g., resume section, position, project)")
quote: str = Field(..., alias="quote", description="Exact text from the resume or other source showing evidence")
context: str = Field(..., alias="context", description="Brief explanation of how this demonstrates the skill")
model_config = {
"populate_by_name": True, # Allow both field names and aliases
}
class ChromaDBGetResponse(BaseModel):
# Chroma fields
ids: List[str] = []
embeddings: List[List[float]] = []
documents: List[str] = []
metadatas: List[Dict[str, Any]] = []
distances: List[float] = []
# Additional fields
name: str = ""
size: int = 0
dimensions: int = 2 | 3
query: str = ""
query_embedding: Optional[List[float]] = Field(default=None, alias="queryEmbedding")
umap_embedding_2d: Optional[List[float]] = Field(default=None, alias="umapEmbedding2D")
umap_embedding_3d: Optional[List[float]] = Field(default=None, alias="umapEmbedding3D")
class SkillAssessment(BaseModel):
candidate_id: str = Field(..., alias='candidateId')
skill: str = Field(..., alias="skill", description="The skill being assessed")
skill_modified: Optional[str] = Field(default="", alias="skillModified", description="The skill rephrased by LLM during skill match")
evidence_found: bool = Field(..., alias="evidenceFound", description="Whether evidence was found for the skill")
evidence_strength: SkillStrength = Field(..., alias="evidenceStrength", description="Strength of evidence found for the skill")
assessment: str = Field(..., alias="assessment", description="Short (one to two sentence) assessment of the candidate's proficiency with the skill")
description: str = Field(..., alias="description", description="Short (two to three sentence) description of what the skill is, independent of whether the candidate has that skill or not")
evidence_details: List[EvidenceDetail] = Field(default_factory=list, alias="evidenceDetails", description="List of evidence details supporting the skill assessment")
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias='createdAt')
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias='updatedAt')
rag_results: List[ChromaDBGetResponse] = Field(default_factory=list, alias="ragResults")
model_config = {
"populate_by_name": True, # Allow both field names and aliases
}
class ApiMessageType(str, Enum):
BINARY = "binary"
TEXT = "text"
JSON = "json"
class ApiStatusType(str, Enum):
STREAMING = "streaming"
STATUS = "status"
DONE = "done"
ERROR = "error"
class ChatContextType(str, Enum):
JOB_SEARCH = "job_search"
JOB_REQUIREMENTS = "job_requirements"
CANDIDATE_CHAT = "candidate_chat"
INTERVIEW_PREP = "interview_prep"
RESUME_REVIEW = "resume_review"
GENERAL = "general"
GENERATE_PERSONA = "generate_persona"
GENERATE_PROFILE = "generate_profile"
GENERATE_RESUME = "generate_resume"
GENERATE_IMAGE = "generate_image"
RAG_SEARCH = "rag_search"
SKILL_MATCH = "skill_match"
class AIModelType(str, Enum):
QWEN2_5 = "qwen2.5"
FLUX_SCHNELL = "flux-schnell"
class MFAMethod(str, Enum):
APP = "app"
SMS = "sms"
EMAIL = "email"
class VectorStoreType(str, Enum):
CHROMA = "chroma",
# FAISS = "faiss",
# PINECONE = "pinecone"
# QDRANT = "qdrant"
# FAISS = "faiss"
# MILVUS = "milvus"
# WEAVIATE = "weaviate"
class DataSourceType(str, Enum):
DOCUMENT = "document"
WEBSITE = "website"
API = "api"
DATABASE = "database"
INTERNAL = "internal"
class ProcessingStepType(str, Enum):
EXTRACT = "extract"
TRANSFORM = "transform"
CHUNK = "chunk"
EMBED = "embed"
FILTER = "filter"
SUMMARIZE = "summarize"
class SearchType(str, Enum):
SIMILARITY = "similarity"
MMR = "mmr"
HYBRID = "hybrid"
KEYWORD = "keyword"
class ActivityType(str, Enum):
LOGIN = "login"
SEARCH = "search"
VIEW_JOB = "view_job"
APPLY_JOB = "apply_job"
MESSAGE = "message"
UPDATE_PROFILE = "update_profile"
CHAT = "chat"
class ThemePreference(str, Enum):
LIGHT = "light"
DARK = "dark"
SYSTEM = "system"
class NotificationType(str, Enum):
EMAIL = "email"
PUSH = "push"
IN_APP = "in_app"
class FontSize(str, Enum):
SMALL = "small"
MEDIUM = "medium"
LARGE = "large"
class SalaryPeriod(str, Enum):
HOUR = "hour"
DAY = "day"
MONTH = "month"
YEAR = "year"
class LanguageProficiency(str, Enum):
BASIC = "basic"
CONVERSATIONAL = "conversational"
FLUENT = "fluent"
NATIVE = "native"
class SocialPlatform(str, Enum):
LINKEDIN = "linkedin"
TWITTER = "twitter"
GITHUB = "github"
DRIBBBLE = "dribbble"
BEHANCE = "behance"
WEBSITE = "website"
OTHER = "other"
class ColorBlindMode(str, Enum):
PROTANOPIA = "protanopia"
DEUTERANOPIA = "deuteranopia"
TRITANOPIA = "tritanopia"
NONE = "none"
class SortOrder(str, Enum):
ASC = "asc"
DESC = "desc"
class LoginRequest(BaseModel):
login: str # Can be email or username
password: str
@field_validator('login')
def sanitize_login(cls, v):
return sanitize_login_input(v)
@field_validator('password')
def validate_password_not_empty(cls, v):
if not v or not v.strip():
raise ValueError('Password cannot be empty')
return v
# ============================
# MFA Models
# ============================
class EmailVerificationRequest(BaseModel):
token: str
class MFARequest(BaseModel):
username: str
password: str
device_id: str = Field(..., alias="deviceId")
device_name: str = Field(..., alias="deviceName")
email: str = Field(..., alias="email")
model_config = {
"populate_by_name": True, # Allow both field names and aliases
}
class MFAVerifyRequest(BaseModel):
email: EmailStr
code: str
device_id: str = Field(..., alias="deviceId")
remember_device: bool = Field(False, alias="rememberDevice")
model_config = {
"populate_by_name": True, # Allow both field names and aliases
}
class MFAData(BaseModel):
message: str
device_id: str = Field(..., alias="deviceId")
device_name: str = Field(..., alias="deviceName")
code_sent: str = Field(..., alias="codeSent")
email: str
model_config = {
"populate_by_name": True, # Allow both field names and aliases
}
class MFARequestResponse(BaseModel):
mfa_required: bool = Field(..., alias="mfaRequired")
mfa_data: Optional[MFAData] = Field(None, alias="mfaData")
model_config = {
"populate_by_name": True, # Allow both field names and aliases
}
class ResendVerificationRequest(BaseModel):
email: EmailStr
# ============================
# Supporting Models
# ============================
class Tunables(BaseModel):
enable_rag: bool = Field(True, alias="enableRAG")
enable_tools: bool = Field(True, alias="enableTools")
enable_context: bool = Field(True, alias="enableContext")
class CandidateQuestion(BaseModel):
question: str
tunables: Optional[Tunables] = None
class Location(BaseModel):
city: str
state: Optional[str] = None
country: str
postal_code: Optional[str] = Field(None, alias="postalCode")
latitude: Optional[float] = None
longitude: Optional[float] = None
remote: Optional[bool] = None
hybrid_options: Optional[List[str]] = Field(None, alias="hybridOptions")
address: Optional[str] = None
class Skill(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
category: str
level: SkillLevel
years_of_experience: Optional[int] = Field(None, alias="yearsOfExperience")
class WorkExperience(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
company_name: str = Field(..., alias="companyName")
position: str
start_date: datetime = Field(..., alias="startDate")
end_date: Optional[datetime] = Field(None, alias="endDate")
is_current: bool = Field(..., alias="isCurrent")
description: str
skills: List[str]
location: Location
achievements: Optional[List[str]] = None
class Education(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
institution: str
degree: str
field_of_study: str = Field(..., alias="fieldOfStudy")
start_date: datetime = Field(..., alias="startDate")
end_date: Optional[datetime] = Field(None, alias="endDate")
is_current: bool = Field(..., alias="isCurrent")
gpa: Optional[float] = None
achievements: Optional[List[str]] = None
location: Optional[Location] = None
class Language(BaseModel):
language: str
proficiency: LanguageProficiency
class Certification(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
issuing_organization: str = Field(..., alias="issuingOrganization")
issue_date: datetime = Field(..., alias="issueDate")
expiration_date: Optional[datetime] = Field(None, alias="expirationDate")
credential_id: Optional[str] = Field(None, alias="credentialId")
credential_url: Optional[HttpUrl] = Field(None, alias="credentialUrl")
class SocialLink(BaseModel):
platform: SocialPlatform
url: HttpUrl
class DesiredSalary(BaseModel):
amount: float
currency: str
period: SalaryPeriod
class SalaryRange(BaseModel):
min: float
max: float
currency: str
period: SalaryPeriod
is_visible: bool = Field(..., alias="isVisible")
class PointOfContact(BaseModel):
name: str
position: str
email: EmailStr
phone: Optional[str] = None
class RefreshToken(BaseModel):
token: str
expires_at: datetime = Field(..., alias="expiresAt")
device: str
ip_address: str = Field(..., alias="ipAddress")
is_revoked: bool = Field(..., alias="isRevoked")
revoked_reason: Optional[str] = Field(None, alias="revokedReason")
class Attachment(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
file_name: str = Field(..., alias="fileName")
file_type: str = Field(..., alias="fileType")
file_size: int = Field(..., alias="fileSize")
file_url: str = Field(..., alias="fileUrl")
uploaded_at: datetime = Field(..., alias="uploadedAt")
is_processed: bool = Field(..., alias="isProcessed")
processing_result: Optional[Any] = Field(None, alias="processingResult")
thumbnail_url: Optional[str] = Field(None, alias="thumbnailUrl")
class MessageReaction(BaseModel):
user_id: str = Field(..., alias="userId")
reaction: str
timestamp: datetime
class EditHistory(BaseModel):
content: str
edited_at: datetime = Field(..., alias="editedAt")
edited_by: str = Field(..., alias="editedBy")
class CustomQuestion(BaseModel):
question: str
answer: str
class CandidateContact(BaseModel):
email: EmailStr
phone: Optional[str] = None
class ApplicationDecision(BaseModel):
status: Literal["accepted", "rejected"]
reason: Optional[str] = None
date: datetime
by: str
class NotificationPreference(BaseModel):
type: NotificationType
events: List[str]
is_enabled: bool = Field(..., alias="isEnabled")
class AccessibilitySettings(BaseModel):
font_size: FontSize = Field(..., alias="fontSize")
high_contrast: bool = Field(..., alias="highContrast")
reduce_motion: bool = Field(..., alias="reduceMotion")
screen_reader: bool = Field(..., alias="screenReader")
color_blind_mode: Optional[ColorBlindMode] = Field(None, alias="colorBlindMode")
class ProcessingStep(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
type: ProcessingStepType
parameters: Dict[str, Any]
order: int
depends_on: Optional[List[str]] = Field(None, alias="dependsOn")
class RetrievalParameters(BaseModel):
search_type: SearchType = Field(..., alias="searchType")
top_k: int = Field(..., alias="topK")
similarity_threshold: Optional[float] = Field(None, alias="similarityThreshold")
reranker_model: Optional[str] = Field(None, alias="rerankerModel")
use_keyword_boost: bool = Field(..., alias="useKeywordBoost")
filter_options: Optional[Dict[str, Any]] = Field(None, alias="filterOptions")
context_window: int = Field(..., alias="contextWindow")
class ErrorDetail(BaseModel):
code: str
message: str
details: Optional[Any] = None
# ============================
# Main Models
# ============================
# Generic base user with user_type for API responses
class BaseUserWithType(BaseModel):
user_type: UserType = Field(..., alias="userType")
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="lastActivity")
model_config = {
"populate_by_name": True, # Allow both field names and aliases
"use_enum_values": True # Use enum values instead of names
}
# Base user model without user_type field
class BaseUser(BaseUserWithType):
email: EmailStr
first_name: str = Field(..., alias="firstName")
last_name: str = Field(..., alias="lastName")
full_name: str = Field(..., alias="fullName")
phone: Optional[str] = None
location: Optional[Location] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
last_login: Optional[datetime] = Field(None, alias="lastLogin")
profile_image: Optional[str] = Field(None, alias="profileImage")
status: UserStatus
is_admin: bool = Field(default=False, alias="isAdmin")
model_config = {
"populate_by_name": True, # Allow both field names and aliases
"use_enum_values": True # Use enum values instead of names
}
class RagEntry(BaseModel):
name: str
description: str = ""
enabled: bool = True
class RagContentMetadata(BaseModel):
source_file: str = Field(..., alias="sourceFile")
line_begin: int = Field(..., alias="lineBegin")
line_end: int = Field(..., alias="lineEnd")
lines: int
chunk_begin: Optional[int] = Field(None, alias="chunkBegin")
chunk_end: Optional[int] = Field(None, alias="chunkEnd")
metadata: Dict[str, Any] = Field(default_factory=dict)
model_config = {
"populate_by_name": True, # Allow both field names and aliases
}
class RagContentResponse(BaseModel):
id: str
content: str
metadata: RagContentMetadata
class DocumentType(str, Enum):
PDF = "pdf"
DOCX = "docx"
TXT = "txt"
MARKDOWN = "markdown"
IMAGE = "image"
class DocumentOptions(BaseModel):
include_in_rag: bool = Field(default=True, alias="includeInRag")
is_job_document: Optional[bool] = Field(default=False, alias="isJobDocument")
overwrite: Optional[bool] = Field(default=False, alias="overwrite")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class Document(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
owner_id: str = Field(..., alias="ownerId")
filename: str
originalName: str
type: DocumentType
size: int
upload_date: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="uploadDate")
options: DocumentOptions = Field(default_factory=lambda: DocumentOptions(), alias="options")
rag_chunks: Optional[int] = Field(default=0, alias="ragChunks")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class DocumentContentResponse(BaseModel):
document_id: str = Field(..., alias="documentId")
filename: str
type: DocumentType
content: str
size: int
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class DocumentListResponse(BaseModel):
documents: List[Document]
total: int
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class DocumentUpdateRequest(BaseModel):
filename: Optional[str] = None
options: Optional[DocumentOptions] = None
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class Candidate(BaseUser):
user_type: UserType = Field(UserType.CANDIDATE, alias="userType")
username: str
description: Optional[str] = None
resume: Optional[str] = None
skills: Optional[List[Skill]] = None
experience: Optional[List[WorkExperience]] = None
questions: Optional[List[CandidateQuestion]] = None
education: Optional[List[Education]] = None
preferred_job_types: Optional[List[EmploymentType]] = Field(None, alias="preferredJobTypes")
desired_salary: Optional[DesiredSalary] = Field(None, alias="desiredSalary")
availability_date: Optional[datetime] = Field(None, alias="availabilityDate")
summary: Optional[str] = None
languages: Optional[List[Language]] = None
certifications: Optional[List[Certification]] = None
job_applications: Optional[List["JobApplication"]] = Field(None, alias="jobApplications")
rags: List[RagEntry] = Field(default_factory=list)
rag_content_size : int = 0
class CandidateAI(Candidate):
user_type: UserType = Field(UserType.CANDIDATE, alias="userType")
is_AI: bool = Field(True, alias="isAI")
age: Optional[int] = None
gender: Optional[UserGender] = None
ethnicity: Optional[str] = None
class Employer(BaseUser):
user_type: UserType = Field(UserType.EMPLOYER, alias="userType")
company_name: str = Field(..., alias="companyName")
industry: str
description: Optional[str] = None
company_size: str = Field(..., alias="companySize")
company_description: str = Field(..., alias="companyDescription")
website_url: Optional[HttpUrl] = Field(None, alias="websiteUrl")
jobs: Optional[List["Job"]] = None
company_logo: Optional[str] = Field(None, alias="companyLogo")
social_links: Optional[List[SocialLink]] = Field(None, alias="socialLinks")
poc: Optional[PointOfContact] = None
class Guest(BaseUser):
user_type: UserType = Field(UserType.GUEST, alias="userType")
session_id: str = Field(..., alias="sessionId")
username: str # Add username for consistency with other user types
converted_to_user_id: Optional[str] = Field(None, alias="convertedToUserId")
ip_address: Optional[str] = Field(None, alias="ipAddress")
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
user_agent: Optional[str] = Field(None, alias="userAgent")
rag_content_size: int = 0
model_config = {
"populate_by_name": True, # Allow both field names and aliases
"use_enum_values": True # Use enum values instead of names
}
class Authentication(BaseModel):
user_id: str = Field(..., alias="userId")
password_hash: str = Field(..., alias="passwordHash")
salt: str
refresh_tokens: List[RefreshToken] = Field(..., alias="refreshTokens")
reset_password_token: Optional[str] = Field(None, alias="resetPasswordToken")
reset_password_expiry: Optional[datetime] = Field(None, alias="resetPasswordExpiry")
last_password_change: datetime = Field(..., alias="lastPasswordChange")
mfa_enabled: bool = Field(..., alias="mfaEnabled")
mfa_method: Optional[MFAMethod] = Field(None, alias="mfaMethod")
mfa_secret: Optional[str] = Field(None, alias="mfaSecret")
login_attempts: int = Field(..., alias="loginAttempts")
locked_until: Optional[datetime] = Field(None, alias="lockedUntil")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class AuthResponse(BaseModel):
access_token: str = Field(..., alias="accessToken")
refresh_token: str = Field(..., alias="refreshToken")
user: Union[Candidate, Employer, Guest] # Add Guest support
expires_at: int = Field(..., alias="expiresAt")
user_type: Optional[str] = Field(default=UserType.GUEST, alias="userType") # Explicit user type
is_guest: Optional[bool] = Field(default=True, alias="isGuest") # Guest indicator
model_config = {
"populate_by_name": True
}
class GuestCleanupRequest(BaseModel):
"""Request to cleanup inactive guests"""
inactive_hours: int = Field(24, alias="inactiveHours")
model_config = {
"populate_by_name": True
}
class Requirements(BaseModel):
required: List[str] = Field(default_factory=list)
preferred: List[str] = Field(default_factory=list)
@model_validator(mode='before')
def validate_requirements(cls, values):
if not isinstance(values, dict):
raise ValueError("Requirements must be a dictionary with 'required' and 'preferred' keys.")
return values
class JobRequirements(BaseModel):
technical_skills: Requirements = Field(..., alias="technicalSkills")
experience_requirements: Requirements = Field(..., alias="experienceRequirements")
soft_skills: Optional[List[str]] = Field(default_factory=list, alias="softSkills")
experience: Optional[List[str]] = []
education: Optional[List[str]] = []
certifications: Optional[List[str]] = []
preferred_attributes: Optional[List[str]] = Field(None, alias="preferredAttributes")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class JobDetails(BaseModel):
location: Location
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
employment_type: EmploymentType = Field(..., alias="employmentType")
date_posted: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="datePosted")
application_deadline: Optional[datetime] = Field(None, alias="applicationDeadline")
is_active: bool = Field(..., alias="isActive")
applicants: Optional[List["JobApplication"]] = None
department: Optional[str] = None
reports_to: Optional[str] = Field(None, alias="reportsTo")
benefits: Optional[List[str]] = None
visa_sponsorship: Optional[bool] = Field(None, alias="visaSponsorship")
featured_until: Optional[datetime] = Field(None, alias="featuredUntil")
views: int = 0
application_count: int = Field(0, alias="applicationCount")
class Job(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
owner_id: str = Field(..., alias="ownerId")
owner_type: UserType = Field(..., alias="ownerType")
owner: Optional[BaseUser] = None
title: Optional[str]
summary: Optional[str]
company: Optional[str]
description: str
requirements: Optional[JobRequirements]
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
details: Optional[JobDetails] = Field(None, alias="details")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class InterviewFeedback(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
interview_id: str = Field(..., alias="interviewId")
reviewer_id: str = Field(..., alias="reviewerId")
technical_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias="technicalScore")
cultural_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias="culturalScore")
overall_score: Annotated[float, Field(ge=0, le=10)] = Field(..., alias="overallScore")
strengths: List[str]
weaknesses: List[str]
recommendation: InterviewRecommendation
comments: str
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
is_visible: bool = Field(..., alias="isVisible")
skill_assessments: Optional[List[SkillAssessment]] = Field(None, alias="skillAssessments")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class InterviewSchedule(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
application_id: str = Field(..., alias="applicationId")
scheduled_date: datetime = Field(..., alias="scheduledDate")
end_date: datetime = Field(..., alias="endDate")
interview_type: InterviewType = Field(..., alias="interviewType")
interviewers: List[str]
location: Optional[Union[str, Location]] = None
notes: Optional[str] = None
feedback: Optional[InterviewFeedback] = None
status: Literal["scheduled", "completed", "cancelled", "rescheduled"]
meeting_link: Optional[HttpUrl] = Field(None, alias="meetingLink")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class JobApplication(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
job_id: str = Field(..., alias="jobId")
candidate_id: str = Field(..., alias="candidateId")
status: ApplicationStatus
applied_date: datetime = Field(..., alias="appliedDate")
updated_date: datetime = Field(..., alias="updatedDate")
resume_version: str = Field(..., alias="resumeVersion")
cover_letter: Optional[str] = Field(None, alias="coverLetter")
notes: Optional[str] = None
interview_schedules: Optional[List[InterviewSchedule]] = Field(None, alias="interviewSchedules")
custom_questions: Optional[List[CustomQuestion]] = Field(None, alias="customQuestions")
candidate_contact: Optional[CandidateContact] = Field(None, alias="candidateContact")
decision: Optional[ApplicationDecision] = None
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class GuestSessionResponse(BaseModel):
"""Response for guest session creation"""
access_token: str = Field(..., alias="accessToken")
refresh_token: str = Field(..., alias="refreshToken")
user: Guest
expires_at: int = Field(..., alias="expiresAt")
user_type: Literal["guest"] = Field("guest", alias="userType")
is_guest: bool = Field(True, alias="isGuest")
model_config = {
"populate_by_name": True
}
class ChatContext(BaseModel):
type: ChatContextType
related_entity_id: Optional[str] = Field(None, alias="relatedEntityId")
related_entity_type: Optional[Literal["job", "candidate", "employer"]] = Field(None, alias="relatedEntityType")
additional_context: Optional[Dict[str, Any]] = Field({}, alias="additionalContext")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class ChatOptions(BaseModel):
seed: Optional[int] = 8911
num_ctx: Optional[int] = Field(default=None, alias="numCtx") # Number of context tokens
temperature: Optional[float] = Field(default=0.7) # Higher temperature to encourage tool usage
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
# Add rate limiting configuration models
class RateLimitConfig(BaseModel):
"""Rate limit configuration"""
requests_per_minute: int = Field(..., alias="requestsPerMinute")
requests_per_hour: int = Field(..., alias="requestsPerHour")
requests_per_day: int = Field(..., alias="requestsPerDay")
burst_limit: int = Field(..., alias="burstLimit")
burst_window_seconds: int = Field(60, alias="burstWindowSeconds")
model_config = {
"populate_by_name": True
}
class RateLimitResult(BaseModel):
"""Result of rate limit check"""
allowed: bool
reason: Optional[str] = None
retry_after_seconds: Optional[int] = Field(None, alias="retryAfterSeconds")
remaining_requests: Dict[str, int] = Field(default_factory=dict, alias="remainingRequests")
reset_times: Dict[str, datetime] = Field(default_factory=dict, alias="resetTimes")
model_config = {
"populate_by_name": True
}
class RateLimitStatus(BaseModel):
"""Rate limit status for a user"""
user_id: str = Field(..., alias="userId")
user_type: str = Field(..., alias="userType")
is_admin: bool = Field(..., alias="isAdmin")
current_usage: Dict[str, int] = Field(..., alias="currentUsage")
limits: Dict[str, int] = Field(..., alias="limits")
remaining: Dict[str, int] = Field(..., alias="remaining")
reset_times: Dict[str, datetime] = Field(..., alias="resetTimes")
config: RateLimitConfig
model_config = {
"populate_by_name": True
}
# Add guest conversion request models
class GuestConversionRequest(BaseModel):
"""Request to convert guest to permanent user"""
account_type: Literal["candidate", "employer"] = Field(..., alias="accountType")
email: EmailStr
username: str
password: str
first_name: str = Field(..., alias="firstName")
last_name: str = Field(..., alias="lastName")
phone: Optional[str] = None
# Employer-specific fields (optional)
company_name: Optional[str] = Field(None, alias="companyName")
industry: Optional[str] = None
company_size: Optional[str] = Field(None, alias="companySize")
company_description: Optional[str] = Field(None, alias="companyDescription")
website_url: Optional[HttpUrl] = Field(None, alias="websiteUrl")
model_config = {
"populate_by_name": True
}
@field_validator('username')
def validate_username(cls, v):
if not v or len(v.strip()) < 3:
raise ValueError('Username must be at least 3 characters long')
return v.strip().lower()
@field_validator('password')
def validate_password_strength(cls, v):
# Import here to avoid circular imports
from auth_utils import validate_password_strength
is_valid, issues = validate_password_strength(v)
if not is_valid:
raise ValueError('; '.join(issues))
return v
# Add guest statistics response model
class GuestStatistics(BaseModel):
"""Guest usage statistics"""
total_guests: int = Field(..., alias="totalGuests")
active_last_hour: int = Field(..., alias="activeLastHour")
active_last_day: int = Field(..., alias="activeLastDay")
converted_guests: int = Field(..., alias="convertedGuests")
by_ip: Dict[str, int] = Field(..., alias="byIp")
creation_timeline: Dict[str, int] = Field(..., alias="creationTimeline")
model_config = {
"populate_by_name": True
}
from llm_proxy import (LLMMessage)
class ApiMessage(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
session_id: str = Field(..., alias="sessionId")
sender_id: Optional[str] = Field(None, alias="senderId")
status: ApiStatusType
type: ApiMessageType
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="timestamp")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
MOCK_UUID = str(uuid.uuid4())
class ChatMessageStreaming(ApiMessage):
status: ApiStatusType = ApiStatusType.STREAMING
type: ApiMessageType = ApiMessageType.TEXT
content: str
class ApiActivityType(str, Enum):
SYSTEM = "system" # Used solely on frontend
INFO = "info" # Used solely on frontend
SEARCHING = "searching" # Used when generating RAG information
THINKING = "thinking" # Used when determing if AI will use tools
GENERATING = "generating" # Used when AI is generating a response
CONVERTING = "converting" # Used when AI is generating a response
GENERATING_IMAGE = "generating_image" # Used when AI is generating an image
TOOLING = "tooling" # Used when AI is using tools
HEARTBEAT = "heartbeat" # Used for periodic updates
class ChatMessageStatus(ApiMessage):
sender_id: Optional[str] = Field(default=MOCK_UUID, alias="senderId")
status: ApiStatusType = ApiStatusType.STATUS
type: ApiMessageType = ApiMessageType.TEXT
activity: ApiActivityType
content: Any
class ChatMessageError(ApiMessage):
sender_id: Optional[str] = Field(default=MOCK_UUID, alias="senderId")
status: ApiStatusType = ApiStatusType.ERROR
type: ApiMessageType = ApiMessageType.TEXT
content: str
class ChatMessageRagSearch(ApiMessage):
type: ApiMessageType = ApiMessageType.JSON
dimensions: int = 2 | 3
content: List[ChromaDBGetResponse] = []
class JobRequirementsMessage(ApiMessage):
type: ApiMessageType = ApiMessageType.JSON
job: Job = Field(..., alias="job")
class DocumentMessage(ApiMessage):
type: ApiMessageType = ApiMessageType.JSON
sender_id: Optional[str] = Field(default=MOCK_UUID, alias="senderId")
document: Document = Field(..., alias="document")
content: Optional[str] = ""
converted: bool = Field(False, alias="converted")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class ChatMessageMetaData(BaseModel):
model: AIModelType = AIModelType.QWEN2_5
temperature: float = 0.7
max_tokens: int = Field(default=8092, alias="maxTokens")
top_p: float = Field(default=1, alias="topP")
frequency_penalty: float = Field(default=0, alias="frequencyPenalty")
presence_penalty: float = Field(default=0, alias="presencePenalty")
stop_sequences: List[str] = Field(default=[], alias="stopSequences")
rag_results: List[ChromaDBGetResponse] = Field(default_factory=list, alias="ragResults")
llm_history: List[LLMMessage] = Field(default_factory=list, alias="llmHistory")
eval_count: int = 0
eval_duration: int = 0
prompt_eval_count: int = 0
prompt_eval_duration: int = 0
options: Optional[ChatOptions] = None
tools: Dict[str, Any] = Field(default_factory=dict)
timers: Dict[str, float] = Field(default_factory=dict)
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class ChatMessageUser(ApiMessage):
type: ApiMessageType = ApiMessageType.TEXT
status: ApiStatusType = ApiStatusType.DONE
role: ChatSenderType = ChatSenderType.USER
content: str = ""
tunables: Optional[Tunables] = None
class ChatMessage(ChatMessageUser):
role: ChatSenderType = ChatSenderType.ASSISTANT
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
#attachments: Optional[List[Attachment]] = None
#reactions: Optional[List[MessageReaction]] = None
#is_edited: bool = Field(False, alias="isEdited")
#edit_history: Optional[List[EditHistory]] = Field(None, alias="editHistory")
class ChatMessageSkillAssessment(ChatMessageUser):
role: ChatSenderType = ChatSenderType.ASSISTANT
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
skill_assessment: SkillAssessment = Field(..., alias="skillAssessment")
class ChatMessageResume(ChatMessageUser):
role: ChatSenderType = ChatSenderType.ASSISTANT
metadata: ChatMessageMetaData = Field(default=ChatMessageMetaData())
resume: str = Field(..., alias="resume")
system_prompt: Optional[str] = Field(None, alias="systemPrompt")
prompt: Optional[str] = Field(None, alias="prompt")
class GPUInfo(BaseModel):
name: str
memory: int
discrete: bool
class SystemInfo(BaseModel):
installed_RAM: str = Field(..., alias="installedRAM")
graphics_cards: List[GPUInfo] = Field(..., alias="graphicsCards")
CPU: str
llm_model: str = Field(default=defines.model, alias="llmModel")
embedding_model: str = Field(default=defines.embedding_model, alias="embeddingModel")
max_context_length: int = Field(default=defines.max_context, alias="maxContextLength")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class ChatSession(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
user_id: Optional[str] = Field(None, alias="userId")
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="lastActivity")
title: Optional[str] = None
context: ChatContext
# messages: Optional[List[ChatMessage]] = None
is_archived: bool = Field(False, alias="isArchived")
system_prompt: Optional[str] = Field(None, alias="systemPrompt")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
@model_validator(mode="after")
def check_user_or_guest(self) -> "ChatSession":
if not self.user_id and not self.guest_id:
raise ValueError("Either user_id or guest_id must be provided")
return self
class DataSourceConfiguration(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
rag_config_id: str = Field(..., alias="ragConfigId")
name: str
source_type: DataSourceType = Field(..., alias="sourceType")
connection_details: Dict[str, Any] = Field(..., alias="connectionDetails")
processing_pipeline: List[ProcessingStep] = Field(..., alias="processingPipeline")
refresh_schedule: Optional[str] = Field(None, alias="refreshSchedule")
last_refreshed: Optional[datetime] = Field(None, alias="lastRefreshed")
status: Literal["active", "pending", "error", "processing"]
error_details: Optional[str] = Field(None, alias="errorDetails")
metadata: Optional[Dict[str, Any]] = None
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class RAGConfiguration(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
user_id: str = Field(..., alias="userId")
name: str
description: Optional[str] = None
data_source_configurations: List[DataSourceConfiguration] = Field(..., alias="dataSourceConfigurations")
embedding_model: str = Field(..., alias="embeddingModel")
vector_store_type: VectorStoreType = Field(..., alias="vectorStoreType")
retrieval_parameters: RetrievalParameters = Field(..., alias="retrievalParameters")
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="updatedAt")
version: int
is_active: bool = Field(..., alias="isActive")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class UserActivity(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
user_id: Optional[str] = Field(None, alias="userId")
guest_id: Optional[str] = Field(None, alias="guestId")
activity_type: ActivityType = Field(..., alias="activityType")
timestamp: datetime
metadata: Dict[str, Any]
ip_address: Optional[str] = Field(None, alias="ipAddress")
user_agent: Optional[str] = Field(None, alias="userAgent")
session_id: Optional[str] = Field(None, alias="sessionId")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
@model_validator(mode="after")
def check_user_or_guest(self):
if not self.user_id and not self.guest_id:
raise ValueError("Either user_id or guest_id must be provided")
return self
class Analytics(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
entity_type: Literal["job", "candidate", "chat", "system", "employer"] = Field(..., alias="entityType")
entity_id: str = Field(..., alias="entityId")
metric_type: str = Field(..., alias="metricType")
value: float
timestamp: datetime
dimensions: Optional[Dict[str, Any]] = None
segment: Optional[str] = None
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class UserPreference(BaseModel):
user_id: str = Field(..., alias="userId")
theme: ThemePreference
notifications: List[NotificationPreference]
accessibility: AccessibilitySettings
dashboard_layout: Optional[Dict[str, Any]] = Field(None, alias="dashboardLayout")
language: str
timezone: str
email_frequency: Literal["immediate", "daily", "weekly", "never"] = Field(..., alias="emailFrequency")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
# ============================
# API Request/Response Models
# ============================
class CreateCandidateRequest(BaseModel):
email: EmailStr
username: str
password: str
first_name: str = Field(..., alias="firstName")
last_name: str = Field(..., alias="lastName")
# Add other required candidate fields as needed
phone: Optional[str] = None
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
@field_validator('username')
def validate_username(cls, v):
if not v or len(v.strip()) < 3:
raise ValueError('Username must be at least 3 characters long')
return v.strip().lower()
@field_validator('password')
def validate_password_strength(cls, v):
is_valid, issues = validate_password_strength(v)
if not is_valid:
raise ValueError('; '.join(issues))
return v
# Create Employer Endpoint (similar pattern)
class CreateEmployerRequest(BaseModel):
email: EmailStr
username: str
password: str
company_name: str = Field(..., alias="companyName")
industry: str
company_size: str = Field(..., alias="companySize")
company_description: str = Field(..., alias="companyDescription")
# Add other required employer fields
website_url: Optional[str] = Field(None, alias="websiteUrl")
phone: Optional[str] = None
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
@field_validator('username')
def validate_username(cls, v):
if not v or len(v.strip()) < 3:
raise ValueError('Username must be at least 3 characters long')
return v.strip().lower()
@field_validator('password')
def validate_password_strength(cls, v):
is_valid, issues = validate_password_strength(v)
if not is_valid:
raise ValueError('; '.join(issues))
return v
class ChatQuery(BaseModel):
prompt: str
tunables: Optional[Tunables] = None
agent_options: Optional[Dict[str, Any]] = Field(None, alias="agentOptions")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class PaginatedRequest(BaseModel):
page: Annotated[int, Field(ge=1)] = 1
limit: Annotated[int, Field(ge=1, le=100)] = 20
sort_by: Optional[str] = Field(None, alias="sortBy")
sort_order: Optional[SortOrder] = Field(None, alias="sortOrder")
filters: Optional[Dict[str, Any]] = None
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class SearchQuery(BaseModel):
query: str
filters: Optional[Dict[str, Any]] = None
page: Annotated[int, Field(ge=1)] = 1
limit: Annotated[int, Field(ge=1, le=100)] = 20
sort_by: Optional[str] = Field(None, alias="sortBy")
sort_order: Optional[SortOrder] = Field(None, alias="sortOrder")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class PaginatedResponse(BaseModel):
data: List[Any] # Will be typed specifically when used
total: int
page: int
limit: int
total_pages: int = Field(..., alias="totalPages")
has_more: bool = Field(..., alias="hasMore")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class ApiResponse(BaseModel):
success: bool
data: Optional[Any] = None # Will be typed specifically when used
error: Optional[ErrorDetail] = None
meta: Optional[Dict[str, Any]] = None
# Specific typed response models for common use cases
class CandidateResponse(BaseModel):
success: bool
data: Optional[Candidate] = None
error: Optional[ErrorDetail] = None
meta: Optional[Dict[str, Any]] = None
class EmployerResponse(BaseModel):
success: bool
data: Optional[Employer] = None
error: Optional[ErrorDetail] = None
meta: Optional[Dict[str, Any]] = None
class JobResponse(BaseModel):
success: bool
data: Optional[Job] = None
error: Optional[ErrorDetail] = None
meta: Optional[Dict[str, Any]] = None
class CandidateListResponse(BaseModel):
success: bool
data: Optional[List[Candidate]] = None
error: Optional[ErrorDetail] = None
meta: Optional[Dict[str, Any]] = None
class JobListResponse(BaseModel):
success: bool
data: Optional[List["Job"]] = None
error: Optional[ErrorDetail] = None
meta: Optional[Dict[str, Any]] = None
User = Union[Candidate, CandidateAI, Employer, Guest]
# Forward references resolution
Candidate.update_forward_refs()
Employer.update_forward_refs()
ChatSession.update_forward_refs()
JobApplication.update_forward_refs()
Job.update_forward_refs()