-- Loyal Companion Database Schema -- Run with: psql -U postgres -d loyal_companion -f schema.sql -- Users table CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, discord_id BIGINT NOT NULL UNIQUE, discord_username VARCHAR(255) NOT NULL, discord_display_name VARCHAR(255), custom_name VARCHAR(255), first_seen_at TIMESTAMPTZ DEFAULT NOW(), last_seen_at TIMESTAMPTZ DEFAULT NOW(), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_users_discord_id ON users(discord_id); CREATE INDEX IF NOT EXISTS ix_users_last_seen_at ON users(last_seen_at); -- User preferences table CREATE TABLE IF NOT EXISTS user_preferences ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, preference_key VARCHAR(100) NOT NULL, preference_value TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, preference_key) ); CREATE INDEX IF NOT EXISTS ix_user_preferences_user_id ON user_preferences(user_id); -- User facts table CREATE TABLE IF NOT EXISTS user_facts ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, fact_type VARCHAR(50) NOT NULL, fact_content TEXT NOT NULL, confidence FLOAT DEFAULT 1.0, source VARCHAR(50) DEFAULT 'conversation', is_active BOOLEAN NOT NULL DEFAULT TRUE, learned_at TIMESTAMPTZ DEFAULT NOW(), last_referenced_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_user_facts_user_id ON user_facts(user_id); CREATE INDEX IF NOT EXISTS ix_user_facts_fact_type ON user_facts(fact_type); CREATE INDEX IF NOT EXISTS ix_user_facts_is_active ON user_facts(is_active); -- Guilds table CREATE TABLE IF NOT EXISTS guilds ( id BIGSERIAL PRIMARY KEY, discord_id BIGINT NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, joined_at TIMESTAMPTZ DEFAULT NOW(), is_active BOOLEAN NOT NULL DEFAULT TRUE, settings JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_guilds_discord_id ON guilds(discord_id); -- Guild members table CREATE TABLE IF NOT EXISTS guild_members ( id BIGSERIAL PRIMARY KEY, guild_id BIGINT NOT NULL REFERENCES guilds(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, guild_nickname VARCHAR(255), roles TEXT[], joined_guild_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(guild_id, user_id) ); CREATE INDEX IF NOT EXISTS ix_guild_members_guild_id ON guild_members(guild_id); CREATE INDEX IF NOT EXISTS ix_guild_members_user_id ON guild_members(user_id); -- Conversations table CREATE TABLE IF NOT EXISTS conversations ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, guild_id BIGINT, channel_id BIGINT, started_at TIMESTAMPTZ DEFAULT NOW(), last_message_at TIMESTAMPTZ DEFAULT NOW(), message_count INTEGER DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_conversations_user_id ON conversations(user_id); CREATE INDEX IF NOT EXISTS ix_conversations_channel_id ON conversations(channel_id); CREATE INDEX IF NOT EXISTS ix_conversations_last_message_at ON conversations(last_message_at); CREATE INDEX IF NOT EXISTS ix_conversations_is_active ON conversations(is_active); -- Messages table CREATE TABLE IF NOT EXISTS messages ( id BIGSERIAL PRIMARY KEY, conversation_id BIGINT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, discord_message_id BIGINT, role VARCHAR(20) NOT NULL, content TEXT NOT NULL, has_images BOOLEAN NOT NULL DEFAULT FALSE, image_urls JSONB, token_count INTEGER, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_messages_conversation_id ON messages(conversation_id); CREATE INDEX IF NOT EXISTS ix_messages_user_id ON messages(user_id); CREATE INDEX IF NOT EXISTS ix_messages_created_at ON messages(created_at); -- ===================================================== -- LIVING AI TABLES -- ===================================================== -- Bot state table (mood, statistics, preferences per guild) CREATE TABLE IF NOT EXISTS bot_states ( id BIGSERIAL PRIMARY KEY, guild_id BIGINT UNIQUE, -- NULL = global state mood_valence FLOAT DEFAULT 0.0, -- -1.0 (sad) to 1.0 (happy) mood_arousal FLOAT DEFAULT 0.0, -- -1.0 (calm) to 1.0 (excited) mood_updated_at TIMESTAMPTZ DEFAULT NOW(), total_messages_sent INTEGER DEFAULT 0, total_facts_learned INTEGER DEFAULT 0, total_users_known INTEGER DEFAULT 0, first_activated_at TIMESTAMPTZ DEFAULT NOW(), preferences JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_bot_states_guild_id ON bot_states(guild_id); -- Bot opinions table (topic preferences) CREATE TABLE IF NOT EXISTS bot_opinions ( id BIGSERIAL PRIMARY KEY, guild_id BIGINT, -- NULL = global opinion topic VARCHAR(255) NOT NULL, sentiment FLOAT DEFAULT 0.0, -- -1.0 to 1.0 interest_level FLOAT DEFAULT 0.5, -- 0.0 to 1.0 discussion_count INTEGER DEFAULT 0, reasoning TEXT, formed_at TIMESTAMPTZ DEFAULT NOW(), last_reinforced_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(guild_id, topic) ); CREATE INDEX IF NOT EXISTS ix_bot_opinions_guild_id ON bot_opinions(guild_id); CREATE INDEX IF NOT EXISTS ix_bot_opinions_topic ON bot_opinions(topic); -- User relationships table (relationship depth tracking) CREATE TABLE IF NOT EXISTS user_relationships ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, guild_id BIGINT, -- NULL = global relationship relationship_score FLOAT DEFAULT 10.0, -- 0-100 scale total_interactions INTEGER DEFAULT 0, positive_interactions INTEGER DEFAULT 0, negative_interactions INTEGER DEFAULT 0, avg_message_length FLOAT DEFAULT 0.0, conversation_depth_avg FLOAT DEFAULT 0.0, shared_references JSONB DEFAULT '{}', first_interaction_at TIMESTAMPTZ DEFAULT NOW(), last_interaction_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, guild_id) ); CREATE INDEX IF NOT EXISTS ix_user_relationships_user_id ON user_relationships(user_id); CREATE INDEX IF NOT EXISTS ix_user_relationships_guild_id ON user_relationships(guild_id); -- User communication styles table (learned preferences) CREATE TABLE IF NOT EXISTS user_communication_styles ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, preferred_length VARCHAR(20) DEFAULT 'medium', -- short/medium/long preferred_formality FLOAT DEFAULT 0.5, -- 0=casual, 1=formal emoji_affinity FLOAT DEFAULT 0.5, -- 0=none, 1=lots humor_affinity FLOAT DEFAULT 0.5, -- 0=serious, 1=playful detail_preference FLOAT DEFAULT 0.5, -- 0=concise, 1=detailed engagement_signals JSONB DEFAULT '{}', samples_collected INTEGER DEFAULT 0, confidence FLOAT DEFAULT 0.0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_user_communication_styles_user_id ON user_communication_styles(user_id); -- Scheduled events table (proactive behavior) CREATE TABLE IF NOT EXISTS scheduled_events ( id BIGSERIAL PRIMARY KEY, user_id BIGINT REFERENCES users(id) ON DELETE CASCADE, guild_id BIGINT, channel_id BIGINT, event_type VARCHAR(50) NOT NULL, -- birthday, follow_up, reminder, etc. trigger_at TIMESTAMPTZ NOT NULL, title VARCHAR(255) NOT NULL, context JSONB DEFAULT '{}', is_recurring BOOLEAN DEFAULT FALSE, recurrence_rule VARCHAR(100), -- yearly, monthly, etc. status VARCHAR(20) DEFAULT 'pending', -- pending, triggered, cancelled triggered_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_scheduled_events_user_id ON scheduled_events(user_id); CREATE INDEX IF NOT EXISTS ix_scheduled_events_trigger_at ON scheduled_events(trigger_at); CREATE INDEX IF NOT EXISTS ix_scheduled_events_status ON scheduled_events(status); -- Fact associations table (cross-user memory links) CREATE TABLE IF NOT EXISTS fact_associations ( id BIGSERIAL PRIMARY KEY, fact_id_1 BIGINT NOT NULL REFERENCES user_facts(id) ON DELETE CASCADE, fact_id_2 BIGINT NOT NULL REFERENCES user_facts(id) ON DELETE CASCADE, association_type VARCHAR(50) NOT NULL, -- shared_interest, same_location, etc. strength FLOAT DEFAULT 0.5, discovered_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(fact_id_1, fact_id_2) ); CREATE INDEX IF NOT EXISTS ix_fact_associations_fact_id_1 ON fact_associations(fact_id_1); CREATE INDEX IF NOT EXISTS ix_fact_associations_fact_id_2 ON fact_associations(fact_id_2); -- Mood history table (track mood changes over time) CREATE TABLE IF NOT EXISTS mood_history ( id BIGSERIAL PRIMARY KEY, guild_id BIGINT, valence FLOAT NOT NULL, arousal FLOAT NOT NULL, trigger_type VARCHAR(50) NOT NULL, -- conversation, time_decay, event trigger_user_id BIGINT REFERENCES users(id) ON DELETE SET NULL, trigger_description TEXT, recorded_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_mood_history_guild_id ON mood_history(guild_id); CREATE INDEX IF NOT EXISTS ix_mood_history_recorded_at ON mood_history(recorded_at); -- Add new columns to user_facts for enhanced memory ALTER TABLE user_facts ADD COLUMN IF NOT EXISTS category VARCHAR(50); ALTER TABLE user_facts ADD COLUMN IF NOT EXISTS importance FLOAT DEFAULT 0.5; ALTER TABLE user_facts ADD COLUMN IF NOT EXISTS temporal_relevance VARCHAR(20); ALTER TABLE user_facts ADD COLUMN IF NOT EXISTS expiry_date TIMESTAMPTZ; ALTER TABLE user_facts ADD COLUMN IF NOT EXISTS extracted_from_message_id BIGINT; ALTER TABLE user_facts ADD COLUMN IF NOT EXISTS extraction_context TEXT;