trading-platform-database-v2/ddl/schemas/llm/tables/002_conversation_messages.sql
rckrdmrd 62c811be45 [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>
2026-01-16 20:13:33 -06:00

249 lines
8.0 KiB
PL/PgSQL

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