backstory/src/backend/models.py

1008 lines
35 KiB
Python

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