- 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>
359 lines
12 KiB
PL/PgSQL
359 lines
12 KiB
PL/PgSQL
-- ============================================================================
|
|
-- 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;
|