Files
loyal_companion/schema.sql
2026-01-14 18:35:57 +01:00

316 lines
14 KiB
SQL

-- 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;
-- =====================================================
-- ATTACHMENT TRACKING TABLES
-- =====================================================
-- User attachment profiles (tracks attachment patterns per user)
CREATE TABLE IF NOT EXISTS user_attachment_profiles (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
guild_id BIGINT, -- NULL = global profile
primary_style VARCHAR(20) DEFAULT 'unknown', -- secure, anxious, avoidant, disorganized, unknown
style_confidence FLOAT DEFAULT 0.0, -- 0.0 to 1.0
current_state VARCHAR(20) DEFAULT 'regulated', -- regulated, activated, mixed
state_intensity FLOAT DEFAULT 0.0, -- 0.0 to 1.0
anxious_indicators INTEGER DEFAULT 0, -- running count of anxious pattern matches
avoidant_indicators INTEGER DEFAULT 0, -- running count of avoidant pattern matches
secure_indicators INTEGER DEFAULT 0, -- running count of secure pattern matches
disorganized_indicators INTEGER DEFAULT 0, -- running count of disorganized pattern matches
last_activation_at TIMESTAMPTZ, -- when attachment system was last activated
activation_count INTEGER DEFAULT 0, -- total activations
activation_triggers JSONB DEFAULT '[]', -- learned triggers that activate attachment
effective_responses JSONB DEFAULT '[]', -- response styles that helped regulate
ineffective_responses JSONB DEFAULT '[]', -- response styles that didn't help
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, guild_id)
);
CREATE INDEX IF NOT EXISTS ix_user_attachment_profiles_user_id ON user_attachment_profiles(user_id);
CREATE INDEX IF NOT EXISTS ix_user_attachment_profiles_guild_id ON user_attachment_profiles(guild_id);
CREATE INDEX IF NOT EXISTS ix_user_attachment_profiles_primary_style ON user_attachment_profiles(primary_style);
-- Attachment events (logs attachment-related events for learning)
CREATE TABLE IF NOT EXISTS attachment_events (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
guild_id BIGINT,
event_type VARCHAR(50) NOT NULL, -- activation, regulation, escalation, etc.
detected_style VARCHAR(20), -- anxious, avoidant, disorganized, mixed
intensity FLOAT DEFAULT 0.0, -- 0.0 to 1.0
trigger_message TEXT, -- the message that triggered the event (truncated)
trigger_indicators JSONB DEFAULT '[]', -- patterns that matched
response_style VARCHAR(50), -- how Bartender responded
outcome VARCHAR(20), -- helpful, neutral, unhelpful (set after follow-up)
notes TEXT, -- any additional context
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS ix_attachment_events_user_id ON attachment_events(user_id);
CREATE INDEX IF NOT EXISTS ix_attachment_events_guild_id ON attachment_events(guild_id);
CREATE INDEX IF NOT EXISTS ix_attachment_events_event_type ON attachment_events(event_type);
CREATE INDEX IF NOT EXISTS ix_attachment_events_created_at ON attachment_events(created_at);