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:
Apple
2026-01-28 06:40:34 -08:00
parent 4aeb69e7ae
commit 0c8bef82f4
120 changed files with 21905 additions and 425 deletions

View 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';

View 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

View 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;

View 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();