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