[DDL] feat: Sprint 5 - Add llm schema with 5 tables
- conversations: Conversaciones con agente LLM - conversation_messages: Mensajes individuales - llm_tools_usage: Registro de uso de herramientas - llm_proactive_notifications: Notificaciones proactivas - llm_usage_limits: Limites de uso por usuario/plan VIP Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9f8bbb7494
commit
62c811be45
251
ddl/schemas/llm/tables/001_conversations.sql
Normal file
251
ddl/schemas/llm/tables/001_conversations.sql
Normal file
@ -0,0 +1,251 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: llm
|
||||
-- TABLE: conversations
|
||||
-- DESCRIPTION: Conversaciones con el agente LLM
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 5 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Crear schema si no existe
|
||||
CREATE SCHEMA IF NOT EXISTS llm;
|
||||
|
||||
COMMENT ON SCHEMA llm IS
|
||||
'Sistema de agente LLM con conversaciones, herramientas y limites de uso';
|
||||
|
||||
-- Enum para tipo de conversacion
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.conversation_type AS ENUM (
|
||||
'chat', -- Chat general
|
||||
'trading_assistant', -- Asistente de trading
|
||||
'market_analysis', -- Analisis de mercado
|
||||
'education', -- Educacion/tutoria
|
||||
'support', -- Soporte tecnico
|
||||
'portfolio_review', -- Revision de portafolio
|
||||
'strategy_builder', -- Constructor de estrategias
|
||||
'code_assistant' -- Asistente de codigo
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para estado de conversacion
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.conversation_status AS ENUM (
|
||||
'active', -- Activa
|
||||
'archived', -- Archivada
|
||||
'deleted', -- Eliminada (soft delete)
|
||||
'expired' -- Expirada por inactividad
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para modelo LLM
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.llm_model AS ENUM (
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
'gpt-4-turbo',
|
||||
'claude-3-5-sonnet',
|
||||
'claude-3-opus',
|
||||
'claude-3-5-haiku',
|
||||
'gemini-pro',
|
||||
'gemini-ultra',
|
||||
'llama-3-70b',
|
||||
'mixtral-8x7b',
|
||||
'custom'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Conversaciones
|
||||
CREATE TABLE IF NOT EXISTS llm.conversations (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tipo y estado
|
||||
type llm.conversation_type NOT NULL DEFAULT 'chat',
|
||||
status llm.conversation_status NOT NULL DEFAULT 'active',
|
||||
|
||||
-- Titulo y descripcion
|
||||
title VARCHAR(255),
|
||||
description TEXT,
|
||||
auto_title BOOLEAN NOT NULL DEFAULT TRUE, -- Generar titulo automaticamente
|
||||
|
||||
-- Modelo
|
||||
model llm.llm_model NOT NULL DEFAULT 'gpt-4o-mini',
|
||||
model_config JSONB DEFAULT '{}'::JSONB, -- Temperatura, max_tokens, etc.
|
||||
|
||||
-- System prompt
|
||||
system_prompt TEXT,
|
||||
custom_instructions TEXT,
|
||||
|
||||
-- Contexto
|
||||
context_type VARCHAR(50), -- 'symbol', 'portfolio', 'position'
|
||||
context_id UUID, -- ID del contexto
|
||||
context_data JSONB DEFAULT '{}'::JSONB, -- Datos de contexto
|
||||
|
||||
-- Estadisticas
|
||||
message_count INTEGER NOT NULL DEFAULT 0,
|
||||
token_count_input INTEGER NOT NULL DEFAULT 0,
|
||||
token_count_output INTEGER NOT NULL DEFAULT 0,
|
||||
total_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Costo estimado
|
||||
estimated_cost_usd DECIMAL(10, 6) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Herramientas habilitadas
|
||||
tools_enabled VARCHAR(100)[], -- Herramientas disponibles
|
||||
tools_auto_execute BOOLEAN NOT NULL DEFAULT FALSE, -- Ejecutar herramientas automaticamente
|
||||
|
||||
-- Feedback
|
||||
rating INTEGER CHECK (rating BETWEEN 1 AND 5),
|
||||
feedback TEXT,
|
||||
rated_at TIMESTAMPTZ,
|
||||
|
||||
-- Compartir
|
||||
is_shared BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
share_code VARCHAR(20),
|
||||
shared_at TIMESTAMPTZ,
|
||||
|
||||
-- Ultima actividad
|
||||
last_message_at TIMESTAMPTZ,
|
||||
last_user_message_at TIMESTAMPTZ,
|
||||
|
||||
-- Metadata
|
||||
tags VARCHAR(50)[],
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
archived_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ, -- Para limpiar conversaciones antiguas
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT conversations_share_code_unique UNIQUE (share_code)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE llm.conversations IS
|
||||
'Conversaciones con el agente LLM por usuario';
|
||||
|
||||
COMMENT ON COLUMN llm.conversations.tools_enabled IS
|
||||
'Herramientas habilitadas: market_data, portfolio, trading, charts, etc.';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_tenant
|
||||
ON llm.conversations(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_user
|
||||
ON llm.conversations(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_status
|
||||
ON llm.conversations(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_active
|
||||
ON llm.conversations(user_id, status, updated_at DESC)
|
||||
WHERE status = 'active';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_type
|
||||
ON llm.conversations(type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_last_message
|
||||
ON llm.conversations(last_message_at DESC)
|
||||
WHERE status = 'active';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_share
|
||||
ON llm.conversations(share_code)
|
||||
WHERE share_code IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_context
|
||||
ON llm.conversations(context_type, context_id)
|
||||
WHERE context_id IS NOT NULL;
|
||||
|
||||
-- Funcion de timestamp
|
||||
CREATE OR REPLACE FUNCTION llm.update_llm_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at := NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS conversation_updated_at ON llm.conversations;
|
||||
CREATE TRIGGER conversation_updated_at
|
||||
BEFORE UPDATE ON llm.conversations
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION llm.update_llm_timestamp();
|
||||
|
||||
-- Funcion para generar share code
|
||||
CREATE OR REPLACE FUNCTION llm.generate_share_code()
|
||||
RETURNS VARCHAR AS $$
|
||||
DECLARE
|
||||
v_code VARCHAR;
|
||||
v_exists BOOLEAN;
|
||||
BEGIN
|
||||
LOOP
|
||||
v_code := UPPER(SUBSTRING(MD5(RANDOM()::TEXT || NOW()::TEXT) FROM 1 FOR 8));
|
||||
SELECT EXISTS (SELECT 1 FROM llm.conversations WHERE share_code = v_code) INTO v_exists;
|
||||
EXIT WHEN NOT v_exists;
|
||||
END LOOP;
|
||||
RETURN v_code;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para compartir conversacion
|
||||
CREATE OR REPLACE FUNCTION llm.share_conversation(p_conversation_id UUID)
|
||||
RETURNS VARCHAR AS $$
|
||||
DECLARE
|
||||
v_code VARCHAR;
|
||||
BEGIN
|
||||
v_code := llm.generate_share_code();
|
||||
|
||||
UPDATE llm.conversations
|
||||
SET is_shared = TRUE,
|
||||
share_code = v_code,
|
||||
shared_at = NOW()
|
||||
WHERE id = p_conversation_id;
|
||||
|
||||
RETURN v_code;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de conversaciones recientes
|
||||
CREATE OR REPLACE VIEW llm.v_recent_conversations AS
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
type,
|
||||
title,
|
||||
model,
|
||||
message_count,
|
||||
total_tokens,
|
||||
estimated_cost_usd,
|
||||
last_message_at,
|
||||
created_at
|
||||
FROM llm.conversations
|
||||
WHERE status = 'active'
|
||||
ORDER BY last_message_at DESC NULLS LAST;
|
||||
|
||||
-- RLS Policies
|
||||
ALTER TABLE llm.conversations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY conversations_tenant_isolation ON llm.conversations
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
CREATE POLICY conversations_user_isolation ON llm.conversations
|
||||
FOR ALL
|
||||
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT USAGE ON SCHEMA llm TO trading_app;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON llm.conversations TO trading_app;
|
||||
GRANT SELECT ON llm.conversations TO trading_readonly;
|
||||
GRANT SELECT ON llm.v_recent_conversations TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.share_conversation TO trading_app;
|
||||
248
ddl/schemas/llm/tables/002_conversation_messages.sql
Normal file
248
ddl/schemas/llm/tables/002_conversation_messages.sql
Normal file
@ -0,0 +1,248 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: llm
|
||||
-- TABLE: conversation_messages
|
||||
-- DESCRIPTION: Mensajes dentro de conversaciones LLM
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 5 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para rol del mensaje
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.message_role AS ENUM (
|
||||
'system', -- Mensaje de sistema
|
||||
'user', -- Mensaje del usuario
|
||||
'assistant', -- Respuesta del asistente
|
||||
'tool', -- Resultado de herramienta
|
||||
'function' -- Llamada a funcion (legacy)
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para estado del mensaje
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.message_status AS ENUM (
|
||||
'pending', -- Pendiente de procesar
|
||||
'streaming', -- Streaming en progreso
|
||||
'completed', -- Completado
|
||||
'error', -- Error
|
||||
'cancelled', -- Cancelado
|
||||
'edited' -- Editado por usuario
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Mensajes
|
||||
CREATE TABLE IF NOT EXISTS llm.conversation_messages (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
conversation_id UUID NOT NULL REFERENCES llm.conversations(id) ON DELETE CASCADE,
|
||||
parent_message_id UUID REFERENCES llm.conversation_messages(id),
|
||||
|
||||
-- Rol y contenido
|
||||
role llm.message_role NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
status llm.message_status NOT NULL DEFAULT 'completed',
|
||||
|
||||
-- Para mensajes de herramientas
|
||||
tool_call_id VARCHAR(100),
|
||||
tool_name VARCHAR(100),
|
||||
tool_input JSONB,
|
||||
tool_output JSONB,
|
||||
|
||||
-- Tokens
|
||||
token_count INTEGER DEFAULT 0,
|
||||
prompt_tokens INTEGER DEFAULT 0,
|
||||
completion_tokens INTEGER DEFAULT 0,
|
||||
|
||||
-- Modelo usado
|
||||
model VARCHAR(50),
|
||||
|
||||
-- Tiempos de respuesta
|
||||
latency_ms INTEGER, -- Tiempo de respuesta
|
||||
time_to_first_token_ms INTEGER, -- TTFT para streaming
|
||||
|
||||
-- Contenido estructurado
|
||||
attachments JSONB DEFAULT '[]'::JSONB, -- Archivos adjuntos
|
||||
citations JSONB DEFAULT '[]'::JSONB, -- Citas/referencias
|
||||
code_blocks JSONB DEFAULT '[]'::JSONB, -- Bloques de codigo
|
||||
|
||||
-- Markdown y formato
|
||||
is_markdown BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
rendered_html TEXT, -- HTML pre-renderizado
|
||||
|
||||
-- Feedback
|
||||
thumbs_up BOOLEAN,
|
||||
feedback_text TEXT,
|
||||
feedback_at TIMESTAMPTZ,
|
||||
|
||||
-- Edicion
|
||||
is_edited BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
original_content TEXT,
|
||||
edited_at TIMESTAMPTZ,
|
||||
|
||||
-- Regeneracion
|
||||
is_regenerated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
regeneration_count INTEGER NOT NULL DEFAULT 0,
|
||||
previous_version_id UUID,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Indices para orden
|
||||
sequence_number INTEGER NOT NULL DEFAULT 0 -- Orden en la conversacion
|
||||
);
|
||||
|
||||
COMMENT ON TABLE llm.conversation_messages IS
|
||||
'Mensajes individuales dentro de conversaciones LLM';
|
||||
|
||||
COMMENT ON COLUMN llm.conversation_messages.tool_call_id IS
|
||||
'ID de llamada a herramienta para vincular request/response';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_conversation
|
||||
ON llm.conversation_messages(conversation_id, sequence_number);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_parent
|
||||
ON llm.conversation_messages(parent_message_id)
|
||||
WHERE parent_message_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_role
|
||||
ON llm.conversation_messages(role);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_tool
|
||||
ON llm.conversation_messages(tool_call_id)
|
||||
WHERE tool_call_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_created
|
||||
ON llm.conversation_messages(created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_feedback
|
||||
ON llm.conversation_messages(thumbs_up, feedback_at)
|
||||
WHERE thumbs_up IS NOT NULL;
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS message_updated_at ON llm.conversation_messages;
|
||||
CREATE TRIGGER message_updated_at
|
||||
BEFORE UPDATE ON llm.conversation_messages
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION llm.update_llm_timestamp();
|
||||
|
||||
-- Trigger para actualizar estadisticas de conversacion
|
||||
CREATE OR REPLACE FUNCTION llm.update_conversation_stats()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
UPDATE llm.conversations
|
||||
SET message_count = message_count + 1,
|
||||
token_count_input = token_count_input + COALESCE(NEW.prompt_tokens, 0),
|
||||
token_count_output = token_count_output + COALESCE(NEW.completion_tokens, 0),
|
||||
total_tokens = total_tokens + COALESCE(NEW.token_count, 0),
|
||||
last_message_at = NEW.created_at,
|
||||
last_user_message_at = CASE WHEN NEW.role = 'user' THEN NEW.created_at ELSE last_user_message_at END
|
||||
WHERE id = NEW.conversation_id;
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
UPDATE llm.conversations
|
||||
SET message_count = message_count - 1,
|
||||
token_count_input = token_count_input - COALESCE(OLD.prompt_tokens, 0),
|
||||
token_count_output = token_count_output - COALESCE(OLD.completion_tokens, 0),
|
||||
total_tokens = total_tokens - COALESCE(OLD.token_count, 0)
|
||||
WHERE id = OLD.conversation_id;
|
||||
END IF;
|
||||
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS message_stats_update ON llm.conversation_messages;
|
||||
CREATE TRIGGER message_stats_update
|
||||
AFTER INSERT OR DELETE ON llm.conversation_messages
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION llm.update_conversation_stats();
|
||||
|
||||
-- Trigger para asignar sequence_number
|
||||
CREATE OR REPLACE FUNCTION llm.assign_message_sequence()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
SELECT COALESCE(MAX(sequence_number), 0) + 1 INTO NEW.sequence_number
|
||||
FROM llm.conversation_messages
|
||||
WHERE conversation_id = NEW.conversation_id;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS message_sequence ON llm.conversation_messages;
|
||||
CREATE TRIGGER message_sequence
|
||||
BEFORE INSERT ON llm.conversation_messages
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.sequence_number = 0)
|
||||
EXECUTE FUNCTION llm.assign_message_sequence();
|
||||
|
||||
-- Funcion para obtener historial de conversacion
|
||||
CREATE OR REPLACE FUNCTION llm.get_conversation_history(
|
||||
p_conversation_id UUID,
|
||||
p_limit INTEGER DEFAULT 50
|
||||
)
|
||||
RETURNS TABLE (
|
||||
role llm.message_role,
|
||||
content TEXT,
|
||||
tool_name VARCHAR,
|
||||
tool_output JSONB,
|
||||
created_at TIMESTAMPTZ
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
m.role,
|
||||
m.content,
|
||||
m.tool_name,
|
||||
m.tool_output,
|
||||
m.created_at
|
||||
FROM llm.conversation_messages m
|
||||
WHERE m.conversation_id = p_conversation_id
|
||||
AND m.status = 'completed'
|
||||
ORDER BY m.sequence_number DESC
|
||||
LIMIT p_limit;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de mensajes con feedback negativo (para mejorar)
|
||||
CREATE OR REPLACE VIEW llm.v_messages_negative_feedback AS
|
||||
SELECT
|
||||
m.id,
|
||||
m.conversation_id,
|
||||
c.type AS conversation_type,
|
||||
m.role,
|
||||
m.content,
|
||||
m.model,
|
||||
m.feedback_text,
|
||||
m.created_at
|
||||
FROM llm.conversation_messages m
|
||||
JOIN llm.conversations c ON m.conversation_id = c.id
|
||||
WHERE m.thumbs_up = FALSE
|
||||
ORDER BY m.feedback_at DESC;
|
||||
|
||||
-- RLS Policies (hereda de conversations via JOIN)
|
||||
ALTER TABLE llm.conversation_messages ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY messages_via_conversation ON llm.conversation_messages
|
||||
FOR ALL
|
||||
USING (
|
||||
conversation_id IN (
|
||||
SELECT id FROM llm.conversations
|
||||
WHERE user_id = current_setting('app.current_user_id', true)::UUID
|
||||
)
|
||||
);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON llm.conversation_messages TO trading_app;
|
||||
GRANT SELECT ON llm.conversation_messages TO trading_readonly;
|
||||
GRANT SELECT ON llm.v_messages_negative_feedback TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.get_conversation_history TO trading_app;
|
||||
263
ddl/schemas/llm/tables/003_llm_tools_usage.sql
Normal file
263
ddl/schemas/llm/tables/003_llm_tools_usage.sql
Normal file
@ -0,0 +1,263 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: llm
|
||||
-- TABLE: llm_tools_usage
|
||||
-- DESCRIPTION: Registro de uso de herramientas por el agente LLM
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 5 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para categoria de herramienta
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.tool_category AS ENUM (
|
||||
'market_data', -- Datos de mercado
|
||||
'trading', -- Operaciones de trading
|
||||
'portfolio', -- Gestion de portafolio
|
||||
'analysis', -- Analisis tecnico/fundamental
|
||||
'charts', -- Graficos
|
||||
'alerts', -- Alertas
|
||||
'education', -- Educacion
|
||||
'news', -- Noticias
|
||||
'calendar', -- Calendario economico
|
||||
'calculations', -- Calculos financieros
|
||||
'external_api', -- APIs externas
|
||||
'system' -- Sistema
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para estado de ejecucion
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.tool_execution_status AS ENUM (
|
||||
'pending', -- Pendiente
|
||||
'executing', -- Ejecutando
|
||||
'success', -- Exitoso
|
||||
'error', -- Error
|
||||
'timeout', -- Timeout
|
||||
'cancelled', -- Cancelado
|
||||
'rate_limited', -- Rate limited
|
||||
'permission_denied' -- Sin permisos
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Uso de Herramientas
|
||||
CREATE TABLE IF NOT EXISTS llm.llm_tools_usage (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Referencias
|
||||
conversation_id UUID REFERENCES llm.conversations(id) ON DELETE SET NULL,
|
||||
message_id UUID REFERENCES llm.conversation_messages(id) ON DELETE SET NULL,
|
||||
|
||||
-- Herramienta
|
||||
tool_name VARCHAR(100) NOT NULL,
|
||||
tool_category llm.tool_category NOT NULL,
|
||||
tool_version VARCHAR(20),
|
||||
|
||||
-- Ejecucion
|
||||
status llm.tool_execution_status NOT NULL DEFAULT 'pending',
|
||||
execution_time_ms INTEGER,
|
||||
|
||||
-- Input/Output
|
||||
input_params JSONB NOT NULL DEFAULT '{}'::JSONB,
|
||||
output_result JSONB,
|
||||
output_size_bytes INTEGER,
|
||||
|
||||
-- Error
|
||||
error_code VARCHAR(50),
|
||||
error_message TEXT,
|
||||
error_details JSONB,
|
||||
|
||||
-- Contexto
|
||||
context_symbol VARCHAR(20),
|
||||
context_timeframe VARCHAR(10),
|
||||
context_data JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Rate limiting
|
||||
rate_limit_bucket VARCHAR(50),
|
||||
rate_limit_remaining INTEGER,
|
||||
|
||||
-- Costo
|
||||
cost_units DECIMAL(10, 4) DEFAULT 0, -- Unidades de costo (API calls, etc)
|
||||
cost_usd DECIMAL(10, 6) DEFAULT 0, -- Costo en USD
|
||||
|
||||
-- Auditoria
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE llm.llm_tools_usage IS
|
||||
'Registro de todas las ejecuciones de herramientas por el agente LLM';
|
||||
|
||||
COMMENT ON COLUMN llm.llm_tools_usage.cost_units IS
|
||||
'Unidades de costo internas (ej: 1 API call = 1 unit)';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_tenant
|
||||
ON llm.llm_tools_usage(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_user
|
||||
ON llm.llm_tools_usage(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_conversation
|
||||
ON llm.llm_tools_usage(conversation_id)
|
||||
WHERE conversation_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_tool
|
||||
ON llm.llm_tools_usage(tool_name, requested_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_category
|
||||
ON llm.llm_tools_usage(tool_category, requested_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_status
|
||||
ON llm.llm_tools_usage(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_errors
|
||||
ON llm.llm_tools_usage(status, tool_name)
|
||||
WHERE status IN ('error', 'timeout');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_date
|
||||
ON llm.llm_tools_usage(requested_at DESC);
|
||||
|
||||
-- Indice BRIN para datos temporales
|
||||
CREATE INDEX IF NOT EXISTS idx_tools_usage_brin
|
||||
ON llm.llm_tools_usage USING BRIN (requested_at);
|
||||
|
||||
-- Funcion para registrar inicio de ejecucion
|
||||
CREATE OR REPLACE FUNCTION llm.start_tool_execution(
|
||||
p_tool_usage_id UUID
|
||||
)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
UPDATE llm.llm_tools_usage
|
||||
SET status = 'executing',
|
||||
started_at = NOW()
|
||||
WHERE id = p_tool_usage_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para registrar fin exitoso
|
||||
CREATE OR REPLACE FUNCTION llm.complete_tool_execution(
|
||||
p_tool_usage_id UUID,
|
||||
p_output JSONB
|
||||
)
|
||||
RETURNS VOID AS $$
|
||||
DECLARE
|
||||
v_started_at TIMESTAMPTZ;
|
||||
BEGIN
|
||||
SELECT started_at INTO v_started_at
|
||||
FROM llm.llm_tools_usage
|
||||
WHERE id = p_tool_usage_id;
|
||||
|
||||
UPDATE llm.llm_tools_usage
|
||||
SET status = 'success',
|
||||
completed_at = NOW(),
|
||||
execution_time_ms = EXTRACT(EPOCH FROM (NOW() - v_started_at)) * 1000,
|
||||
output_result = p_output,
|
||||
output_size_bytes = LENGTH(p_output::TEXT)
|
||||
WHERE id = p_tool_usage_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para registrar error
|
||||
CREATE OR REPLACE FUNCTION llm.fail_tool_execution(
|
||||
p_tool_usage_id UUID,
|
||||
p_error_code VARCHAR,
|
||||
p_error_message TEXT,
|
||||
p_error_details JSONB DEFAULT NULL
|
||||
)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
UPDATE llm.llm_tools_usage
|
||||
SET status = 'error',
|
||||
completed_at = NOW(),
|
||||
error_code = p_error_code,
|
||||
error_message = p_error_message,
|
||||
error_details = p_error_details
|
||||
WHERE id = p_tool_usage_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de estadisticas de herramientas
|
||||
CREATE OR REPLACE VIEW llm.v_tool_usage_stats AS
|
||||
SELECT
|
||||
tool_name,
|
||||
tool_category,
|
||||
COUNT(*) AS total_calls,
|
||||
COUNT(*) FILTER (WHERE status = 'success') AS successful_calls,
|
||||
COUNT(*) FILTER (WHERE status = 'error') AS failed_calls,
|
||||
ROUND((COUNT(*) FILTER (WHERE status = 'success')::DECIMAL
|
||||
/ NULLIF(COUNT(*), 0) * 100), 2) AS success_rate,
|
||||
ROUND(AVG(execution_time_ms)::NUMERIC, 2) AS avg_execution_ms,
|
||||
ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY execution_time_ms)::NUMERIC, 2) AS p95_execution_ms,
|
||||
SUM(cost_units) AS total_cost_units,
|
||||
SUM(cost_usd) AS total_cost_usd
|
||||
FROM llm.llm_tools_usage
|
||||
WHERE completed_at IS NOT NULL
|
||||
GROUP BY tool_name, tool_category
|
||||
ORDER BY total_calls DESC;
|
||||
|
||||
-- Vista de errores recientes
|
||||
CREATE OR REPLACE VIEW llm.v_tool_errors AS
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
tool_name,
|
||||
tool_category,
|
||||
error_code,
|
||||
error_message,
|
||||
input_params,
|
||||
requested_at
|
||||
FROM llm.llm_tools_usage
|
||||
WHERE status IN ('error', 'timeout')
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT 100;
|
||||
|
||||
-- Vista de uso por usuario
|
||||
CREATE OR REPLACE VIEW llm.v_user_tool_usage AS
|
||||
SELECT
|
||||
user_id,
|
||||
tool_category,
|
||||
COUNT(*) AS total_calls,
|
||||
SUM(cost_units) AS total_cost_units,
|
||||
SUM(cost_usd) AS total_cost_usd,
|
||||
MAX(requested_at) AS last_used_at
|
||||
FROM llm.llm_tools_usage
|
||||
WHERE requested_at >= NOW() - INTERVAL '30 days'
|
||||
GROUP BY user_id, tool_category
|
||||
ORDER BY user_id, total_calls DESC;
|
||||
|
||||
-- RLS Policies
|
||||
ALTER TABLE llm.llm_tools_usage ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY tools_usage_tenant_isolation ON llm.llm_tools_usage
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
CREATE POLICY tools_usage_user_isolation ON llm.llm_tools_usage
|
||||
FOR SELECT
|
||||
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE ON llm.llm_tools_usage TO trading_app;
|
||||
GRANT SELECT ON llm.llm_tools_usage TO trading_readonly;
|
||||
GRANT SELECT ON llm.v_tool_usage_stats TO trading_app;
|
||||
GRANT SELECT ON llm.v_tool_errors TO trading_app;
|
||||
GRANT SELECT ON llm.v_user_tool_usage TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.start_tool_execution TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.complete_tool_execution TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.fail_tool_execution TO trading_app;
|
||||
284
ddl/schemas/llm/tables/004_llm_proactive_notifications.sql
Normal file
284
ddl/schemas/llm/tables/004_llm_proactive_notifications.sql
Normal file
@ -0,0 +1,284 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: llm
|
||||
-- TABLE: llm_proactive_notifications
|
||||
-- DESCRIPTION: Notificaciones proactivas generadas por el agente LLM
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 5 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para tipo de notificacion proactiva
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.proactive_notification_type AS ENUM (
|
||||
'market_alert', -- Alerta de mercado
|
||||
'trading_opportunity', -- Oportunidad de trading
|
||||
'portfolio_update', -- Actualizacion de portafolio
|
||||
'risk_warning', -- Advertencia de riesgo
|
||||
'news_digest', -- Resumen de noticias
|
||||
'learning_reminder', -- Recordatorio de aprendizaje
|
||||
'goal_progress', -- Progreso hacia metas
|
||||
'weekly_summary', -- Resumen semanal
|
||||
'strategy_insight', -- Insight de estrategia
|
||||
'market_open', -- Apertura de mercado
|
||||
'market_close', -- Cierre de mercado
|
||||
'custom' -- Personalizado
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para prioridad
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.notification_priority AS ENUM (
|
||||
'low',
|
||||
'normal',
|
||||
'high',
|
||||
'urgent'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para estado de notificacion
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.notification_status AS ENUM (
|
||||
'pending', -- Pendiente de enviar
|
||||
'scheduled', -- Programada
|
||||
'sent', -- Enviada
|
||||
'delivered', -- Entregada
|
||||
'read', -- Leida
|
||||
'clicked', -- Click en accion
|
||||
'dismissed', -- Descartada
|
||||
'failed', -- Fallo en envio
|
||||
'expired' -- Expirada
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Notificaciones Proactivas
|
||||
CREATE TABLE IF NOT EXISTS llm.llm_proactive_notifications (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tipo y prioridad
|
||||
type llm.proactive_notification_type NOT NULL,
|
||||
priority llm.notification_priority NOT NULL DEFAULT 'normal',
|
||||
status llm.notification_status NOT NULL DEFAULT 'pending',
|
||||
|
||||
-- Contenido
|
||||
title VARCHAR(255) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
summary TEXT, -- Resumen corto para push
|
||||
|
||||
-- Accion
|
||||
action_type VARCHAR(50), -- 'open_chart', 'view_position', 'start_lesson'
|
||||
action_url TEXT,
|
||||
action_data JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Contexto
|
||||
context_type VARCHAR(50), -- 'symbol', 'position', 'course'
|
||||
context_id UUID,
|
||||
related_symbols VARCHAR(20)[],
|
||||
|
||||
-- Canales de entrega
|
||||
channels VARCHAR(20)[] DEFAULT ARRAY['in_app'], -- 'in_app', 'push', 'email', 'sms'
|
||||
channel_results JSONB DEFAULT '{}'::JSONB, -- Resultado por canal
|
||||
|
||||
-- Generacion
|
||||
generated_by VARCHAR(100), -- Modelo/servicio que genero
|
||||
generation_reason TEXT,
|
||||
generation_data JSONB DEFAULT '{}'::JSONB, -- Datos usados para generar
|
||||
|
||||
-- Confianza y relevancia
|
||||
confidence_score DECIMAL(5, 4), -- 0-1
|
||||
relevance_score DECIMAL(5, 4), -- 0-1
|
||||
personalization_score DECIMAL(5, 4), -- 0-1
|
||||
|
||||
-- Programacion
|
||||
scheduled_for TIMESTAMPTZ,
|
||||
send_window_start TIME, -- Ventana de envio preferida
|
||||
send_window_end TIME,
|
||||
timezone VARCHAR(50) DEFAULT 'UTC',
|
||||
|
||||
-- Envio
|
||||
sent_at TIMESTAMPTZ,
|
||||
delivered_at TIMESTAMPTZ,
|
||||
read_at TIMESTAMPTZ,
|
||||
clicked_at TIMESTAMPTZ,
|
||||
dismissed_at TIMESTAMPTZ,
|
||||
|
||||
-- Expiracion
|
||||
expires_at TIMESTAMPTZ,
|
||||
|
||||
-- Feedback
|
||||
user_feedback VARCHAR(20), -- 'helpful', 'not_helpful', 'spam'
|
||||
feedback_text TEXT,
|
||||
feedback_at TIMESTAMPTZ,
|
||||
|
||||
-- Agrupacion
|
||||
group_key VARCHAR(100), -- Para agrupar notificaciones similares
|
||||
is_grouped BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
group_count INTEGER DEFAULT 1,
|
||||
|
||||
-- Metadata
|
||||
tags VARCHAR(50)[],
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE llm.llm_proactive_notifications IS
|
||||
'Notificaciones proactivas generadas por el agente LLM para engagement del usuario';
|
||||
|
||||
COMMENT ON COLUMN llm.llm_proactive_notifications.confidence_score IS
|
||||
'Confianza del modelo en la relevancia de la notificacion';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_tenant
|
||||
ON llm.llm_proactive_notifications(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_user
|
||||
ON llm.llm_proactive_notifications(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_status
|
||||
ON llm.llm_proactive_notifications(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_pending
|
||||
ON llm.llm_proactive_notifications(user_id, status, scheduled_for)
|
||||
WHERE status IN ('pending', 'scheduled');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_type
|
||||
ON llm.llm_proactive_notifications(type, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_priority
|
||||
ON llm.llm_proactive_notifications(priority, status)
|
||||
WHERE priority IN ('high', 'urgent') AND status = 'pending';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_scheduled
|
||||
ON llm.llm_proactive_notifications(scheduled_for)
|
||||
WHERE status = 'scheduled' AND scheduled_for IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_unread
|
||||
ON llm.llm_proactive_notifications(user_id, read_at)
|
||||
WHERE status IN ('sent', 'delivered') AND read_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proactive_notif_group
|
||||
ON llm.llm_proactive_notifications(group_key)
|
||||
WHERE group_key IS NOT NULL;
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS proactive_notif_updated_at ON llm.llm_proactive_notifications;
|
||||
CREATE TRIGGER proactive_notif_updated_at
|
||||
BEFORE UPDATE ON llm.llm_proactive_notifications
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION llm.update_llm_timestamp();
|
||||
|
||||
-- Funcion para marcar como leida
|
||||
CREATE OR REPLACE FUNCTION llm.mark_notification_read(
|
||||
p_notification_id UUID
|
||||
)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
UPDATE llm.llm_proactive_notifications
|
||||
SET status = 'read',
|
||||
read_at = NOW()
|
||||
WHERE id = p_notification_id
|
||||
AND read_at IS NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para obtener notificaciones pendientes
|
||||
CREATE OR REPLACE FUNCTION llm.get_pending_notifications(
|
||||
p_user_id UUID,
|
||||
p_limit INTEGER DEFAULT 20
|
||||
)
|
||||
RETURNS SETOF llm.llm_proactive_notifications AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT *
|
||||
FROM llm.llm_proactive_notifications
|
||||
WHERE user_id = p_user_id
|
||||
AND status IN ('sent', 'delivered')
|
||||
AND read_at IS NULL
|
||||
AND (expires_at IS NULL OR expires_at > NOW())
|
||||
ORDER BY
|
||||
CASE priority
|
||||
WHEN 'urgent' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'normal' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
created_at DESC
|
||||
LIMIT p_limit;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para contar no leidas
|
||||
CREATE OR REPLACE FUNCTION llm.count_unread_notifications(p_user_id UUID)
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
v_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_count
|
||||
FROM llm.llm_proactive_notifications
|
||||
WHERE user_id = p_user_id
|
||||
AND status IN ('sent', 'delivered')
|
||||
AND read_at IS NULL
|
||||
AND (expires_at IS NULL OR expires_at > NOW());
|
||||
|
||||
RETURN v_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de notificaciones para enviar
|
||||
CREATE OR REPLACE VIEW llm.v_notifications_to_send AS
|
||||
SELECT *
|
||||
FROM llm.llm_proactive_notifications
|
||||
WHERE status = 'scheduled'
|
||||
AND scheduled_for <= NOW()
|
||||
AND (expires_at IS NULL OR expires_at > NOW())
|
||||
ORDER BY priority DESC, scheduled_for;
|
||||
|
||||
-- Vista de efectividad de notificaciones
|
||||
CREATE OR REPLACE VIEW llm.v_notification_effectiveness AS
|
||||
SELECT
|
||||
type,
|
||||
priority,
|
||||
COUNT(*) AS total_sent,
|
||||
COUNT(*) FILTER (WHERE read_at IS NOT NULL) AS read_count,
|
||||
COUNT(*) FILTER (WHERE clicked_at IS NOT NULL) AS clicked_count,
|
||||
ROUND((COUNT(*) FILTER (WHERE read_at IS NOT NULL)::DECIMAL
|
||||
/ NULLIF(COUNT(*), 0) * 100), 2) AS read_rate,
|
||||
ROUND((COUNT(*) FILTER (WHERE clicked_at IS NOT NULL)::DECIMAL
|
||||
/ NULLIF(COUNT(*) FILTER (WHERE read_at IS NOT NULL), 0) * 100), 2) AS click_rate,
|
||||
COUNT(*) FILTER (WHERE user_feedback = 'helpful') AS helpful_count,
|
||||
COUNT(*) FILTER (WHERE user_feedback = 'not_helpful') AS not_helpful_count
|
||||
FROM llm.llm_proactive_notifications
|
||||
WHERE status IN ('sent', 'delivered', 'read', 'clicked', 'dismissed')
|
||||
GROUP BY type, priority
|
||||
ORDER BY total_sent DESC;
|
||||
|
||||
-- RLS Policies
|
||||
ALTER TABLE llm.llm_proactive_notifications ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY proactive_notif_tenant ON llm.llm_proactive_notifications
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
CREATE POLICY proactive_notif_user ON llm.llm_proactive_notifications
|
||||
FOR ALL
|
||||
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON llm.llm_proactive_notifications TO trading_app;
|
||||
GRANT SELECT ON llm.llm_proactive_notifications TO trading_readonly;
|
||||
GRANT SELECT ON llm.v_notifications_to_send TO trading_app;
|
||||
GRANT SELECT ON llm.v_notification_effectiveness TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.mark_notification_read TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.get_pending_notifications TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.count_unread_notifications TO trading_app;
|
||||
358
ddl/schemas/llm/tables/005_llm_usage_limits.sql
Normal file
358
ddl/schemas/llm/tables/005_llm_usage_limits.sql
Normal file
@ -0,0 +1,358 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: llm
|
||||
-- TABLE: llm_usage_limits
|
||||
-- DESCRIPTION: Limites de uso del agente LLM por usuario/plan
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 5 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para periodo de reset
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE llm.reset_period AS ENUM (
|
||||
'hourly',
|
||||
'daily',
|
||||
'weekly',
|
||||
'monthly',
|
||||
'never' -- Sin reset (lifetime limit)
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Limites de Uso
|
||||
CREATE TABLE IF NOT EXISTS llm.llm_usage_limits (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Plan VIP asociado
|
||||
vip_plan_id UUID, -- FK a vip.plans si existe
|
||||
vip_level VARCHAR(20), -- 'free', 'basic', 'pro', 'enterprise'
|
||||
|
||||
-- Limites de mensajes
|
||||
messages_limit INTEGER NOT NULL DEFAULT 100, -- Mensajes por periodo
|
||||
messages_used INTEGER NOT NULL DEFAULT 0,
|
||||
messages_reset_period llm.reset_period NOT NULL DEFAULT 'daily',
|
||||
messages_reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '1 day',
|
||||
|
||||
-- Limites de tokens
|
||||
tokens_limit INTEGER NOT NULL DEFAULT 100000, -- Tokens por periodo
|
||||
tokens_used INTEGER NOT NULL DEFAULT 0,
|
||||
tokens_reset_period llm.reset_period NOT NULL DEFAULT 'monthly',
|
||||
tokens_reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '1 month',
|
||||
|
||||
-- Limites de herramientas
|
||||
tools_limit INTEGER NOT NULL DEFAULT 50, -- Llamadas a herramientas por periodo
|
||||
tools_used INTEGER NOT NULL DEFAULT 0,
|
||||
tools_reset_period llm.reset_period NOT NULL DEFAULT 'daily',
|
||||
tools_reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '1 day',
|
||||
|
||||
-- Limites de conversaciones
|
||||
conversations_limit INTEGER, -- Conversaciones activas max (NULL = unlimited)
|
||||
conversations_active INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Modelos permitidos
|
||||
allowed_models llm.llm_model[] DEFAULT ARRAY['gpt-4o-mini']::llm.llm_model[],
|
||||
default_model llm.llm_model NOT NULL DEFAULT 'gpt-4o-mini',
|
||||
|
||||
-- Herramientas permitidas
|
||||
allowed_tools VARCHAR(100)[], -- NULL = todas
|
||||
blocked_tools VARCHAR(100)[], -- Herramientas bloqueadas
|
||||
|
||||
-- Features
|
||||
can_use_vision BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
can_use_voice BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
can_share_conversations BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
can_export_conversations BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
can_use_code_interpreter BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Rate limiting
|
||||
requests_per_minute INTEGER DEFAULT 10,
|
||||
requests_per_hour INTEGER DEFAULT 100,
|
||||
current_minute_requests INTEGER NOT NULL DEFAULT 0,
|
||||
current_hour_requests INTEGER NOT NULL DEFAULT 0,
|
||||
minute_reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '1 minute',
|
||||
hour_reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '1 hour',
|
||||
|
||||
-- Costo
|
||||
monthly_budget_usd DECIMAL(10, 2), -- Presupuesto mensual
|
||||
monthly_spent_usd DECIMAL(10, 6) NOT NULL DEFAULT 0,
|
||||
budget_alert_threshold DECIMAL(5, 2) DEFAULT 80, -- % para alerta
|
||||
budget_alert_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Overage (exceso)
|
||||
allow_overage BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
overage_rate_multiplier DECIMAL(5, 2) DEFAULT 1.5, -- Multiplicador de costo en overage
|
||||
overage_limit_percent DECIMAL(5, 2) DEFAULT 20, -- % max de overage permitido
|
||||
|
||||
-- Estado
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_suspended BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
suspension_reason TEXT,
|
||||
suspended_at TIMESTAMPTZ,
|
||||
|
||||
-- Notificaciones
|
||||
notify_at_percent INTEGER[] DEFAULT ARRAY[50, 80, 100],
|
||||
last_notification_percent INTEGER,
|
||||
|
||||
-- Metadata
|
||||
custom_limits JSONB DEFAULT '{}'::JSONB, -- Limites personalizados
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT usage_limits_user_unique UNIQUE (user_id),
|
||||
CONSTRAINT usage_limits_valid_percents CHECK (
|
||||
budget_alert_threshold BETWEEN 0 AND 100
|
||||
AND overage_limit_percent BETWEEN 0 AND 100
|
||||
)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE llm.llm_usage_limits IS
|
||||
'Limites de uso del agente LLM por usuario, vinculado a planes VIP';
|
||||
|
||||
COMMENT ON COLUMN llm.llm_usage_limits.allow_overage IS
|
||||
'Si TRUE, permite uso adicional con costo extra cuando se alcanza el limite';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_limits_tenant
|
||||
ON llm.llm_usage_limits(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_limits_user
|
||||
ON llm.llm_usage_limits(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_limits_vip
|
||||
ON llm.llm_usage_limits(vip_level);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_limits_reset
|
||||
ON llm.llm_usage_limits(messages_reset_at, tokens_reset_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_limits_suspended
|
||||
ON llm.llm_usage_limits(is_suspended)
|
||||
WHERE is_suspended = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_limits_budget_alert
|
||||
ON llm.llm_usage_limits(monthly_budget_usd, monthly_spent_usd)
|
||||
WHERE monthly_budget_usd IS NOT NULL;
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS usage_limits_updated_at ON llm.llm_usage_limits;
|
||||
CREATE TRIGGER usage_limits_updated_at
|
||||
BEFORE UPDATE ON llm.llm_usage_limits
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION llm.update_llm_timestamp();
|
||||
|
||||
-- Funcion para verificar y resetear limites
|
||||
CREATE OR REPLACE FUNCTION llm.check_and_reset_limits(p_user_id UUID)
|
||||
RETURNS llm.llm_usage_limits AS $$
|
||||
DECLARE
|
||||
v_limits llm.llm_usage_limits;
|
||||
BEGIN
|
||||
SELECT * INTO v_limits FROM llm.llm_usage_limits WHERE user_id = p_user_id FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- Reset mensajes si es necesario
|
||||
IF v_limits.messages_reset_at <= NOW() THEN
|
||||
UPDATE llm.llm_usage_limits
|
||||
SET messages_used = 0,
|
||||
messages_reset_at = NOW() + (
|
||||
CASE messages_reset_period
|
||||
WHEN 'hourly' THEN INTERVAL '1 hour'
|
||||
WHEN 'daily' THEN INTERVAL '1 day'
|
||||
WHEN 'weekly' THEN INTERVAL '1 week'
|
||||
WHEN 'monthly' THEN INTERVAL '1 month'
|
||||
ELSE INTERVAL '100 years'
|
||||
END
|
||||
)
|
||||
WHERE user_id = p_user_id;
|
||||
END IF;
|
||||
|
||||
-- Reset tokens si es necesario
|
||||
IF v_limits.tokens_reset_at <= NOW() THEN
|
||||
UPDATE llm.llm_usage_limits
|
||||
SET tokens_used = 0,
|
||||
tokens_reset_at = NOW() + (
|
||||
CASE tokens_reset_period
|
||||
WHEN 'hourly' THEN INTERVAL '1 hour'
|
||||
WHEN 'daily' THEN INTERVAL '1 day'
|
||||
WHEN 'weekly' THEN INTERVAL '1 week'
|
||||
WHEN 'monthly' THEN INTERVAL '1 month'
|
||||
ELSE INTERVAL '100 years'
|
||||
END
|
||||
)
|
||||
WHERE user_id = p_user_id;
|
||||
END IF;
|
||||
|
||||
-- Reset herramientas si es necesario
|
||||
IF v_limits.tools_reset_at <= NOW() THEN
|
||||
UPDATE llm.llm_usage_limits
|
||||
SET tools_used = 0,
|
||||
tools_reset_at = NOW() + (
|
||||
CASE tools_reset_period
|
||||
WHEN 'hourly' THEN INTERVAL '1 hour'
|
||||
WHEN 'daily' THEN INTERVAL '1 day'
|
||||
WHEN 'weekly' THEN INTERVAL '1 week'
|
||||
WHEN 'monthly' THEN INTERVAL '1 month'
|
||||
ELSE INTERVAL '100 years'
|
||||
END
|
||||
)
|
||||
WHERE user_id = p_user_id;
|
||||
END IF;
|
||||
|
||||
-- Reset rate limits
|
||||
IF v_limits.minute_reset_at <= NOW() THEN
|
||||
UPDATE llm.llm_usage_limits
|
||||
SET current_minute_requests = 0,
|
||||
minute_reset_at = NOW() + INTERVAL '1 minute'
|
||||
WHERE user_id = p_user_id;
|
||||
END IF;
|
||||
|
||||
IF v_limits.hour_reset_at <= NOW() THEN
|
||||
UPDATE llm.llm_usage_limits
|
||||
SET current_hour_requests = 0,
|
||||
hour_reset_at = NOW() + INTERVAL '1 hour'
|
||||
WHERE user_id = p_user_id;
|
||||
END IF;
|
||||
|
||||
-- Retornar limites actualizados
|
||||
SELECT * INTO v_limits FROM llm.llm_usage_limits WHERE user_id = p_user_id;
|
||||
RETURN v_limits;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para verificar si puede enviar mensaje
|
||||
CREATE OR REPLACE FUNCTION llm.can_send_message(p_user_id UUID)
|
||||
RETURNS TABLE (
|
||||
allowed BOOLEAN,
|
||||
reason TEXT,
|
||||
messages_remaining INTEGER,
|
||||
tokens_remaining INTEGER
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_limits llm.llm_usage_limits;
|
||||
BEGIN
|
||||
v_limits := llm.check_and_reset_limits(p_user_id);
|
||||
|
||||
IF v_limits IS NULL THEN
|
||||
RETURN QUERY SELECT FALSE, 'User limits not configured'::TEXT, 0, 0;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_limits.is_suspended THEN
|
||||
RETURN QUERY SELECT FALSE, v_limits.suspension_reason, 0, 0;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF NOT v_limits.is_active THEN
|
||||
RETURN QUERY SELECT FALSE, 'Account inactive'::TEXT, 0, 0;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_limits.messages_used >= v_limits.messages_limit AND NOT v_limits.allow_overage THEN
|
||||
RETURN QUERY SELECT FALSE, 'Message limit reached'::TEXT,
|
||||
0, v_limits.tokens_limit - v_limits.tokens_used;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_limits.current_minute_requests >= v_limits.requests_per_minute THEN
|
||||
RETURN QUERY SELECT FALSE, 'Rate limit: too many requests per minute'::TEXT,
|
||||
v_limits.messages_limit - v_limits.messages_used,
|
||||
v_limits.tokens_limit - v_limits.tokens_used;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
RETURN QUERY SELECT TRUE, NULL::TEXT,
|
||||
v_limits.messages_limit - v_limits.messages_used,
|
||||
v_limits.tokens_limit - v_limits.tokens_used;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para incrementar uso
|
||||
CREATE OR REPLACE FUNCTION llm.increment_usage(
|
||||
p_user_id UUID,
|
||||
p_messages INTEGER DEFAULT 1,
|
||||
p_tokens INTEGER DEFAULT 0,
|
||||
p_tools INTEGER DEFAULT 0,
|
||||
p_cost_usd DECIMAL DEFAULT 0
|
||||
)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
UPDATE llm.llm_usage_limits
|
||||
SET messages_used = messages_used + p_messages,
|
||||
tokens_used = tokens_used + p_tokens,
|
||||
tools_used = tools_used + p_tools,
|
||||
monthly_spent_usd = monthly_spent_usd + p_cost_usd,
|
||||
current_minute_requests = current_minute_requests + 1,
|
||||
current_hour_requests = current_hour_requests + 1
|
||||
WHERE user_id = p_user_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de uso actual
|
||||
CREATE OR REPLACE VIEW llm.v_current_usage AS
|
||||
SELECT
|
||||
user_id,
|
||||
vip_level,
|
||||
messages_used,
|
||||
messages_limit,
|
||||
ROUND((messages_used::DECIMAL / NULLIF(messages_limit, 0) * 100), 1) AS messages_percent,
|
||||
tokens_used,
|
||||
tokens_limit,
|
||||
ROUND((tokens_used::DECIMAL / NULLIF(tokens_limit, 0) * 100), 1) AS tokens_percent,
|
||||
tools_used,
|
||||
tools_limit,
|
||||
monthly_spent_usd,
|
||||
monthly_budget_usd,
|
||||
messages_reset_at,
|
||||
tokens_reset_at,
|
||||
is_active,
|
||||
is_suspended
|
||||
FROM llm.llm_usage_limits;
|
||||
|
||||
-- Vista de usuarios cerca del limite
|
||||
CREATE OR REPLACE VIEW llm.v_users_near_limit AS
|
||||
SELECT
|
||||
user_id,
|
||||
vip_level,
|
||||
messages_used,
|
||||
messages_limit,
|
||||
ROUND((messages_used::DECIMAL / NULLIF(messages_limit, 0) * 100), 1) AS messages_percent,
|
||||
tokens_used,
|
||||
tokens_limit,
|
||||
ROUND((tokens_used::DECIMAL / NULLIF(tokens_limit, 0) * 100), 1) AS tokens_percent
|
||||
FROM llm.llm_usage_limits
|
||||
WHERE (messages_used::DECIMAL / NULLIF(messages_limit, 0) >= 0.8)
|
||||
OR (tokens_used::DECIMAL / NULLIF(tokens_limit, 0) >= 0.8)
|
||||
ORDER BY
|
||||
GREATEST(
|
||||
messages_used::DECIMAL / NULLIF(messages_limit, 0),
|
||||
tokens_used::DECIMAL / NULLIF(tokens_limit, 0)
|
||||
) DESC;
|
||||
|
||||
-- RLS Policies
|
||||
ALTER TABLE llm.llm_usage_limits ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY usage_limits_tenant ON llm.llm_usage_limits
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
CREATE POLICY usage_limits_user ON llm.llm_usage_limits
|
||||
FOR SELECT
|
||||
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE ON llm.llm_usage_limits TO trading_app;
|
||||
GRANT SELECT ON llm.llm_usage_limits TO trading_readonly;
|
||||
GRANT SELECT ON llm.v_current_usage TO trading_app;
|
||||
GRANT SELECT ON llm.v_users_near_limit TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.check_and_reset_limits TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.can_send_message TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION llm.increment_usage TO trading_app;
|
||||
Loading…
Reference in New Issue
Block a user