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