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