feat: Add Alateya, Clan, Eonarch agents + fix gateway-router connection
## Agents Added - Alateya: R&D, biotech, innovations - Clan (Spirit): Community spirit agent - Eonarch: Consciousness evolution agent ## Changes - docker-compose.node1.yml: Added tokens for all 3 new agents - gateway-bot/http_api.py: Added configs and webhook endpoints - gateway-bot/clan_prompt.txt: New prompt file - gateway-bot/eonarch_prompt.txt: New prompt file ## Fixes - Fixed ROUTER_URL from :9102 to :8000 (internal container port) - All 9 Telegram agents now working ## Documentation - Created PROJECT-MASTER-INDEX.md - single entry point - Added various status documents and scripts Tokens configured: - Helion, NUTRA, Agromatrix (existing) - Alateya, Clan, Eonarch (new) - Druid, GreenFood, DAARWIZZ (configured)
This commit is contained in:
407
migrations/049_memory_v3_human_memory_model.sql
Normal file
407
migrations/049_memory_v3_human_memory_model.sql
Normal file
@@ -0,0 +1,407 @@
|
||||
-- Migration 049: Human Memory Model v3.0 for Helion
|
||||
-- Full identity, roles, session state, and organizational memory schema
|
||||
-- Created: 2026-01-17
|
||||
|
||||
-- ============================================================================
|
||||
-- L2: PLATFORM IDENTITY & ROLES (PIR)
|
||||
-- ============================================================================
|
||||
|
||||
-- Global platform users (cross-channel identity)
|
||||
CREATE TABLE IF NOT EXISTS platform_users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
status TEXT DEFAULT 'active', -- active, inactive, banned, deleted
|
||||
-- Aggregated profile (derived from channel_identities)
|
||||
preferred_language TEXT DEFAULT 'uk',
|
||||
timezone TEXT,
|
||||
metadata JSONB DEFAULT '{}'
|
||||
);
|
||||
|
||||
-- Channel-specific identities (Telegram, Discord, Email, etc.)
|
||||
CREATE TABLE IF NOT EXISTS channel_identities (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
platform_user_id UUID NOT NULL REFERENCES platform_users(id) ON DELETE CASCADE,
|
||||
channel TEXT NOT NULL, -- telegram, discord, email, web
|
||||
channel_user_id TEXT NOT NULL, -- Telegram from.id, Discord user_id, etc.
|
||||
username TEXT, -- @username
|
||||
display_name TEXT, -- first_name + last_name
|
||||
avatar_url TEXT,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
first_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
metadata JSONB DEFAULT '{}', -- channel-specific data
|
||||
UNIQUE(channel, channel_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_identities_platform_user ON channel_identities(platform_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_identities_channel_user ON channel_identities(channel, channel_user_id);
|
||||
|
||||
-- Platform roles (mentor, dev, investor, ops, moderator)
|
||||
CREATE TABLE IF NOT EXISTS platform_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code TEXT NOT NULL UNIQUE, -- mentor, developer, investor, ops, moderator, admin
|
||||
display_name TEXT NOT NULL,
|
||||
scope TEXT NOT NULL DEFAULT 'platform', -- platform, group, thread
|
||||
description TEXT,
|
||||
permissions JSONB DEFAULT '[]', -- list of permission codes
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- User-role assignments
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
platform_user_id UUID NOT NULL REFERENCES platform_users(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES platform_roles(id) ON DELETE CASCADE,
|
||||
scope_ref TEXT, -- group_id for group-scoped roles, null for platform-wide
|
||||
confidence REAL DEFAULT 1.0, -- 0.0-1.0, lower for inferred roles
|
||||
assigned_by TEXT, -- 'system', 'admin', 'helion_inferred', user_id
|
||||
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
revoked_at TIMESTAMP WITH TIME ZONE, -- null if active
|
||||
notes TEXT,
|
||||
UNIQUE(platform_user_id, role_id, scope_ref)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_platform_user ON user_roles(platform_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_active ON user_roles(platform_user_id) WHERE revoked_at IS NULL;
|
||||
|
||||
-- ============================================================================
|
||||
-- L1: SESSION STATE MEMORY (SSM)
|
||||
-- ============================================================================
|
||||
|
||||
-- Conversations (chat sessions)
|
||||
CREATE TABLE IF NOT EXISTS helion_conversations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel TEXT NOT NULL, -- telegram, discord, web
|
||||
chat_id TEXT NOT NULL, -- Telegram chat_id, Discord channel_id
|
||||
thread_id TEXT, -- Optional thread within chat
|
||||
platform_user_id UUID REFERENCES platform_users(id), -- Primary user (for DMs)
|
||||
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
last_activity_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
status TEXT DEFAULT 'active', -- active, archived, deleted
|
||||
metadata JSONB DEFAULT '{}',
|
||||
UNIQUE(channel, chat_id, thread_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_chat ON helion_conversations(channel, chat_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_user ON helion_conversations(platform_user_id);
|
||||
|
||||
-- Session state (SSM) - per conversation
|
||||
CREATE TABLE IF NOT EXISTS helion_conversation_state (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
conversation_id UUID NOT NULL REFERENCES helion_conversations(id) ON DELETE CASCADE,
|
||||
|
||||
-- Addressing state
|
||||
last_addressed_to_helion BOOLEAN DEFAULT FALSE,
|
||||
last_user_id TEXT,
|
||||
last_user_nick TEXT,
|
||||
|
||||
-- Topic/context tracking
|
||||
active_topic_id TEXT,
|
||||
active_context_open BOOLEAN DEFAULT FALSE,
|
||||
closed_context_ids TEXT[] DEFAULT '{}',
|
||||
|
||||
-- Media handling
|
||||
last_media_id TEXT,
|
||||
last_media_type TEXT,
|
||||
last_media_handled BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Anti-repeat mechanism
|
||||
last_answer_fingerprint TEXT,
|
||||
last_answer_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Group settings
|
||||
group_trust_mode BOOLEAN DEFAULT FALSE,
|
||||
apprentice_mode BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Proactive question limits
|
||||
proactive_questions_today INTEGER DEFAULT 0,
|
||||
last_proactive_question_at TIMESTAMP WITH TIME ZONE,
|
||||
proactive_question_reset_date DATE DEFAULT CURRENT_DATE,
|
||||
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
state_json JSONB DEFAULT '{}', -- Additional flexible state
|
||||
|
||||
UNIQUE(conversation_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversation_state_conv ON helion_conversation_state(conversation_id);
|
||||
|
||||
-- Media index (for tracking processed media)
|
||||
CREATE TABLE IF NOT EXISTS helion_media_index (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
conversation_id UUID NOT NULL REFERENCES helion_conversations(id) ON DELETE CASCADE,
|
||||
message_id TEXT NOT NULL,
|
||||
media_type TEXT NOT NULL, -- photo, voice, video, document, video_note
|
||||
file_id TEXT NOT NULL,
|
||||
file_hash TEXT, -- For deduplication
|
||||
handled BOOLEAN DEFAULT FALSE,
|
||||
handled_at TIMESTAMP WITH TIME ZONE,
|
||||
result_summary TEXT, -- Brief summary of what was extracted
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(conversation_id, file_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_index_conv ON helion_media_index(conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_media_index_file ON helion_media_index(file_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- L3: ORGANIZATIONAL MEMORY (OM)
|
||||
-- ============================================================================
|
||||
|
||||
-- Extended memory items (long-term facts)
|
||||
CREATE TABLE IF NOT EXISTS helion_memory_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
platform_user_id UUID REFERENCES platform_users(id) ON DELETE SET NULL,
|
||||
|
||||
-- Memory classification
|
||||
type TEXT NOT NULL, -- preference, decision, agreement, profile_fact, mentor_lesson, project_fact
|
||||
category TEXT, -- personal, technical, organizational, stylistic
|
||||
|
||||
-- Content
|
||||
text TEXT NOT NULL,
|
||||
summary TEXT, -- Short summary for retrieval brief
|
||||
|
||||
-- Source tracking
|
||||
source_ref TEXT, -- conversation_id, message_id, or external source
|
||||
source_type TEXT, -- conversation, manual, import, inferred
|
||||
|
||||
-- Confidence and verification
|
||||
confidence REAL DEFAULT 0.7, -- 0.0-1.0
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
verified_by TEXT,
|
||||
verified_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Visibility and scope
|
||||
visibility TEXT DEFAULT 'platform', -- private_dm, group_only, platform
|
||||
scope_ref TEXT, -- group_id if group_only
|
||||
|
||||
-- Lifecycle
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
expires_at TIMESTAMP WITH TIME ZONE, -- null = never expires
|
||||
archived_at TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- Vector reference
|
||||
embedding_id TEXT, -- Qdrant point ID
|
||||
|
||||
metadata JSONB DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_items_user ON helion_memory_items(platform_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_items_type ON helion_memory_items(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_items_visibility ON helion_memory_items(visibility);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_items_active ON helion_memory_items(platform_user_id)
|
||||
WHERE archived_at IS NULL AND (expires_at IS NULL OR expires_at > NOW());
|
||||
|
||||
-- Memory events (audit log)
|
||||
CREATE TABLE IF NOT EXISTS helion_memory_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
platform_user_id UUID REFERENCES platform_users(id),
|
||||
memory_item_id UUID REFERENCES helion_memory_items(id) ON DELETE SET NULL,
|
||||
event_type TEXT NOT NULL, -- add, update, verify, revoke, retrieve, archive
|
||||
actor TEXT NOT NULL, -- user, helion, admin, system
|
||||
actor_ref TEXT, -- user_id or service name
|
||||
payload_json JSONB DEFAULT '{}',
|
||||
ts TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_events_user ON helion_memory_events(platform_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_events_item ON helion_memory_events(memory_item_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_events_ts ON helion_memory_events(ts);
|
||||
|
||||
-- ============================================================================
|
||||
-- MENTORS & TRUSTED GROUPS (Updated from v2.3)
|
||||
-- ============================================================================
|
||||
|
||||
-- Mentors table (platform-wide or group-specific)
|
||||
CREATE TABLE IF NOT EXISTS helion_mentors (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
platform_user_id UUID REFERENCES platform_users(id),
|
||||
scope TEXT DEFAULT 'platform', -- platform, group
|
||||
scope_ref TEXT, -- group chat_id if group-scoped
|
||||
|
||||
-- Telegram info (for matching)
|
||||
telegram_user_id TEXT,
|
||||
telegram_username TEXT,
|
||||
display_name TEXT,
|
||||
phone_hash TEXT, -- Hashed phone for matching (privacy)
|
||||
|
||||
-- Status
|
||||
confidence TEXT DEFAULT 'configured', -- configured, confirmed, inferred
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
UNIQUE(platform_user_id, scope, scope_ref)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mentors_telegram ON helion_mentors(telegram_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mentors_username ON helion_mentors(telegram_username);
|
||||
|
||||
-- Trusted groups/chats
|
||||
CREATE TABLE IF NOT EXISTS helion_trusted_groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel TEXT NOT NULL DEFAULT 'telegram',
|
||||
chat_id TEXT NOT NULL,
|
||||
chat_username TEXT, -- @energyunionofficial
|
||||
chat_title TEXT,
|
||||
|
||||
-- Settings
|
||||
trust_mode BOOLEAN DEFAULT TRUE,
|
||||
apprentice_mode BOOLEAN DEFAULT TRUE,
|
||||
auto_respond BOOLEAN DEFAULT FALSE, -- Respond even without mention
|
||||
|
||||
-- Limits
|
||||
proactive_questions_per_day INTEGER DEFAULT 3,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
UNIQUE(channel, chat_id)
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- INITIAL DATA
|
||||
-- ============================================================================
|
||||
|
||||
-- Insert default roles
|
||||
INSERT INTO platform_roles (code, display_name, scope, description) VALUES
|
||||
('admin', 'Administrator', 'platform', 'Full platform access'),
|
||||
('mentor', 'Mentor', 'platform', 'Can teach Helion, elevated trust'),
|
||||
('developer', 'Developer', 'platform', 'Technical team member'),
|
||||
('investor', 'Investor', 'platform', 'Token holder or investor'),
|
||||
('ops', 'Operations', 'platform', 'Operations team member'),
|
||||
('moderator', 'Moderator', 'group', 'Group moderator'),
|
||||
('member', 'Member', 'platform', 'Regular platform member')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- Insert known mentors
|
||||
INSERT INTO helion_mentors (telegram_username, display_name, confidence) VALUES
|
||||
('@ivantytar', 'Іван Титар', 'configured'),
|
||||
('@archenvis', 'Александр Вертій', 'configured'),
|
||||
('@olegarch88', 'Олег Ковальчук', 'configured')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Insert trusted groups
|
||||
INSERT INTO helion_trusted_groups (channel, chat_id, chat_username, chat_title, trust_mode, apprentice_mode) VALUES
|
||||
('telegram', '-1001234567890', '@energyunionofficial', 'Energy Union Official', TRUE, TRUE),
|
||||
('telegram', '-1009876543210', '@energyunionteam', 'Energy Union Team', TRUE, TRUE)
|
||||
ON CONFLICT (channel, chat_id) DO UPDATE SET
|
||||
chat_username = EXCLUDED.chat_username,
|
||||
chat_title = EXCLUDED.chat_title;
|
||||
|
||||
-- ============================================================================
|
||||
-- HELPER FUNCTIONS
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to resolve or create platform user from channel identity
|
||||
CREATE OR REPLACE FUNCTION resolve_platform_user(
|
||||
p_channel TEXT,
|
||||
p_channel_user_id TEXT,
|
||||
p_username TEXT DEFAULT NULL,
|
||||
p_display_name TEXT DEFAULT NULL
|
||||
) RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_platform_user_id UUID;
|
||||
v_identity_id UUID;
|
||||
BEGIN
|
||||
-- Check if identity exists
|
||||
SELECT platform_user_id INTO v_platform_user_id
|
||||
FROM channel_identities
|
||||
WHERE channel = p_channel AND channel_user_id = p_channel_user_id;
|
||||
|
||||
IF v_platform_user_id IS NULL THEN
|
||||
-- Create new platform user
|
||||
INSERT INTO platform_users (status) VALUES ('active')
|
||||
RETURNING id INTO v_platform_user_id;
|
||||
|
||||
-- Create channel identity
|
||||
INSERT INTO channel_identities (platform_user_id, channel, channel_user_id, username, display_name)
|
||||
VALUES (v_platform_user_id, p_channel, p_channel_user_id, p_username, p_display_name);
|
||||
ELSE
|
||||
-- Update last seen and username if changed
|
||||
UPDATE channel_identities
|
||||
SET last_seen_at = NOW(),
|
||||
username = COALESCE(p_username, username),
|
||||
display_name = COALESCE(p_display_name, display_name)
|
||||
WHERE channel = p_channel AND channel_user_id = p_channel_user_id;
|
||||
END IF;
|
||||
|
||||
RETURN v_platform_user_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to get or create conversation
|
||||
CREATE OR REPLACE FUNCTION get_or_create_conversation(
|
||||
p_channel TEXT,
|
||||
p_chat_id TEXT,
|
||||
p_thread_id TEXT DEFAULT NULL,
|
||||
p_platform_user_id UUID DEFAULT NULL
|
||||
) RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_conversation_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO v_conversation_id
|
||||
FROM helion_conversations
|
||||
WHERE channel = p_channel
|
||||
AND chat_id = p_chat_id
|
||||
AND COALESCE(thread_id, '') = COALESCE(p_thread_id, '');
|
||||
|
||||
IF v_conversation_id IS NULL THEN
|
||||
INSERT INTO helion_conversations (channel, chat_id, thread_id, platform_user_id)
|
||||
VALUES (p_channel, p_chat_id, p_thread_id, p_platform_user_id)
|
||||
RETURNING id INTO v_conversation_id;
|
||||
ELSE
|
||||
UPDATE helion_conversations
|
||||
SET last_activity_at = NOW()
|
||||
WHERE id = v_conversation_id;
|
||||
END IF;
|
||||
|
||||
RETURN v_conversation_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to check if user is mentor
|
||||
CREATE OR REPLACE FUNCTION is_mentor(
|
||||
p_telegram_user_id TEXT DEFAULT NULL,
|
||||
p_telegram_username TEXT DEFAULT NULL
|
||||
) RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM helion_mentors
|
||||
WHERE active = TRUE
|
||||
AND (telegram_user_id = p_telegram_user_id OR telegram_username = p_telegram_username)
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to check if chat is trusted
|
||||
CREATE OR REPLACE FUNCTION is_trusted_group(
|
||||
p_channel TEXT,
|
||||
p_chat_id TEXT
|
||||
) RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM helion_trusted_groups
|
||||
WHERE channel = p_channel AND chat_id = p_chat_id AND trust_mode = TRUE
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================================================
|
||||
-- COMMENTS
|
||||
-- ============================================================================
|
||||
|
||||
COMMENT ON TABLE platform_users IS 'Global user identity across all channels (L2 PIR)';
|
||||
COMMENT ON TABLE channel_identities IS 'Channel-specific user identities linked to platform users';
|
||||
COMMENT ON TABLE platform_roles IS 'Available roles in the platform';
|
||||
COMMENT ON TABLE user_roles IS 'User role assignments with scope and confidence';
|
||||
COMMENT ON TABLE helion_conversations IS 'Chat sessions/conversations';
|
||||
COMMENT ON TABLE helion_conversation_state IS 'Session state memory (L1 SSM) per conversation';
|
||||
COMMENT ON TABLE helion_media_index IS 'Index of processed media to avoid re-processing';
|
||||
COMMENT ON TABLE helion_memory_items IS 'Long-term memory facts (L3 OM)';
|
||||
COMMENT ON TABLE helion_memory_events IS 'Audit log of memory operations';
|
||||
COMMENT ON TABLE helion_mentors IS 'Configured and confirmed mentors';
|
||||
COMMENT ON TABLE helion_trusted_groups IS 'Trusted groups with special settings';
|
||||
286
migrations/050_neo4j_graph_schema.cypher
Normal file
286
migrations/050_neo4j_graph_schema.cypher
Normal file
@@ -0,0 +1,286 @@
|
||||
// Neo4j Graph Schema for Helion Memory v3.0
|
||||
// Run this in Neo4j Browser or via Cypher shell
|
||||
// Created: 2026-01-17
|
||||
|
||||
// ============================================================================
|
||||
// CONSTRAINTS (Uniqueness)
|
||||
// ============================================================================
|
||||
|
||||
// User node uniqueness
|
||||
CREATE CONSTRAINT user_platform_id IF NOT EXISTS
|
||||
FOR (u:User) REQUIRE u.platform_user_id IS UNIQUE;
|
||||
|
||||
// Group node uniqueness
|
||||
CREATE CONSTRAINT group_chat_id IF NOT EXISTS
|
||||
FOR (g:Group) REQUIRE g.chat_id IS UNIQUE;
|
||||
|
||||
// Role node uniqueness
|
||||
CREATE CONSTRAINT role_code IF NOT EXISTS
|
||||
FOR (r:Role) REQUIRE r.code IS UNIQUE;
|
||||
|
||||
// Topic node uniqueness
|
||||
CREATE CONSTRAINT topic_id IF NOT EXISTS
|
||||
FOR (t:Topic) REQUIRE t.topic_id IS UNIQUE;
|
||||
|
||||
// Project node uniqueness
|
||||
CREATE CONSTRAINT project_id IF NOT EXISTS
|
||||
FOR (p:Project) REQUIRE p.project_id IS UNIQUE;
|
||||
|
||||
// Artifact node uniqueness
|
||||
CREATE CONSTRAINT artifact_id IF NOT EXISTS
|
||||
FOR (a:Artifact) REQUIRE a.artifact_id IS UNIQUE;
|
||||
|
||||
// Decision node uniqueness
|
||||
CREATE CONSTRAINT decision_id IF NOT EXISTS
|
||||
FOR (d:Decision) REQUIRE d.decision_id IS UNIQUE;
|
||||
|
||||
// ============================================================================
|
||||
// INDEXES (Performance)
|
||||
// ============================================================================
|
||||
|
||||
// User indexes
|
||||
CREATE INDEX user_telegram_id IF NOT EXISTS FOR (u:User) ON (u.telegram_user_id);
|
||||
CREATE INDEX user_username IF NOT EXISTS FOR (u:User) ON (u.username);
|
||||
CREATE INDEX user_status IF NOT EXISTS FOR (u:User) ON (u.status);
|
||||
|
||||
// Group indexes
|
||||
CREATE INDEX group_channel IF NOT EXISTS FOR (g:Group) ON (g.channel);
|
||||
CREATE INDEX group_trust IF NOT EXISTS FOR (g:Group) ON (g.trust_mode);
|
||||
|
||||
// Topic indexes
|
||||
CREATE INDEX topic_name IF NOT EXISTS FOR (t:Topic) ON (t.name);
|
||||
CREATE INDEX topic_category IF NOT EXISTS FOR (t:Topic) ON (t.category);
|
||||
|
||||
// Project indexes
|
||||
CREATE INDEX project_name IF NOT EXISTS FOR (p:Project) ON (p.name);
|
||||
CREATE INDEX project_status IF NOT EXISTS FOR (p:Project) ON (p.status);
|
||||
|
||||
// ============================================================================
|
||||
// INITIAL NODES: Roles
|
||||
// ============================================================================
|
||||
|
||||
MERGE (r:Role {code: 'admin'})
|
||||
SET r.display_name = 'Administrator',
|
||||
r.scope = 'platform',
|
||||
r.description = 'Full platform access',
|
||||
r.created_at = datetime();
|
||||
|
||||
MERGE (r:Role {code: 'mentor'})
|
||||
SET r.display_name = 'Mentor',
|
||||
r.scope = 'platform',
|
||||
r.description = 'Can teach Helion, elevated trust',
|
||||
r.created_at = datetime();
|
||||
|
||||
MERGE (r:Role {code: 'developer'})
|
||||
SET r.display_name = 'Developer',
|
||||
r.scope = 'platform',
|
||||
r.description = 'Technical team member',
|
||||
r.created_at = datetime();
|
||||
|
||||
MERGE (r:Role {code: 'investor'})
|
||||
SET r.display_name = 'Investor',
|
||||
r.scope = 'platform',
|
||||
r.description = 'Token holder or investor',
|
||||
r.created_at = datetime();
|
||||
|
||||
MERGE (r:Role {code: 'ops'})
|
||||
SET r.display_name = 'Operations',
|
||||
r.scope = 'platform',
|
||||
r.description = 'Operations team member',
|
||||
r.created_at = datetime();
|
||||
|
||||
MERGE (r:Role {code: 'moderator'})
|
||||
SET r.display_name = 'Moderator',
|
||||
r.scope = 'group',
|
||||
r.description = 'Group moderator',
|
||||
r.created_at = datetime();
|
||||
|
||||
MERGE (r:Role {code: 'member'})
|
||||
SET r.display_name = 'Member',
|
||||
r.scope = 'platform',
|
||||
r.description = 'Regular platform member',
|
||||
r.created_at = datetime();
|
||||
|
||||
// ============================================================================
|
||||
// INITIAL NODES: Energy Union Projects
|
||||
// ============================================================================
|
||||
|
||||
MERGE (p:Project {project_id: 'energy-union'})
|
||||
SET p.name = 'Energy Union',
|
||||
p.description = 'Main platform for decentralized energy solutions',
|
||||
p.status = 'active',
|
||||
p.created_at = datetime();
|
||||
|
||||
MERGE (p:Project {project_id: 'biominer'})
|
||||
SET p.name = 'BioMiner',
|
||||
p.description = 'Bioenergy mining system',
|
||||
p.status = 'active',
|
||||
p.parent_project = 'energy-union',
|
||||
p.created_at = datetime();
|
||||
|
||||
MERGE (p:Project {project_id: 'ecominer'})
|
||||
SET p.name = 'EcoMiner',
|
||||
p.description = 'Modular cogeneration system',
|
||||
p.status = 'active',
|
||||
p.parent_project = 'energy-union',
|
||||
p.created_at = datetime();
|
||||
|
||||
MERGE (p:Project {project_id: 'eu-token'})
|
||||
SET p.name = 'EU Token',
|
||||
p.description = 'Energy Union utility token',
|
||||
p.status = 'active',
|
||||
p.parent_project = 'energy-union',
|
||||
p.created_at = datetime();
|
||||
|
||||
// ============================================================================
|
||||
// INITIAL NODES: Topics
|
||||
// ============================================================================
|
||||
|
||||
MERGE (t:Topic {topic_id: 'tokenomics'})
|
||||
SET t.name = 'Tokenomics',
|
||||
t.category = 'technical',
|
||||
t.description = 'Token economics and distribution',
|
||||
t.created_at = datetime();
|
||||
|
||||
MERGE (t:Topic {topic_id: 'dao-governance'})
|
||||
SET t.name = 'DAO Governance',
|
||||
t.category = 'organizational',
|
||||
t.description = 'Decentralized governance mechanisms',
|
||||
t.created_at = datetime();
|
||||
|
||||
MERGE (t:Topic {topic_id: 'cogeneration'})
|
||||
SET t.name = 'Cogeneration',
|
||||
t.category = 'technical',
|
||||
t.description = 'Combined heat and power generation',
|
||||
t.created_at = datetime();
|
||||
|
||||
MERGE (t:Topic {topic_id: 'biogas'})
|
||||
SET t.name = 'Biogas',
|
||||
t.category = 'technical',
|
||||
t.description = 'Biogas production and utilization',
|
||||
t.created_at = datetime();
|
||||
|
||||
MERGE (t:Topic {topic_id: 'investment'})
|
||||
SET t.name = 'Investment',
|
||||
t.category = 'business',
|
||||
t.description = 'Investment opportunities and strategies',
|
||||
t.created_at = datetime();
|
||||
|
||||
MERGE (t:Topic {topic_id: 'staking'})
|
||||
SET t.name = 'Staking',
|
||||
t.category = 'technical',
|
||||
t.description = 'Token staking mechanisms',
|
||||
t.created_at = datetime();
|
||||
|
||||
// ============================================================================
|
||||
// INITIAL NODES: Known Mentors (as Users)
|
||||
// ============================================================================
|
||||
|
||||
MERGE (u:User {username: '@ivantytar'})
|
||||
SET u.display_name = 'Іван Титар',
|
||||
u.status = 'active',
|
||||
u.is_mentor = true,
|
||||
u.created_at = datetime();
|
||||
|
||||
MERGE (u:User {username: '@archenvis'})
|
||||
SET u.display_name = 'Александр Вертій',
|
||||
u.status = 'active',
|
||||
u.is_mentor = true,
|
||||
u.created_at = datetime();
|
||||
|
||||
MERGE (u:User {username: '@olegarch88'})
|
||||
SET u.display_name = 'Олег Ковальчук',
|
||||
u.status = 'active',
|
||||
u.is_mentor = true,
|
||||
u.created_at = datetime();
|
||||
|
||||
// ============================================================================
|
||||
// INITIAL NODES: Trusted Groups
|
||||
// ============================================================================
|
||||
|
||||
MERGE (g:Group {chat_id: 'energyunionofficial'})
|
||||
SET g.channel = 'telegram',
|
||||
g.username = '@energyunionofficial',
|
||||
g.title = 'Energy Union Official',
|
||||
g.trust_mode = true,
|
||||
g.apprentice_mode = true,
|
||||
g.created_at = datetime();
|
||||
|
||||
MERGE (g:Group {chat_id: 'energyunionteam'})
|
||||
SET g.channel = 'telegram',
|
||||
g.username = '@energyunionteam',
|
||||
g.title = 'Energy Union Team',
|
||||
g.trust_mode = true,
|
||||
g.apprentice_mode = true,
|
||||
g.created_at = datetime();
|
||||
|
||||
// ============================================================================
|
||||
// INITIAL RELATIONSHIPS
|
||||
// ============================================================================
|
||||
|
||||
// Mentors have MENTOR role
|
||||
MATCH (u:User {username: '@ivantytar'}), (r:Role {code: 'mentor'})
|
||||
MERGE (u)-[:HAS_ROLE {confidence: 1.0, assigned_by: 'config', assigned_at: datetime()}]->(r);
|
||||
|
||||
MATCH (u:User {username: '@archenvis'}), (r:Role {code: 'mentor'})
|
||||
MERGE (u)-[:HAS_ROLE {confidence: 1.0, assigned_by: 'config', assigned_at: datetime()}]->(r);
|
||||
|
||||
MATCH (u:User {username: '@olegarch88'}), (r:Role {code: 'mentor'})
|
||||
MERGE (u)-[:HAS_ROLE {confidence: 1.0, assigned_by: 'config', assigned_at: datetime()}]->(r);
|
||||
|
||||
// Mentors can TEACH Helion
|
||||
MATCH (u:User {is_mentor: true})
|
||||
MERGE (h:Agent {agent_id: 'helion'})
|
||||
SET h.name = 'Helion', h.status = 'active'
|
||||
MERGE (u)-[:MENTORS {scope: 'platform', since: datetime()}]->(h);
|
||||
|
||||
// Projects related to Topics
|
||||
MATCH (p:Project {project_id: 'biominer'}), (t:Topic {topic_id: 'biogas'})
|
||||
MERGE (p)-[:RELATED_TO]->(t);
|
||||
|
||||
MATCH (p:Project {project_id: 'ecominer'}), (t:Topic {topic_id: 'cogeneration'})
|
||||
MERGE (p)-[:RELATED_TO]->(t);
|
||||
|
||||
MATCH (p:Project {project_id: 'eu-token'}), (t:Topic {topic_id: 'tokenomics'})
|
||||
MERGE (p)-[:RELATED_TO]->(t);
|
||||
|
||||
MATCH (p:Project {project_id: 'eu-token'}), (t:Topic {topic_id: 'staking'})
|
||||
MERGE (p)-[:RELATED_TO]->(t);
|
||||
|
||||
MATCH (p:Project {project_id: 'energy-union'}), (t:Topic {topic_id: 'dao-governance'})
|
||||
MERGE (p)-[:RELATED_TO]->(t);
|
||||
|
||||
// Sub-projects
|
||||
MATCH (parent:Project {project_id: 'energy-union'}), (child:Project)
|
||||
WHERE child.parent_project = 'energy-union'
|
||||
MERGE (child)-[:PART_OF]->(parent);
|
||||
|
||||
// ============================================================================
|
||||
// USEFUL QUERIES (Examples)
|
||||
// ============================================================================
|
||||
|
||||
// Find all mentors:
|
||||
// MATCH (u:User)-[:HAS_ROLE]->(r:Role {code: 'mentor'}) RETURN u.display_name, u.username
|
||||
|
||||
// Find user's roles:
|
||||
// MATCH (u:User {username: $username})-[hr:HAS_ROLE]->(r:Role) RETURN r.code, hr.confidence
|
||||
|
||||
// Find who works on a project:
|
||||
// MATCH (u:User)-[:WORKS_ON]->(p:Project {name: 'EcoMiner'}) RETURN u.display_name
|
||||
|
||||
// Find related topics for a project:
|
||||
// MATCH (p:Project {name: 'BioMiner'})-[:RELATED_TO]->(t:Topic) RETURN t.name
|
||||
|
||||
// Find who asked about a topic:
|
||||
// MATCH (u:User)-[a:ASKED_ABOUT]->(t:Topic {name: 'Tokenomics'}) RETURN u.display_name, a.count, a.last_asked
|
||||
|
||||
// Find mentors who can help with a topic:
|
||||
// MATCH (u:User)-[:HAS_ROLE]->(r:Role {code: 'mentor'}), (u)-[:KNOWS_ABOUT]->(t:Topic {name: $topic})
|
||||
// RETURN u.display_name, u.username
|
||||
|
||||
// Get full user context (roles, projects, topics):
|
||||
// MATCH (u:User {username: $username})
|
||||
// OPTIONAL MATCH (u)-[hr:HAS_ROLE]->(r:Role)
|
||||
// OPTIONAL MATCH (u)-[:WORKS_ON]->(p:Project)
|
||||
// OPTIONAL MATCH (u)-[:ASKED_ABOUT]->(t:Topic)
|
||||
// RETURN u, collect(DISTINCT r.code) as roles, collect(DISTINCT p.name) as projects, collect(DISTINCT t.name) as topics
|
||||
39
migrations/051_fix_is_mentor_function.sql
Normal file
39
migrations/051_fix_is_mentor_function.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
-- Migration 051: Fix is_mentor function type mismatch
|
||||
-- Issue: bigint = text error when comparing telegram_user_id
|
||||
|
||||
DROP FUNCTION IF EXISTS is_mentor(text, text);
|
||||
|
||||
CREATE FUNCTION is_mentor(
|
||||
p_telegram_user_id TEXT,
|
||||
p_telegram_username TEXT
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_id_num BIGINT;
|
||||
BEGIN
|
||||
-- Safely convert telegram_user_id to bigint if it's numeric
|
||||
IF p_telegram_user_id IS NOT NULL AND p_telegram_user_id ~ '^[0-9]+$' THEN
|
||||
user_id_num := p_telegram_user_id::BIGINT;
|
||||
ELSE
|
||||
user_id_num := NULL;
|
||||
END IF;
|
||||
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM helion_mentors
|
||||
WHERE active = true
|
||||
AND (
|
||||
-- Match by telegram user ID (bigint)
|
||||
(user_id_num IS NOT NULL AND telegram_user_id = user_id_num)
|
||||
OR
|
||||
-- Match by username (with or without @)
|
||||
(p_telegram_username IS NOT NULL AND username = p_telegram_username)
|
||||
OR
|
||||
(p_telegram_username IS NOT NULL AND username = '@' || p_telegram_username)
|
||||
)
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Test the function
|
||||
SELECT is_mentor('1642840513', 'ivantytar') as test_by_id;
|
||||
SELECT is_mentor(NULL, 'ivantytar') as test_by_username;
|
||||
SELECT is_mentor(NULL, 'unknown_user') as test_not_mentor;
|
||||
417
migrations/052_account_linking_schema.sql
Normal file
417
migrations/052_account_linking_schema.sql
Normal file
@@ -0,0 +1,417 @@
|
||||
-- Migration 052: Account Linking Schema for Energy Union Platform
|
||||
-- Version: 2.7
|
||||
-- Date: 2026-01-18
|
||||
-- Purpose: Enable Telegram ↔ Energy Union account linking for cross-channel memory
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. ACCOUNT LINKS - Core binding between Telegram and Platform accounts
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS account_links (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Energy Union platform account
|
||||
account_id UUID NOT NULL,
|
||||
|
||||
-- Telegram identifiers
|
||||
telegram_user_id BIGINT NOT NULL UNIQUE,
|
||||
telegram_username VARCHAR(255),
|
||||
telegram_first_name VARCHAR(255),
|
||||
telegram_last_name VARCHAR(255),
|
||||
|
||||
-- Linking metadata
|
||||
linked_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
linked_via VARCHAR(50) DEFAULT 'bot_command', -- bot_command, web_dashboard, api
|
||||
link_code_used VARCHAR(64),
|
||||
|
||||
-- Status
|
||||
status VARCHAR(20) DEFAULT 'active', -- active, suspended, revoked
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_reason TEXT,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT unique_account_telegram UNIQUE (account_id, telegram_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_account_links_account_id ON account_links(account_id);
|
||||
CREATE INDEX idx_account_links_telegram_user_id ON account_links(telegram_user_id);
|
||||
CREATE INDEX idx_account_links_status ON account_links(status);
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. LINK CODES - One-time codes for account linking
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS link_codes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- The code itself
|
||||
code VARCHAR(64) NOT NULL UNIQUE,
|
||||
|
||||
-- Who generated it
|
||||
account_id UUID NOT NULL,
|
||||
generated_via VARCHAR(50) DEFAULT 'web_dashboard', -- web_dashboard, api, admin
|
||||
|
||||
-- Expiration
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
|
||||
-- Usage tracking
|
||||
used BOOLEAN DEFAULT FALSE,
|
||||
used_at TIMESTAMPTZ,
|
||||
used_by_telegram_id BIGINT,
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT link_code_valid CHECK (expires_at > created_at)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_link_codes_code ON link_codes(code);
|
||||
CREATE INDEX idx_link_codes_account_id ON link_codes(account_id);
|
||||
CREATE INDEX idx_link_codes_expires ON link_codes(expires_at) WHERE NOT used;
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. USER TIMELINE - Cross-channel interaction history
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_timeline (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Account reference (linked user)
|
||||
account_id UUID NOT NULL,
|
||||
|
||||
-- Source channel
|
||||
channel VARCHAR(50) NOT NULL, -- telegram_dm, telegram_group, web_chat, api
|
||||
channel_id VARCHAR(255), -- specific chat_id or session_id
|
||||
|
||||
-- Event type
|
||||
event_type VARCHAR(50) NOT NULL, -- message, command, action, milestone
|
||||
|
||||
-- Content
|
||||
summary TEXT NOT NULL, -- Short summary, not raw content
|
||||
content_hash VARCHAR(64), -- For deduplication
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}',
|
||||
|
||||
-- Importance scoring (for retrieval)
|
||||
importance_score FLOAT DEFAULT 0.5,
|
||||
|
||||
-- Timestamps
|
||||
event_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- Retention
|
||||
expires_at TIMESTAMPTZ, -- NULL = permanent
|
||||
|
||||
CONSTRAINT valid_importance CHECK (importance_score >= 0 AND importance_score <= 1)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_timeline_account_id ON user_timeline(account_id);
|
||||
CREATE INDEX idx_user_timeline_event_at ON user_timeline(event_at DESC);
|
||||
CREATE INDEX idx_user_timeline_channel ON user_timeline(channel);
|
||||
CREATE INDEX idx_user_timeline_importance ON user_timeline(importance_score DESC);
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. ORG CHAT MESSAGES - Official chat logging
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS org_chat_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Chat identification
|
||||
chat_id BIGINT NOT NULL,
|
||||
chat_type VARCHAR(50) NOT NULL, -- official_ops, mentor_room, public_community
|
||||
chat_title VARCHAR(255),
|
||||
|
||||
-- Message
|
||||
message_id BIGINT NOT NULL,
|
||||
sender_telegram_id BIGINT,
|
||||
sender_account_id UUID, -- Linked account if exists
|
||||
sender_username VARCHAR(255),
|
||||
sender_display_name VARCHAR(255),
|
||||
|
||||
-- Content
|
||||
text TEXT,
|
||||
has_media BOOLEAN DEFAULT FALSE,
|
||||
media_type VARCHAR(50), -- photo, video, document, voice
|
||||
attachments_ref JSONB DEFAULT '[]',
|
||||
|
||||
-- Reply tracking
|
||||
reply_to_message_id BIGINT,
|
||||
|
||||
-- Processing status
|
||||
processed_for_decisions BOOLEAN DEFAULT FALSE,
|
||||
processed_at TIMESTAMPTZ,
|
||||
|
||||
-- Timestamps
|
||||
message_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT unique_chat_message UNIQUE (chat_id, message_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_org_chat_messages_chat ON org_chat_messages(chat_id);
|
||||
CREATE INDEX idx_org_chat_messages_sender ON org_chat_messages(sender_telegram_id);
|
||||
CREATE INDEX idx_org_chat_messages_at ON org_chat_messages(message_at DESC);
|
||||
CREATE INDEX idx_org_chat_messages_unprocessed ON org_chat_messages(chat_id)
|
||||
WHERE NOT processed_for_decisions;
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. DECISION RECORDS - Extracted decisions from chats
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decision_records (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Source
|
||||
chat_id BIGINT NOT NULL,
|
||||
source_message_id BIGINT NOT NULL,
|
||||
|
||||
-- Decision content
|
||||
decision TEXT NOT NULL,
|
||||
action TEXT,
|
||||
owner VARCHAR(255), -- @username or role
|
||||
due_date DATE,
|
||||
canon_change BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Status tracking
|
||||
status VARCHAR(50) DEFAULT 'pending', -- pending, in_progress, completed, cancelled
|
||||
status_updated_at TIMESTAMPTZ,
|
||||
status_updated_by VARCHAR(255),
|
||||
|
||||
-- Extraction metadata
|
||||
extracted_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
extraction_method VARCHAR(50) DEFAULT 'regex', -- regex, llm, manual
|
||||
confidence_score FLOAT DEFAULT 1.0,
|
||||
|
||||
-- Verification
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
verified_at TIMESTAMPTZ,
|
||||
verified_by VARCHAR(255),
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT fk_source_message FOREIGN KEY (chat_id, source_message_id)
|
||||
REFERENCES org_chat_messages(chat_id, message_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_decision_records_chat ON decision_records(chat_id);
|
||||
CREATE INDEX idx_decision_records_status ON decision_records(status);
|
||||
CREATE INDEX idx_decision_records_due ON decision_records(due_date) WHERE status NOT IN ('completed', 'cancelled');
|
||||
CREATE INDEX idx_decision_records_canon ON decision_records(canon_change) WHERE canon_change = TRUE;
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. KYC ATTESTATIONS - Status without PII
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kyc_attestations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Account reference
|
||||
account_id UUID NOT NULL UNIQUE,
|
||||
|
||||
-- Attestation fields (NO RAW PII)
|
||||
kyc_status VARCHAR(20) NOT NULL DEFAULT 'unverified', -- unverified, pending, passed, failed
|
||||
kyc_provider VARCHAR(100),
|
||||
jurisdiction VARCHAR(10), -- ISO country code
|
||||
risk_tier VARCHAR(20) DEFAULT 'unknown', -- low, medium, high, unknown
|
||||
pep_sanctions_flag BOOLEAN DEFAULT FALSE,
|
||||
wallet_verified BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Timestamps
|
||||
attested_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ, -- Some KYC needs periodic renewal
|
||||
|
||||
-- Audit
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT valid_kyc_status CHECK (kyc_status IN ('unverified', 'pending', 'passed', 'failed')),
|
||||
CONSTRAINT valid_risk_tier CHECK (risk_tier IN ('low', 'medium', 'high', 'unknown'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_kyc_attestations_account ON kyc_attestations(account_id);
|
||||
CREATE INDEX idx_kyc_attestations_status ON kyc_attestations(kyc_status);
|
||||
|
||||
-- ============================================================================
|
||||
-- 7. HELPER FUNCTIONS
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to resolve Telegram user to account
|
||||
CREATE OR REPLACE FUNCTION resolve_telegram_account(p_telegram_user_id BIGINT)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_account_id UUID;
|
||||
BEGIN
|
||||
SELECT account_id INTO v_account_id
|
||||
FROM account_links
|
||||
WHERE telegram_user_id = p_telegram_user_id
|
||||
AND status = 'active';
|
||||
|
||||
RETURN v_account_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to check if link code is valid
|
||||
CREATE OR REPLACE FUNCTION is_link_code_valid(p_code VARCHAR)
|
||||
RETURNS TABLE(valid BOOLEAN, account_id UUID, error_message TEXT) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
CASE
|
||||
WHEN lc.id IS NULL THEN FALSE
|
||||
WHEN lc.used THEN FALSE
|
||||
WHEN lc.expires_at < NOW() THEN FALSE
|
||||
ELSE TRUE
|
||||
END as valid,
|
||||
lc.account_id,
|
||||
CASE
|
||||
WHEN lc.id IS NULL THEN 'Code not found'
|
||||
WHEN lc.used THEN 'Code already used'
|
||||
WHEN lc.expires_at < NOW() THEN 'Code expired'
|
||||
ELSE NULL
|
||||
END as error_message
|
||||
FROM link_codes lc
|
||||
WHERE lc.code = p_code;
|
||||
|
||||
-- If no rows returned, code doesn't exist
|
||||
IF NOT FOUND THEN
|
||||
RETURN QUERY SELECT FALSE, NULL::UUID, 'Code not found'::TEXT;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to complete linking
|
||||
CREATE OR REPLACE FUNCTION complete_account_link(
|
||||
p_code VARCHAR,
|
||||
p_telegram_user_id BIGINT,
|
||||
p_telegram_username VARCHAR DEFAULT NULL,
|
||||
p_telegram_first_name VARCHAR DEFAULT NULL,
|
||||
p_telegram_last_name VARCHAR DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE(success BOOLEAN, account_id UUID, error_message TEXT) AS $$
|
||||
DECLARE
|
||||
v_account_id UUID;
|
||||
v_link_id UUID;
|
||||
BEGIN
|
||||
-- Check code validity
|
||||
SELECT lc.account_id INTO v_account_id
|
||||
FROM link_codes lc
|
||||
WHERE lc.code = p_code
|
||||
AND NOT lc.used
|
||||
AND lc.expires_at > NOW();
|
||||
|
||||
IF v_account_id IS NULL THEN
|
||||
RETURN QUERY SELECT FALSE, NULL::UUID, 'Invalid or expired code'::TEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Check if already linked
|
||||
IF EXISTS (SELECT 1 FROM account_links WHERE telegram_user_id = p_telegram_user_id AND status = 'active') THEN
|
||||
RETURN QUERY SELECT FALSE, NULL::UUID, 'Telegram account already linked'::TEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Create link
|
||||
INSERT INTO account_links (
|
||||
account_id, telegram_user_id, telegram_username,
|
||||
telegram_first_name, telegram_last_name, link_code_used
|
||||
) VALUES (
|
||||
v_account_id, p_telegram_user_id, p_telegram_username,
|
||||
p_telegram_first_name, p_telegram_last_name, p_code
|
||||
)
|
||||
RETURNING id INTO v_link_id;
|
||||
|
||||
-- Mark code as used
|
||||
UPDATE link_codes SET
|
||||
used = TRUE,
|
||||
used_at = NOW(),
|
||||
used_by_telegram_id = p_telegram_user_id
|
||||
WHERE code = p_code;
|
||||
|
||||
RETURN QUERY SELECT TRUE, v_account_id, NULL::TEXT;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to add timeline event
|
||||
CREATE OR REPLACE FUNCTION add_timeline_event(
|
||||
p_account_id UUID,
|
||||
p_channel VARCHAR,
|
||||
p_channel_id VARCHAR,
|
||||
p_event_type VARCHAR,
|
||||
p_summary TEXT,
|
||||
p_metadata JSONB DEFAULT '{}',
|
||||
p_importance FLOAT DEFAULT 0.5
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_event_id UUID;
|
||||
BEGIN
|
||||
INSERT INTO user_timeline (
|
||||
account_id, channel, channel_id, event_type,
|
||||
summary, metadata, importance_score, event_at
|
||||
) VALUES (
|
||||
p_account_id, p_channel, p_channel_id, p_event_type,
|
||||
p_summary, p_metadata, p_importance, NOW()
|
||||
)
|
||||
RETURNING id INTO v_event_id;
|
||||
|
||||
RETURN v_event_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================================================
|
||||
-- 8. TRIGGERS
|
||||
-- ============================================================================
|
||||
|
||||
-- Update timestamp trigger
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_account_links_timestamp
|
||||
BEFORE UPDATE ON account_links
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER update_decision_records_timestamp
|
||||
BEFORE UPDATE ON decision_records
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER update_kyc_attestations_timestamp
|
||||
BEFORE UPDATE ON kyc_attestations
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================================================
|
||||
-- MIGRATION COMPLETE
|
||||
-- ============================================================================
|
||||
|
||||
-- Insert migration record
|
||||
INSERT INTO helion_session_state (session_id, state_type, state_data)
|
||||
VALUES (
|
||||
'migration_052',
|
||||
'migration',
|
||||
jsonb_build_object(
|
||||
'version', '052',
|
||||
'name', 'account_linking_schema',
|
||||
'applied_at', NOW(),
|
||||
'tables_created', ARRAY[
|
||||
'account_links',
|
||||
'link_codes',
|
||||
'user_timeline',
|
||||
'org_chat_messages',
|
||||
'decision_records',
|
||||
'kyc_attestations'
|
||||
]
|
||||
)
|
||||
) ON CONFLICT (session_id) DO UPDATE SET
|
||||
state_data = EXCLUDED.state_data,
|
||||
updated_at = NOW();
|
||||
Reference in New Issue
Block a user