trading-platform-database-v2/ddl/schemas/llm/tables/003_llm_tools_usage.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

264 lines
8.1 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: llm
-- TABLE: llm_tools_usage
-- DESCRIPTION: Registro de uso de herramientas por el agente LLM
-- VERSION: 1.0.0
-- CREATED: 2026-01-16
-- SPRINT: Sprint 5 - DDL Implementation Roadmap Q1-2026
-- ============================================================================
-- Enum para categoria de herramienta
DO $$ BEGIN
CREATE TYPE llm.tool_category AS ENUM (
'market_data', -- Datos de mercado
'trading', -- Operaciones de trading
'portfolio', -- Gestion de portafolio
'analysis', -- Analisis tecnico/fundamental
'charts', -- Graficos
'alerts', -- Alertas
'education', -- Educacion
'news', -- Noticias
'calendar', -- Calendario economico
'calculations', -- Calculos financieros
'external_api', -- APIs externas
'system' -- Sistema
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Enum para estado de ejecucion
DO $$ BEGIN
CREATE TYPE llm.tool_execution_status AS ENUM (
'pending', -- Pendiente
'executing', -- Ejecutando
'success', -- Exitoso
'error', -- Error
'timeout', -- Timeout
'cancelled', -- Cancelado
'rate_limited', -- Rate limited
'permission_denied' -- Sin permisos
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tabla de Uso de Herramientas
CREATE TABLE IF NOT EXISTS llm.llm_tools_usage (
-- 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,
-- Referencias
conversation_id UUID REFERENCES llm.conversations(id) ON DELETE SET NULL,
message_id UUID REFERENCES llm.conversation_messages(id) ON DELETE SET NULL,
-- Herramienta
tool_name VARCHAR(100) NOT NULL,
tool_category llm.tool_category NOT NULL,
tool_version VARCHAR(20),
-- Ejecucion
status llm.tool_execution_status NOT NULL DEFAULT 'pending',
execution_time_ms INTEGER,
-- Input/Output
input_params JSONB NOT NULL DEFAULT '{}'::JSONB,
output_result JSONB,
output_size_bytes INTEGER,
-- Error
error_code VARCHAR(50),
error_message TEXT,
error_details JSONB,
-- Contexto
context_symbol VARCHAR(20),
context_timeframe VARCHAR(10),
context_data JSONB DEFAULT '{}'::JSONB,
-- Rate limiting
rate_limit_bucket VARCHAR(50),
rate_limit_remaining INTEGER,
-- Costo
cost_units DECIMAL(10, 4) DEFAULT 0, -- Unidades de costo (API calls, etc)
cost_usd DECIMAL(10, 6) DEFAULT 0, -- Costo en USD
-- Auditoria
ip_address INET,
user_agent TEXT,
-- Metadata
metadata JSONB DEFAULT '{}'::JSONB,
-- Timestamps
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE llm.llm_tools_usage IS
'Registro de todas las ejecuciones de herramientas por el agente LLM';
COMMENT ON COLUMN llm.llm_tools_usage.cost_units IS
'Unidades de costo internas (ej: 1 API call = 1 unit)';
-- Indices
CREATE INDEX IF NOT EXISTS idx_tools_usage_tenant
ON llm.llm_tools_usage(tenant_id);
CREATE INDEX IF NOT EXISTS idx_tools_usage_user
ON llm.llm_tools_usage(user_id);
CREATE INDEX IF NOT EXISTS idx_tools_usage_conversation
ON llm.llm_tools_usage(conversation_id)
WHERE conversation_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tools_usage_tool
ON llm.llm_tools_usage(tool_name, requested_at DESC);
CREATE INDEX IF NOT EXISTS idx_tools_usage_category
ON llm.llm_tools_usage(tool_category, requested_at DESC);
CREATE INDEX IF NOT EXISTS idx_tools_usage_status
ON llm.llm_tools_usage(status);
CREATE INDEX IF NOT EXISTS idx_tools_usage_errors
ON llm.llm_tools_usage(status, tool_name)
WHERE status IN ('error', 'timeout');
CREATE INDEX IF NOT EXISTS idx_tools_usage_date
ON llm.llm_tools_usage(requested_at DESC);
-- Indice BRIN para datos temporales
CREATE INDEX IF NOT EXISTS idx_tools_usage_brin
ON llm.llm_tools_usage USING BRIN (requested_at);
-- Funcion para registrar inicio de ejecucion
CREATE OR REPLACE FUNCTION llm.start_tool_execution(
p_tool_usage_id UUID
)
RETURNS VOID AS $$
BEGIN
UPDATE llm.llm_tools_usage
SET status = 'executing',
started_at = NOW()
WHERE id = p_tool_usage_id;
END;
$$ LANGUAGE plpgsql;
-- Funcion para registrar fin exitoso
CREATE OR REPLACE FUNCTION llm.complete_tool_execution(
p_tool_usage_id UUID,
p_output JSONB
)
RETURNS VOID AS $$
DECLARE
v_started_at TIMESTAMPTZ;
BEGIN
SELECT started_at INTO v_started_at
FROM llm.llm_tools_usage
WHERE id = p_tool_usage_id;
UPDATE llm.llm_tools_usage
SET status = 'success',
completed_at = NOW(),
execution_time_ms = EXTRACT(EPOCH FROM (NOW() - v_started_at)) * 1000,
output_result = p_output,
output_size_bytes = LENGTH(p_output::TEXT)
WHERE id = p_tool_usage_id;
END;
$$ LANGUAGE plpgsql;
-- Funcion para registrar error
CREATE OR REPLACE FUNCTION llm.fail_tool_execution(
p_tool_usage_id UUID,
p_error_code VARCHAR,
p_error_message TEXT,
p_error_details JSONB DEFAULT NULL
)
RETURNS VOID AS $$
BEGIN
UPDATE llm.llm_tools_usage
SET status = 'error',
completed_at = NOW(),
error_code = p_error_code,
error_message = p_error_message,
error_details = p_error_details
WHERE id = p_tool_usage_id;
END;
$$ LANGUAGE plpgsql;
-- Vista de estadisticas de herramientas
CREATE OR REPLACE VIEW llm.v_tool_usage_stats AS
SELECT
tool_name,
tool_category,
COUNT(*) AS total_calls,
COUNT(*) FILTER (WHERE status = 'success') AS successful_calls,
COUNT(*) FILTER (WHERE status = 'error') AS failed_calls,
ROUND((COUNT(*) FILTER (WHERE status = 'success')::DECIMAL
/ NULLIF(COUNT(*), 0) * 100), 2) AS success_rate,
ROUND(AVG(execution_time_ms)::NUMERIC, 2) AS avg_execution_ms,
ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY execution_time_ms)::NUMERIC, 2) AS p95_execution_ms,
SUM(cost_units) AS total_cost_units,
SUM(cost_usd) AS total_cost_usd
FROM llm.llm_tools_usage
WHERE completed_at IS NOT NULL
GROUP BY tool_name, tool_category
ORDER BY total_calls DESC;
-- Vista de errores recientes
CREATE OR REPLACE VIEW llm.v_tool_errors AS
SELECT
id,
user_id,
tool_name,
tool_category,
error_code,
error_message,
input_params,
requested_at
FROM llm.llm_tools_usage
WHERE status IN ('error', 'timeout')
ORDER BY requested_at DESC
LIMIT 100;
-- Vista de uso por usuario
CREATE OR REPLACE VIEW llm.v_user_tool_usage AS
SELECT
user_id,
tool_category,
COUNT(*) AS total_calls,
SUM(cost_units) AS total_cost_units,
SUM(cost_usd) AS total_cost_usd,
MAX(requested_at) AS last_used_at
FROM llm.llm_tools_usage
WHERE requested_at >= NOW() - INTERVAL '30 days'
GROUP BY user_id, tool_category
ORDER BY user_id, total_calls DESC;
-- RLS Policies
ALTER TABLE llm.llm_tools_usage ENABLE ROW LEVEL SECURITY;
CREATE POLICY tools_usage_tenant_isolation ON llm.llm_tools_usage
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY tools_usage_user_isolation ON llm.llm_tools_usage
FOR SELECT
USING (user_id = current_setting('app.current_user_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE ON llm.llm_tools_usage TO trading_app;
GRANT SELECT ON llm.llm_tools_usage TO trading_readonly;
GRANT SELECT ON llm.v_tool_usage_stats TO trading_app;
GRANT SELECT ON llm.v_tool_errors TO trading_app;
GRANT SELECT ON llm.v_user_tool_usage TO trading_app;
GRANT EXECUTE ON FUNCTION llm.start_tool_execution TO trading_app;
GRANT EXECUTE ON FUNCTION llm.complete_tool_execution TO trading_app;
GRANT EXECUTE ON FUNCTION llm.fail_tool_execution TO trading_app;