[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