erp-core-database-v2/ddl/14-ai.sql
rckrdmrd 5043a640e4 refactor: Restructure DDL with numbered schema files
- Replace old DDL structure with new numbered files (01-24)
- Update migrations and seeds for new schema
- Clean up deprecated files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 00:40:32 -06:00

853 lines
29 KiB
PL/PgSQL

-- =============================================================
-- ARCHIVO: 14-ai.sql
-- DESCRIPCION: Sistema de AI/ML, prompts, completions, embeddings
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-10
-- EPIC: SAAS-AI (EPIC-SAAS-007)
-- HISTORIAS: US-080, US-081, US-082
-- =============================================================
-- =====================
-- SCHEMA: ai
-- =====================
CREATE SCHEMA IF NOT EXISTS ai;
-- =====================
-- EXTENSIÓN: pgvector para embeddings
-- =====================
-- CREATE EXTENSION IF NOT EXISTS vector;
-- =====================
-- TABLA: ai.models
-- Modelos de AI disponibles
-- =====================
CREATE TABLE IF NOT EXISTS ai.models (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Identificación
code VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
description TEXT,
-- Proveedor
provider VARCHAR(50) NOT NULL, -- openai, anthropic, google, azure, local
model_id VARCHAR(100) NOT NULL, -- gpt-4, claude-3, etc.
-- Tipo
model_type VARCHAR(30) NOT NULL, -- chat, completion, embedding, image, audio
-- Capacidades
max_tokens INTEGER,
supports_functions BOOLEAN DEFAULT FALSE,
supports_vision BOOLEAN DEFAULT FALSE,
supports_streaming BOOLEAN DEFAULT TRUE,
-- Costos (por 1K tokens)
input_cost_per_1k DECIMAL(10,6),
output_cost_per_1k DECIMAL(10,6),
-- Límites
rate_limit_rpm INTEGER, -- Requests per minute
rate_limit_tpm INTEGER, -- Tokens per minute
-- Estado
is_active BOOLEAN DEFAULT TRUE,
is_default BOOLEAN DEFAULT FALSE,
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- =====================
-- TABLA: ai.prompts
-- Biblioteca de prompts del sistema
-- =====================
CREATE TABLE IF NOT EXISTS ai.prompts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, -- NULL = global
-- Identificación
code VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
category VARCHAR(50), -- assistant, analysis, generation, extraction
-- Contenido
system_prompt TEXT,
user_prompt_template TEXT NOT NULL,
-- Variables: {{variable_name}}
-- Configuración del modelo
model_id UUID REFERENCES ai.models(id),
temperature DECIMAL(3,2) DEFAULT 0.7,
max_tokens INTEGER,
top_p DECIMAL(3,2),
frequency_penalty DECIMAL(3,2),
presence_penalty DECIMAL(3,2),
-- Variables requeridas
required_variables TEXT[] DEFAULT '{}',
variable_schema JSONB DEFAULT '{}',
-- Funciones (para function calling)
functions JSONB DEFAULT '[]',
-- Versionamiento
version INTEGER DEFAULT 1,
is_latest BOOLEAN DEFAULT TRUE,
parent_version_id UUID REFERENCES ai.prompts(id),
-- Estado
is_active BOOLEAN DEFAULT TRUE,
is_system BOOLEAN DEFAULT FALSE,
-- Estadísticas
usage_count INTEGER DEFAULT 0,
avg_tokens_used INTEGER,
avg_latency_ms INTEGER,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
UNIQUE(tenant_id, code, version)
);
-- =====================
-- TABLA: ai.conversations
-- Conversaciones con el asistente AI
-- =====================
CREATE TABLE IF NOT EXISTS ai.conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- Identificación
title VARCHAR(255),
summary TEXT,
-- Contexto
context_type VARCHAR(50), -- general, sales, inventory, support
context_data JSONB DEFAULT '{}',
-- Modelo usado
model_id UUID REFERENCES ai.models(id),
prompt_id UUID REFERENCES ai.prompts(id),
-- Estado
status VARCHAR(20) DEFAULT 'active', -- active, archived, deleted
is_pinned BOOLEAN DEFAULT FALSE,
-- Estadísticas
message_count INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
total_cost DECIMAL(10,4) DEFAULT 0,
-- Metadata
metadata JSONB DEFAULT '{}',
tags TEXT[] DEFAULT '{}',
-- Tiempos
last_message_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- =====================
-- TABLA: ai.messages
-- Mensajes en conversaciones
-- =====================
CREATE TABLE IF NOT EXISTS ai.messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES ai.conversations(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Mensaje
role VARCHAR(20) NOT NULL, -- system, user, assistant, function
content TEXT NOT NULL,
-- Función (si aplica)
function_name VARCHAR(100),
function_arguments JSONB,
function_result JSONB,
-- Modelo usado
model_id UUID REFERENCES ai.models(id),
model_response_id VARCHAR(255), -- ID de respuesta del proveedor
-- Tokens y costos
prompt_tokens INTEGER,
completion_tokens INTEGER,
total_tokens INTEGER,
cost DECIMAL(10,6),
-- Performance
latency_ms INTEGER,
finish_reason VARCHAR(30), -- stop, length, function_call, content_filter
-- Metadata
metadata JSONB DEFAULT '{}',
-- Feedback
feedback_rating INTEGER, -- 1-5
feedback_text TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- =====================
-- TABLA: ai.completions
-- Completaciones individuales (no conversacionales)
-- =====================
CREATE TABLE IF NOT EXISTS ai.completions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id),
-- Prompt usado
prompt_id UUID REFERENCES ai.prompts(id),
prompt_code VARCHAR(100),
-- Modelo
model_id UUID REFERENCES ai.models(id),
-- Input/Output
input_text TEXT NOT NULL,
input_variables JSONB DEFAULT '{}',
output_text TEXT,
-- Tokens y costos
prompt_tokens INTEGER,
completion_tokens INTEGER,
total_tokens INTEGER,
cost DECIMAL(10,6),
-- Performance
latency_ms INTEGER,
finish_reason VARCHAR(30),
-- Estado
status VARCHAR(20) DEFAULT 'pending', -- pending, processing, completed, failed
error_message TEXT,
-- Contexto
context_type VARCHAR(50),
context_id UUID,
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- =====================
-- TABLA: ai.embeddings
-- Embeddings vectoriales
-- =====================
CREATE TABLE IF NOT EXISTS ai.embeddings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Contenido original
content TEXT NOT NULL,
content_hash VARCHAR(64), -- SHA-256 para deduplicación
-- Vector
-- embedding vector(1536), -- Para OpenAI ada-002
embedding_json JSONB, -- Alternativa si no hay pgvector
-- Modelo usado
model_id UUID REFERENCES ai.models(id),
model_name VARCHAR(100),
dimensions INTEGER,
-- Asociación
entity_type VARCHAR(100), -- product, document, faq
entity_id UUID,
-- Metadata
metadata JSONB DEFAULT '{}',
tags TEXT[] DEFAULT '{}',
-- Chunks (si es parte de un documento grande)
chunk_index INTEGER,
chunk_total INTEGER,
parent_embedding_id UUID REFERENCES ai.embeddings(id),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- =====================
-- TABLA: ai.usage_logs
-- Log de uso de AI para billing
-- =====================
CREATE TABLE IF NOT EXISTS ai.usage_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id),
-- Modelo
model_id UUID REFERENCES ai.models(id),
model_name VARCHAR(100),
provider VARCHAR(50),
-- Tipo de uso
usage_type VARCHAR(30) NOT NULL, -- chat, completion, embedding, image
-- Tokens
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
-- Costos
cost DECIMAL(10,6) DEFAULT 0,
-- Contexto
conversation_id UUID,
completion_id UUID,
request_id VARCHAR(255),
-- Periodo
usage_date DATE DEFAULT CURRENT_DATE,
usage_month VARCHAR(7), -- 2026-01
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- =====================
-- TABLA: ai.tenant_quotas
-- Cuotas de AI por tenant
-- =====================
CREATE TABLE IF NOT EXISTS ai.tenant_quotas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Límites mensuales
monthly_token_limit INTEGER,
monthly_request_limit INTEGER,
monthly_cost_limit DECIMAL(10,2),
-- Uso actual del mes
current_tokens INTEGER DEFAULT 0,
current_requests INTEGER DEFAULT 0,
current_cost DECIMAL(10,4) DEFAULT 0,
-- Periodo
quota_month VARCHAR(7) NOT NULL, -- 2026-01
-- Estado
is_exceeded BOOLEAN DEFAULT FALSE,
exceeded_at TIMESTAMPTZ,
-- Alertas
alert_threshold_percent INTEGER DEFAULT 80,
alert_sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(tenant_id, quota_month)
);
-- =====================
-- TABLA: ai.knowledge_base
-- Base de conocimiento para RAG
-- =====================
CREATE TABLE IF NOT EXISTS ai.knowledge_base (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, -- NULL = global
-- Identificación
code VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
-- Fuente
source_type VARCHAR(30), -- manual, document, website, api
source_url TEXT,
source_file_id UUID,
-- Contenido
content TEXT NOT NULL,
content_type VARCHAR(50), -- faq, documentation, policy, procedure
-- Categorización
category VARCHAR(100),
subcategory VARCHAR(100),
tags TEXT[] DEFAULT '{}',
-- Embedding
embedding_id UUID REFERENCES ai.embeddings(id),
-- Relevancia
priority INTEGER DEFAULT 0,
relevance_score DECIMAL(5,4),
-- Estado
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
verified_by UUID REFERENCES auth.users(id),
verified_at TIMESTAMPTZ,
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
UNIQUE(tenant_id, code)
);
-- =====================
-- INDICES
-- =====================
-- Indices para models
CREATE INDEX IF NOT EXISTS idx_models_provider ON ai.models(provider);
CREATE INDEX IF NOT EXISTS idx_models_type ON ai.models(model_type);
CREATE INDEX IF NOT EXISTS idx_models_active ON ai.models(is_active) WHERE is_active = TRUE;
-- Indices para prompts
CREATE INDEX IF NOT EXISTS idx_prompts_tenant ON ai.prompts(tenant_id);
CREATE INDEX IF NOT EXISTS idx_prompts_code ON ai.prompts(code);
CREATE INDEX IF NOT EXISTS idx_prompts_category ON ai.prompts(category);
CREATE INDEX IF NOT EXISTS idx_prompts_active ON ai.prompts(is_active) WHERE is_active = TRUE;
-- Indices para conversations
CREATE INDEX IF NOT EXISTS idx_conversations_tenant ON ai.conversations(tenant_id);
CREATE INDEX IF NOT EXISTS idx_conversations_user ON ai.conversations(user_id);
CREATE INDEX IF NOT EXISTS idx_conversations_status ON ai.conversations(status);
CREATE INDEX IF NOT EXISTS idx_conversations_created ON ai.conversations(created_at DESC);
-- Indices para messages
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON ai.messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_messages_tenant ON ai.messages(tenant_id);
CREATE INDEX IF NOT EXISTS idx_messages_created ON ai.messages(created_at);
-- Indices para completions
CREATE INDEX IF NOT EXISTS idx_completions_tenant ON ai.completions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_completions_user ON ai.completions(user_id);
CREATE INDEX IF NOT EXISTS idx_completions_prompt ON ai.completions(prompt_id);
CREATE INDEX IF NOT EXISTS idx_completions_context ON ai.completions(context_type, context_id);
CREATE INDEX IF NOT EXISTS idx_completions_created ON ai.completions(created_at DESC);
-- Indices para embeddings
CREATE INDEX IF NOT EXISTS idx_embeddings_tenant ON ai.embeddings(tenant_id);
CREATE INDEX IF NOT EXISTS idx_embeddings_entity ON ai.embeddings(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_embeddings_hash ON ai.embeddings(content_hash);
CREATE INDEX IF NOT EXISTS idx_embeddings_tags ON ai.embeddings USING GIN(tags);
-- Indices para usage_logs
CREATE INDEX IF NOT EXISTS idx_usage_tenant ON ai.usage_logs(tenant_id);
CREATE INDEX IF NOT EXISTS idx_usage_date ON ai.usage_logs(usage_date);
CREATE INDEX IF NOT EXISTS idx_usage_month ON ai.usage_logs(usage_month);
CREATE INDEX IF NOT EXISTS idx_usage_model ON ai.usage_logs(model_id);
-- Indices para tenant_quotas
CREATE INDEX IF NOT EXISTS idx_quotas_tenant ON ai.tenant_quotas(tenant_id);
CREATE INDEX IF NOT EXISTS idx_quotas_month ON ai.tenant_quotas(quota_month);
-- Indices para knowledge_base
CREATE INDEX IF NOT EXISTS idx_kb_tenant ON ai.knowledge_base(tenant_id);
CREATE INDEX IF NOT EXISTS idx_kb_category ON ai.knowledge_base(category);
CREATE INDEX IF NOT EXISTS idx_kb_tags ON ai.knowledge_base USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_kb_active ON ai.knowledge_base(is_active) WHERE is_active = TRUE;
-- =====================
-- RLS POLICIES
-- =====================
-- Models son globales
ALTER TABLE ai.models ENABLE ROW LEVEL SECURITY;
CREATE POLICY public_read_models ON ai.models
FOR SELECT USING (is_active = TRUE);
-- Prompts: globales o por tenant
ALTER TABLE ai.prompts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_or_global_prompts ON ai.prompts
FOR SELECT USING (
tenant_id IS NULL
OR tenant_id = current_setting('app.current_tenant_id', true)::uuid
);
-- Conversations por tenant
ALTER TABLE ai.conversations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_conversations ON ai.conversations
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Messages por tenant
ALTER TABLE ai.messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_messages ON ai.messages
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Completions por tenant
ALTER TABLE ai.completions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_completions ON ai.completions
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Embeddings por tenant
ALTER TABLE ai.embeddings ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_embeddings ON ai.embeddings
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Usage logs por tenant
ALTER TABLE ai.usage_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_usage ON ai.usage_logs
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Quotas por tenant
ALTER TABLE ai.tenant_quotas ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_quotas ON ai.tenant_quotas
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- Knowledge base: global o por tenant
ALTER TABLE ai.knowledge_base ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_or_global_kb ON ai.knowledge_base
FOR SELECT USING (
tenant_id IS NULL
OR tenant_id = current_setting('app.current_tenant_id', true)::uuid
);
-- =====================
-- FUNCIONES
-- =====================
-- Función para crear conversación
CREATE OR REPLACE FUNCTION ai.create_conversation(
p_tenant_id UUID,
p_user_id UUID,
p_title VARCHAR(255) DEFAULT NULL,
p_context_type VARCHAR(50) DEFAULT 'general',
p_model_id UUID DEFAULT NULL
)
RETURNS UUID AS $$
DECLARE
v_conversation_id UUID;
BEGIN
INSERT INTO ai.conversations (
tenant_id, user_id, title, context_type, model_id
) VALUES (
p_tenant_id, p_user_id, p_title, p_context_type, p_model_id
) RETURNING id INTO v_conversation_id;
RETURN v_conversation_id;
END;
$$ LANGUAGE plpgsql;
-- Función para agregar mensaje a conversación
CREATE OR REPLACE FUNCTION ai.add_message(
p_conversation_id UUID,
p_role VARCHAR(20),
p_content TEXT,
p_model_id UUID DEFAULT NULL,
p_tokens JSONB DEFAULT NULL,
p_latency_ms INTEGER DEFAULT NULL
)
RETURNS UUID AS $$
DECLARE
v_message_id UUID;
v_tenant_id UUID;
v_prompt_tokens INTEGER;
v_completion_tokens INTEGER;
v_total_tokens INTEGER;
v_cost DECIMAL(10,6);
BEGIN
-- Obtener tenant de la conversación
SELECT tenant_id INTO v_tenant_id
FROM ai.conversations WHERE id = p_conversation_id;
-- Extraer tokens
v_prompt_tokens := (p_tokens->>'prompt_tokens')::INTEGER;
v_completion_tokens := (p_tokens->>'completion_tokens')::INTEGER;
v_total_tokens := COALESCE(v_prompt_tokens, 0) + COALESCE(v_completion_tokens, 0);
-- Calcular costo (si hay modelo)
IF p_model_id IS NOT NULL THEN
SELECT
(COALESCE(v_prompt_tokens, 0) * m.input_cost_per_1k / 1000) +
(COALESCE(v_completion_tokens, 0) * m.output_cost_per_1k / 1000)
INTO v_cost
FROM ai.models m WHERE m.id = p_model_id;
END IF;
-- Crear mensaje
INSERT INTO ai.messages (
conversation_id, tenant_id, role, content, model_id,
prompt_tokens, completion_tokens, total_tokens, cost, latency_ms
) VALUES (
p_conversation_id, v_tenant_id, p_role, p_content, p_model_id,
v_prompt_tokens, v_completion_tokens, v_total_tokens, v_cost, p_latency_ms
) RETURNING id INTO v_message_id;
-- Actualizar estadísticas de conversación
UPDATE ai.conversations
SET
message_count = message_count + 1,
total_tokens = total_tokens + COALESCE(v_total_tokens, 0),
total_cost = total_cost + COALESCE(v_cost, 0),
last_message_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = p_conversation_id;
-- Registrar uso
IF v_total_tokens > 0 THEN
PERFORM ai.log_usage(
v_tenant_id, NULL, p_model_id, 'chat',
v_prompt_tokens, v_completion_tokens, v_cost
);
END IF;
RETURN v_message_id;
END;
$$ LANGUAGE plpgsql;
-- Función para registrar uso
CREATE OR REPLACE FUNCTION ai.log_usage(
p_tenant_id UUID,
p_user_id UUID,
p_model_id UUID,
p_usage_type VARCHAR(30),
p_prompt_tokens INTEGER DEFAULT 0,
p_completion_tokens INTEGER DEFAULT 0,
p_cost DECIMAL(10,6) DEFAULT 0
)
RETURNS UUID AS $$
DECLARE
v_log_id UUID;
v_model_name VARCHAR(100);
v_provider VARCHAR(50);
v_current_month VARCHAR(7);
BEGIN
-- Obtener info del modelo
SELECT name, provider INTO v_model_name, v_provider
FROM ai.models WHERE id = p_model_id;
v_current_month := TO_CHAR(CURRENT_DATE, 'YYYY-MM');
-- Registrar uso
INSERT INTO ai.usage_logs (
tenant_id, user_id, model_id, model_name, provider,
usage_type, prompt_tokens, completion_tokens,
total_tokens, cost, usage_month
) VALUES (
p_tenant_id, p_user_id, p_model_id, v_model_name, v_provider,
p_usage_type, p_prompt_tokens, p_completion_tokens,
p_prompt_tokens + p_completion_tokens, p_cost, v_current_month
) RETURNING id INTO v_log_id;
-- Actualizar cuota del tenant
INSERT INTO ai.tenant_quotas (tenant_id, quota_month, current_tokens, current_requests, current_cost)
VALUES (p_tenant_id, v_current_month, p_prompt_tokens + p_completion_tokens, 1, p_cost)
ON CONFLICT (tenant_id, quota_month)
DO UPDATE SET
current_tokens = ai.tenant_quotas.current_tokens + p_prompt_tokens + p_completion_tokens,
current_requests = ai.tenant_quotas.current_requests + 1,
current_cost = ai.tenant_quotas.current_cost + p_cost,
updated_at = CURRENT_TIMESTAMP;
RETURN v_log_id;
END;
$$ LANGUAGE plpgsql;
-- Función para verificar cuota
CREATE OR REPLACE FUNCTION ai.check_quota(p_tenant_id UUID)
RETURNS TABLE (
has_quota BOOLEAN,
tokens_remaining INTEGER,
requests_remaining INTEGER,
cost_remaining DECIMAL,
percent_used INTEGER
) AS $$
DECLARE
v_quota RECORD;
BEGIN
SELECT * INTO v_quota
FROM ai.tenant_quotas
WHERE tenant_id = p_tenant_id
AND quota_month = TO_CHAR(CURRENT_DATE, 'YYYY-MM');
IF NOT FOUND THEN
-- Sin límites configurados
RETURN QUERY SELECT TRUE, NULL::INTEGER, NULL::INTEGER, NULL::DECIMAL, 0;
RETURN;
END IF;
RETURN QUERY SELECT
NOT v_quota.is_exceeded AND
(v_quota.monthly_token_limit IS NULL OR v_quota.current_tokens < v_quota.monthly_token_limit) AND
(v_quota.monthly_request_limit IS NULL OR v_quota.current_requests < v_quota.monthly_request_limit) AND
(v_quota.monthly_cost_limit IS NULL OR v_quota.current_cost < v_quota.monthly_cost_limit),
CASE WHEN v_quota.monthly_token_limit IS NOT NULL
THEN v_quota.monthly_token_limit - v_quota.current_tokens
ELSE NULL END,
CASE WHEN v_quota.monthly_request_limit IS NOT NULL
THEN v_quota.monthly_request_limit - v_quota.current_requests
ELSE NULL END,
CASE WHEN v_quota.monthly_cost_limit IS NOT NULL
THEN v_quota.monthly_cost_limit - v_quota.current_cost
ELSE NULL END,
CASE WHEN v_quota.monthly_token_limit IS NOT NULL
THEN (v_quota.current_tokens * 100 / v_quota.monthly_token_limit)::INTEGER
ELSE 0 END;
END;
$$ LANGUAGE plpgsql STABLE;
-- Función para obtener uso del tenant
CREATE OR REPLACE FUNCTION ai.get_tenant_usage(
p_tenant_id UUID,
p_month VARCHAR(7) DEFAULT NULL
)
RETURNS TABLE (
total_tokens BIGINT,
total_requests BIGINT,
total_cost DECIMAL,
usage_by_model JSONB,
usage_by_type JSONB,
daily_usage JSONB
) AS $$
DECLARE
v_month VARCHAR(7);
BEGIN
v_month := COALESCE(p_month, TO_CHAR(CURRENT_DATE, 'YYYY-MM'));
RETURN QUERY
SELECT
COALESCE(SUM(ul.total_tokens), 0)::BIGINT as total_tokens,
COUNT(*)::BIGINT as total_requests,
COALESCE(SUM(ul.cost), 0)::DECIMAL as total_cost,
jsonb_object_agg(
COALESCE(ul.model_name, 'unknown'),
model_tokens
) as usage_by_model,
jsonb_object_agg(ul.usage_type, type_tokens) as usage_by_type,
jsonb_object_agg(ul.usage_date::TEXT, day_tokens) as daily_usage
FROM ai.usage_logs ul
LEFT JOIN (
SELECT model_name, SUM(total_tokens) as model_tokens
FROM ai.usage_logs
WHERE tenant_id = p_tenant_id AND usage_month = v_month
GROUP BY model_name
) models ON models.model_name = ul.model_name
LEFT JOIN (
SELECT usage_type, SUM(total_tokens) as type_tokens
FROM ai.usage_logs
WHERE tenant_id = p_tenant_id AND usage_month = v_month
GROUP BY usage_type
) types ON types.usage_type = ul.usage_type
LEFT JOIN (
SELECT usage_date, SUM(total_tokens) as day_tokens
FROM ai.usage_logs
WHERE tenant_id = p_tenant_id AND usage_month = v_month
GROUP BY usage_date
) days ON days.usage_date = ul.usage_date
WHERE ul.tenant_id = p_tenant_id
AND ul.usage_month = v_month;
END;
$$ LANGUAGE plpgsql STABLE;
-- =====================
-- TRIGGERS
-- =====================
CREATE OR REPLACE FUNCTION ai.update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_models_updated_at
BEFORE UPDATE ON ai.models
FOR EACH ROW EXECUTE FUNCTION ai.update_timestamp();
CREATE TRIGGER trg_prompts_updated_at
BEFORE UPDATE ON ai.prompts
FOR EACH ROW EXECUTE FUNCTION ai.update_timestamp();
CREATE TRIGGER trg_conversations_updated_at
BEFORE UPDATE ON ai.conversations
FOR EACH ROW EXECUTE FUNCTION ai.update_timestamp();
CREATE TRIGGER trg_embeddings_updated_at
BEFORE UPDATE ON ai.embeddings
FOR EACH ROW EXECUTE FUNCTION ai.update_timestamp();
CREATE TRIGGER trg_kb_updated_at
BEFORE UPDATE ON ai.knowledge_base
FOR EACH ROW EXECUTE FUNCTION ai.update_timestamp();
-- =====================
-- SEED DATA: Modelos
-- =====================
INSERT INTO ai.models (code, name, provider, model_id, model_type, max_tokens, supports_functions, supports_vision, input_cost_per_1k, output_cost_per_1k) VALUES
('gpt-4o', 'GPT-4o', 'openai', 'gpt-4o', 'chat', 128000, TRUE, TRUE, 0.005, 0.015),
('gpt-4o-mini', 'GPT-4o Mini', 'openai', 'gpt-4o-mini', 'chat', 128000, TRUE, TRUE, 0.00015, 0.0006),
('gpt-4-turbo', 'GPT-4 Turbo', 'openai', 'gpt-4-turbo', 'chat', 128000, TRUE, TRUE, 0.01, 0.03),
('claude-3-opus', 'Claude 3 Opus', 'anthropic', 'claude-3-opus-20240229', 'chat', 200000, TRUE, TRUE, 0.015, 0.075),
('claude-3-sonnet', 'Claude 3 Sonnet', 'anthropic', 'claude-3-sonnet-20240229', 'chat', 200000, TRUE, TRUE, 0.003, 0.015),
('claude-3-haiku', 'Claude 3 Haiku', 'anthropic', 'claude-3-haiku-20240307', 'chat', 200000, TRUE, TRUE, 0.00025, 0.00125),
('text-embedding-3-small', 'Text Embedding 3 Small', 'openai', 'text-embedding-3-small', 'embedding', 8191, FALSE, FALSE, 0.00002, 0),
('text-embedding-3-large', 'Text Embedding 3 Large', 'openai', 'text-embedding-3-large', 'embedding', 8191, FALSE, FALSE, 0.00013, 0)
ON CONFLICT (code) DO NOTHING;
-- =====================
-- SEED DATA: Prompts del Sistema
-- =====================
INSERT INTO ai.prompts (code, name, category, system_prompt, user_prompt_template, required_variables, is_system) VALUES
('assistant_general', 'Asistente General', 'assistant',
'Eres un asistente virtual para un sistema ERP. Ayudas a los usuarios con consultas sobre ventas, inventario, facturación y gestión empresarial. Responde de forma clara y concisa en español.',
'{{user_message}}',
'{user_message}', TRUE),
('sales_analysis', 'Análisis de Ventas', 'analysis',
'Eres un analista de ventas experto. Analiza los datos proporcionados y genera insights accionables.',
'Analiza los siguientes datos de ventas:\n\n{{sales_data}}\n\nGenera un resumen ejecutivo con los principales hallazgos.',
'{sales_data}', TRUE),
('product_description', 'Generador de Descripción', 'generation',
'Eres un copywriter experto en productos. Genera descripciones atractivas y persuasivas.',
'Genera una descripción de producto para:\n\nNombre: {{product_name}}\nCategoría: {{category}}\nCaracterísticas: {{features}}\n\nLa descripción debe ser de {{word_count}} palabras aproximadamente.',
'{product_name,category,features,word_count}', TRUE),
('invoice_data_extraction', 'Extracción de Facturas', 'extraction',
'Eres un experto en extracción de datos de documentos fiscales mexicanos. Extrae la información estructurada de facturas.',
'Extrae los datos de la siguiente factura:\n\n{{invoice_text}}\n\nDevuelve los datos en formato JSON con los campos: rfc_emisor, rfc_receptor, fecha, total, conceptos.',
'{invoice_text}', TRUE),
('support_response', 'Respuesta de Soporte', 'assistant',
'Eres un agente de soporte técnico. Responde de forma amable y profesional, proporcionando soluciones claras.',
'El cliente tiene el siguiente problema:\n\n{{issue_description}}\n\nContexto adicional:\n{{context}}\n\nGenera una respuesta de soporte apropiada.',
'{issue_description,context}', TRUE)
ON CONFLICT (tenant_id, code, version) DO NOTHING;
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE ai.models IS 'Modelos de AI disponibles (OpenAI, Anthropic, etc.)';
COMMENT ON TABLE ai.prompts IS 'Biblioteca de prompts del sistema y personalizados';
COMMENT ON TABLE ai.conversations IS 'Conversaciones con el asistente AI';
COMMENT ON TABLE ai.messages IS 'Mensajes individuales en conversaciones';
COMMENT ON TABLE ai.completions IS 'Completaciones individuales (no conversacionales)';
COMMENT ON TABLE ai.embeddings IS 'Embeddings vectoriales para búsqueda semántica';
COMMENT ON TABLE ai.usage_logs IS 'Log de uso de AI para billing y analytics';
COMMENT ON TABLE ai.tenant_quotas IS 'Cuotas de uso de AI por tenant';
COMMENT ON TABLE ai.knowledge_base IS 'Base de conocimiento para RAG';
COMMENT ON FUNCTION ai.create_conversation IS 'Crea una nueva conversación con el asistente';
COMMENT ON FUNCTION ai.add_message IS 'Agrega un mensaje a una conversación';
COMMENT ON FUNCTION ai.log_usage IS 'Registra uso de AI para billing';
COMMENT ON FUNCTION ai.check_quota IS 'Verifica si el tenant tiene cuota disponible';