Files
cv-app/database.py

292 lines
11 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);
-- ============================================================
-- CV BATCHES — group candidates for a specific requirement
-- ============================================================
-- Main batch table (one per requirement document or manual creation)
CREATE TABLE IF NOT EXISTS cv_batches (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
description TEXT,
-- Raw text of the uploaded requirements document (backend-only, AI use)
-- Null for manually created batches
requirements_text TEXT,
-- JSON array of positions extracted from the document
-- [{job_title, num_positions, required_skills, required_years, required_certs, description}]
positions JSONB DEFAULT '[]',
-- Selected Carbone template ID for CV generation
template_id TEXT,
-- draft = still working, active = matching done, exported = CVs generated
status VARCHAR(50) DEFAULT 'draft',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Batch items (candidates linked to a batch)
CREATE TABLE IF NOT EXISTS batch_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
batch_id UUID REFERENCES cv_batches(id) ON DELETE CASCADE,
candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE,
-- Which position in the batch this candidate is matched to (free text)
position_title VARCHAR(255),
-- AI match score 0-100
match_score INTEGER DEFAULT 0,
-- AI reasoning for the match
match_reasoning TEXT,
-- Realigned CV data (JSON — the reworded CV for this specific post)
realigned_cv_data JSONB,
-- proposed = AI suggested, approved = user confirmed, removed = user rejected
status VARCHAR(50) DEFAULT 'proposed',
-- Path to generated PDF after generation
generated_cv_path TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Batch chat (messages per batch's discussion)
CREATE TABLE IF NOT EXISTS batch_chat (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
batch_id UUID REFERENCES cv_batches(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL, -- 'user' or 'assistant'
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_batch_items_batch ON batch_items(batch_id);
CREATE INDEX IF NOT EXISTS idx_batch_items_candidate ON batch_items(candidate_id);
CREATE INDEX IF NOT EXISTS idx_batch_chat_batch ON batch_chat(batch_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()