235 lines
8.4 KiB
Python
235 lines
8.4 KiB
Python
"""Database schema initialization and connection management."""
|
|
import psycopg2
|
|
from psycopg2.extras import RealDictCursor, Json
|
|
from contextlib import contextmanager
|
|
from config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
SCHEMA_SQL = """
|
|
-- Enable extensions
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
-- Candidates table (person info)
|
|
CREATE TABLE IF NOT EXISTS candidates (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
first_name VARCHAR(255),
|
|
last_name VARCHAR(255),
|
|
email VARCHAR(255),
|
|
phone VARCHAR(255),
|
|
address TEXT,
|
|
linkedin VARCHAR(500),
|
|
github VARCHAR(500),
|
|
website VARCHAR(500),
|
|
summary TEXT,
|
|
-- Full raw text of original CV (extracted from document)
|
|
raw_cv_text TEXT,
|
|
-- Original filename
|
|
source_filename VARCHAR(500),
|
|
-- Metadata
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
-- AI parse status: pending, parsed, error
|
|
parse_status VARCHAR(50) DEFAULT 'pending',
|
|
parse_error TEXT
|
|
);
|
|
|
|
-- Skills table (normalized)
|
|
CREATE TABLE IF NOT EXISTS skills (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE,
|
|
skill_name VARCHAR(255) NOT NULL,
|
|
skill_category VARCHAR(100), -- e.g. "Programming", "Database", "Cloud", "Tools"
|
|
proficiency VARCHAR(50), -- e.g. "Expert", "Advanced", "Intermediate", "Beginner"
|
|
-- When did they start using this skill (for dynamic experience calculation)
|
|
start_date DATE, -- can be just a year or full date
|
|
-- When did they stop (NULL = still using it = "present")
|
|
end_date DATE, -- NULL means still active
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Experience table (work history)
|
|
CREATE TABLE IF NOT EXISTS experience (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE,
|
|
company VARCHAR(255) NOT NULL,
|
|
position VARCHAR(255) NOT NULL,
|
|
location VARCHAR(255),
|
|
start_date DATE,
|
|
end_date DATE, -- NULL = current/present
|
|
description TEXT,
|
|
-- JSON array of achievements/bullet points
|
|
achievements JSONB DEFAULT '[]',
|
|
-- JSON array of skills used in this role
|
|
skills_used JSONB DEFAULT '[]',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Education table
|
|
CREATE TABLE IF NOT EXISTS education (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE,
|
|
institution VARCHAR(255) NOT NULL,
|
|
degree VARCHAR(255),
|
|
field_of_study VARCHAR(255),
|
|
start_date DATE,
|
|
end_date DATE,
|
|
grade VARCHAR(100),
|
|
description TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Certifications table
|
|
CREATE TABLE IF NOT EXISTS certifications (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL,
|
|
issuer VARCHAR(255),
|
|
issue_date DATE,
|
|
expiry_date DATE,
|
|
credential_id VARCHAR(255),
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- CV Templates table
|
|
CREATE TABLE IF NOT EXISTS cv_templates (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
-- JSON structure defining the template layout
|
|
-- sections: [{name, type, order, fields: [...], styling: {...}}]
|
|
template_structure JSONB NOT NULL,
|
|
-- CSS/styling for the template
|
|
styling TEXT,
|
|
-- Whether AI generated this or user created manually
|
|
created_by VARCHAR(50) DEFAULT 'manual', -- 'ai' or 'manual'
|
|
-- The prompt that was used to generate it (if AI)
|
|
generation_prompt TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Requirement requests (customer sends a list of positions to fill)
|
|
CREATE TABLE IF NOT EXISTS requirement_requests (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
customer_name VARCHAR(255),
|
|
-- JSON array of position requirements
|
|
-- [{position_title, quantity, skills: [{name, min_years, required/optional}],
|
|
-- experience_years, education, certifications, description}]
|
|
requirements JSONB NOT NULL DEFAULT '[]',
|
|
status VARCHAR(50) DEFAULT 'active', -- active, completed, archived
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Generated CVs (CVs regenerated to align with specific requirements)
|
|
CREATE TABLE IF NOT EXISTS generated_cvs (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE,
|
|
requirement_request_id UUID REFERENCES requirement_requests(id) ON DELETE CASCADE,
|
|
template_id UUID REFERENCES cv_templates(id) ON DELETE SET NULL,
|
|
-- Which specific position in the requirements this CV targets
|
|
position_title VARCHAR(255),
|
|
-- The generated CV content (formatted text/HTML)
|
|
generated_content TEXT NOT NULL,
|
|
-- JSON snapshot of the data used to generate this CV (for editing)
|
|
generated_data JSONB NOT NULL DEFAULT '{}',
|
|
-- The date this CV was generated (used for dynamic experience calculation)
|
|
generation_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
-- Match score (0-100) how well the candidate matches the requirement
|
|
match_score INTEGER DEFAULT 0,
|
|
-- AI reasoning for the match
|
|
match_reasoning TEXT,
|
|
-- Status: draft, reviewed, approved, rejected
|
|
status VARCHAR(50) DEFAULT 'draft',
|
|
-- User can edit the generated content
|
|
edited_content TEXT,
|
|
edited_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Chat conversations for AI interaction
|
|
CREATE TABLE IF NOT EXISTS chat_conversations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
-- Context: what is the user working on when they started this chat
|
|
context_type VARCHAR(100), -- e.g. "requirement_match", "template_create", "general"
|
|
context_ref_id UUID, -- reference to the related entity
|
|
title VARCHAR(255),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
conversation_id UUID REFERENCES chat_conversations(id) ON DELETE CASCADE,
|
|
role VARCHAR(50) NOT NULL, -- 'user' or 'assistant'
|
|
content TEXT NOT NULL,
|
|
-- Optional: metadata about what the AI did (tools used, etc.)
|
|
metadata JSONB DEFAULT '{}',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX IF NOT EXISTS idx_skills_candidate ON skills(candidate_id);
|
|
CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(skill_name);
|
|
CREATE INDEX IF NOT EXISTS idx_experience_candidate ON experience(candidate_id);
|
|
CREATE INDEX IF NOT EXISTS idx_education_candidate ON education(candidate_id);
|
|
CREATE INDEX IF NOT EXISTS idx_certifications_candidate ON certifications(candidate_id);
|
|
CREATE INDEX IF NOT EXISTS idx_generated_cvs_candidate ON generated_cvs(candidate_id);
|
|
CREATE INDEX IF NOT EXISTS idx_generated_cvs_requirement ON generated_cvs(requirement_request_id);
|
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_conversation ON chat_messages(conversation_id);
|
|
"""
|
|
|
|
|
|
@contextmanager
|
|
def get_db():
|
|
"""Get a database connection."""
|
|
conn = psycopg2.connect(settings.database_url)
|
|
try:
|
|
yield conn
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def init_db():
|
|
"""Initialize database schema."""
|
|
with get_db() as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(SCHEMA_SQL)
|
|
conn.commit()
|
|
print("Database schema initialized successfully.")
|
|
|
|
|
|
def query(sql, params=None, fetch='all'):
|
|
"""Execute a query and return results."""
|
|
with get_db() as conn:
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
cur.execute(sql, params or ())
|
|
if fetch == 'all':
|
|
return cur.fetchall()
|
|
elif fetch == 'one':
|
|
return cur.fetchone()
|
|
elif fetch == 'none':
|
|
conn.commit()
|
|
return None
|
|
else:
|
|
return cur.fetchall()
|
|
|
|
|
|
def execute(sql, params=None):
|
|
"""Execute a query that modifies data, return the row if RETURNING is used."""
|
|
with get_db() as conn:
|
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
|
cur.execute(sql, params or ())
|
|
conn.commit()
|
|
try:
|
|
return cur.fetchone()
|
|
except psycopg2.ProgrammingError:
|
|
return None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
init_db() |