# ET-LLM-005: Arquitectura del Sistema de Tools

**Épica:** OQI-007 - LLM Strategy Agent
**Versión:** 1.0
**Fecha:** 2025-12-05
**Estado:** Planificado
**Prioridad:** P0 - Crítico

---

## Resumen

Esta especificación define la arquitectura del sistema de tools (herramientas) que el agente LLM puede invocar para obtener información y ejecutar acciones en la plataforma.

---

## Arquitectura General

```
┌─────────────────────────────────────────────────────────────────────────┐
│                           TOOLS SYSTEM                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                     Tool Registry                                  │  │
│  │                                                                    │  │
│  │  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐  │  │
│  │  │   Market    │ │  Portfolio  │ │    News     │ │     ML      │  │  │
│  │  │   Tools     │ │   Tools     │ │   Tools     │ │   Tools     │  │  │
│  │  └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘  │  │
│  │                                                                    │  │
│  │  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐  │  │
│  │  │   Trading   │ │   Alert     │ │  Calculate  │ │  Education  │  │  │
│  │  │   Tools     │ │   Tools     │ │   Tools     │ │   Tools     │  │  │
│  │  └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘  │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│                                   │                                      │
│                                   ↓                                      │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                     Tool Executor                                  │  │
│  │                                                                    │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐ │  │
│  │  │  Permission  │  │    Rate      │  │      Execution           │ │  │
│  │  │   Checker    │  │   Limiter    │  │      Engine              │ │  │
│  │  └──────────────┘  └──────────────┘  └──────────────────────────┘ │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│                                   │                                      │
│                                   ↓                                      │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                     Service Adapters                               │  │
│  │                                                                    │  │
│  │  MarketData │ Portfolio │ News │ ML │ Trading │ Alerts │ ...     │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

---

## Tool Registry

### Estructura de Registro

```typescript
// src/modules/copilot/tools/tool-registry.ts

interface ToolDefinition {
  id: string;
  name: string;
  description: string;
  category: ToolCategory;
  requiredPlan: 'free' | 'pro' | 'premium';
  rateLimit: RateLimitConfig;
  parameters: ToolParameters;
  handler: ToolHandler;
  validator?: ToolValidator;
  postProcessor?: ToolPostProcessor;
}

type ToolCategory =
  | 'market'
  | 'portfolio'
  | 'news'
  | 'ml'
  | 'trading'
  | 'alerts'
  | 'calculate'
  | 'education';

interface RateLimitConfig {
  requestsPerMinute: number;
  requestsPerHour?: number;
  requestsPerDay?: number;
}

@Injectable()
export class ToolRegistry {
  private tools: Map<string, ToolDefinition> = new Map();

  constructor(
    private readonly marketTools: MarketToolsProvider,
    private readonly portfolioTools: PortfolioToolsProvider,
    private readonly newsTools: NewsToolsProvider,
    private readonly mlTools: MLToolsProvider,
    private readonly tradingTools: TradingToolsProvider,
    private readonly alertTools: AlertToolsProvider,
    private readonly calculateTools: CalculateToolsProvider,
    private readonly educationTools: EducationToolsProvider,
  ) {
    this.registerAllTools();
  }

  private registerAllTools(): void {
    // Register market tools
    this.marketTools.getTools().forEach(tool => this.register(tool));
    // Register portfolio tools
    this.portfolioTools.getTools().forEach(tool => this.register(tool));
    // Register all other tools...
    this.newsTools.getTools().forEach(tool => this.register(tool));
    this.mlTools.getTools().forEach(tool => this.register(tool));
    this.tradingTools.getTools().forEach(tool => this.register(tool));
    this.alertTools.getTools().forEach(tool => this.register(tool));
    this.calculateTools.getTools().forEach(tool => this.register(tool));
    this.educationTools.getTools().forEach(tool => this.register(tool));
  }

  register(tool: ToolDefinition): void {
    if (this.tools.has(tool.id)) {
      throw new Error(`Tool ${tool.id} already registered`);
    }
    this.tools.set(tool.id, tool);
  }

  get(toolId: string): ToolDefinition | undefined {
    return this.tools.get(toolId);
  }

  getByCategory(category: ToolCategory): ToolDefinition[] {
    return Array.from(this.tools.values()).filter(t => t.category === category);
  }

  getAvailableForPlan(plan: string): ToolDefinition[] {
    const planHierarchy = { free: 0, pro: 1, premium: 2 };
    const userPlanLevel = planHierarchy[plan] || 0;

    return Array.from(this.tools.values()).filter(tool => {
      const toolPlanLevel = planHierarchy[tool.requiredPlan] || 0;
      return toolPlanLevel <= userPlanLevel;
    });
  }

  getOpenAIToolDefinitions(plan: string): OpenAITool[] {
    return this.getAvailableForPlan(plan).map(tool => ({
      type: 'function',
      function: {
        name: tool.name,
        description: tool.description,
        parameters: tool.parameters,
      },
    }));
  }
}
```

### Catálogo Completo de Tools

```typescript
// src/modules/copilot/tools/catalog.ts

export const TOOL_CATALOG: ToolDefinition[] = [
  // ============================================
  // MARKET TOOLS
  // ============================================
  {
    id: 'market.get_price',
    name: 'get_price',
    description: 'Obtiene el precio actual de un símbolo con cambio 24h y volumen',
    category: 'market',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 60 },
    parameters: {
      type: 'object',
      properties: {
        symbol: {
          type: 'string',
          description: 'Símbolo del activo (ej: AAPL, BTC/USD, ETH/USD)',
        },
      },
      required: ['symbol'],
    },
    handler: 'marketService.getPrice',
  },
  {
    id: 'market.get_ohlcv',
    name: 'get_ohlcv',
    description: 'Obtiene datos históricos OHLCV (velas) de un símbolo',
    category: 'market',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 30 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string', description: 'Símbolo del activo' },
        timeframe: {
          type: 'string',
          enum: ['1m', '5m', '15m', '1h', '4h', '1d', '1w'],
          description: 'Intervalo temporal de las velas',
        },
        limit: {
          type: 'number',
          description: 'Número de velas (máximo 500)',
          default: 100,
        },
      },
      required: ['symbol', 'timeframe'],
    },
    handler: 'marketService.getOHLCV',
  },
  {
    id: 'market.get_indicators',
    name: 'get_indicators',
    description: 'Calcula indicadores técnicos para un símbolo',
    category: 'market',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 30 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string' },
        indicators: {
          type: 'array',
          items: {
            type: 'string',
            enum: ['RSI', 'MACD', 'BB', 'SMA', 'EMA', 'ATR', 'VWAP', 'OBV', 'ADX'],
          },
        },
        timeframe: {
          type: 'string',
          enum: ['1h', '4h', '1d'],
          default: '1d',
        },
      },
      required: ['symbol', 'indicators'],
    },
    handler: 'marketService.getIndicators',
  },
  {
    id: 'market.get_fundamentals',
    name: 'get_fundamentals',
    description: 'Obtiene datos fundamentales de una acción (P/E, revenue, etc.)',
    category: 'market',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 20 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string', description: 'Símbolo de la acción' },
      },
      required: ['symbol'],
    },
    handler: 'marketService.getFundamentals',
  },

  // ============================================
  // NEWS TOOLS
  // ============================================
  {
    id: 'news.get_news',
    name: 'get_news',
    description: 'Obtiene noticias recientes con análisis de sentimiento',
    category: 'news',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 10 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string', description: 'Símbolo para buscar noticias' },
        limit: { type: 'number', default: 5, description: 'Número de noticias' },
      },
      required: ['symbol'],
    },
    handler: 'newsService.getBySymbol',
  },
  {
    id: 'news.get_sentiment',
    name: 'get_sentiment',
    description: 'Obtiene análisis de sentimiento agregado de múltiples fuentes',
    category: 'news',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 20 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string' },
      },
      required: ['symbol'],
    },
    handler: 'newsService.getSentiment',
  },

  // ============================================
  // ML TOOLS (Pro/Premium)
  // ============================================
  {
    id: 'ml.get_prediction',
    name: 'get_ml_signals',
    description: 'Obtiene predicciones del modelo ML con nivel de confianza',
    category: 'ml',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 30 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string' },
      },
      required: ['symbol'],
    },
    handler: 'mlService.getPrediction',
  },
  {
    id: 'ml.get_features',
    name: 'get_ml_features',
    description: 'Obtiene las features principales usadas en la predicción',
    category: 'ml',
    requiredPlan: 'premium',
    rateLimit: { requestsPerMinute: 20 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string' },
        prediction_id: { type: 'string', description: 'ID de predicción previa' },
      },
      required: ['symbol'],
    },
    handler: 'mlService.getFeatureImportance',
  },

  // ============================================
  // PORTFOLIO TOOLS (Pro/Premium)
  // ============================================
  {
    id: 'portfolio.get_positions',
    name: 'get_portfolio',
    description: 'Obtiene las posiciones actuales del usuario',
    category: 'portfolio',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 30 },
    parameters: {
      type: 'object',
      properties: {},
    },
    handler: 'portfolioService.getPositions',
  },
  {
    id: 'portfolio.get_history',
    name: 'get_trade_history',
    description: 'Obtiene el historial de trades del usuario',
    category: 'portfolio',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 20 },
    parameters: {
      type: 'object',
      properties: {
        limit: { type: 'number', default: 20 },
        symbol: { type: 'string', description: 'Filtrar por símbolo (opcional)' },
      },
    },
    handler: 'portfolioService.getHistory',
  },

  // ============================================
  // TRADING TOOLS (Pro/Premium)
  // ============================================
  {
    id: 'trading.create_paper_order',
    name: 'create_paper_order',
    description: 'Crea una orden de paper trading (requiere confirmación)',
    category: 'trading',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 10 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string' },
        side: { type: 'string', enum: ['buy', 'sell'] },
        quantity: { type: 'number' },
        order_type: {
          type: 'string',
          enum: ['market', 'limit', 'stop', 'stop_limit'],
        },
        limit_price: { type: 'number', description: 'Precio límite (para limit orders)' },
        stop_price: { type: 'number', description: 'Precio stop (para stop orders)' },
      },
      required: ['symbol', 'side', 'quantity', 'order_type'],
    },
    handler: 'tradingService.createPaperOrder',
    validator: 'tradingValidator.validateOrder',
    postProcessor: 'tradingService.requireConfirmation',
  },
  {
    id: 'trading.cancel_paper_order',
    name: 'cancel_paper_order',
    description: 'Cancela una orden de paper trading pendiente',
    category: 'trading',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 20 },
    parameters: {
      type: 'object',
      properties: {
        order_id: { type: 'string' },
      },
      required: ['order_id'],
    },
    handler: 'tradingService.cancelPaperOrder',
  },
  {
    id: 'trading.get_pending_orders',
    name: 'get_pending_orders',
    description: 'Obtiene las órdenes pendientes del usuario',
    category: 'trading',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 30 },
    parameters: {
      type: 'object',
      properties: {},
    },
    handler: 'tradingService.getPendingOrders',
  },

  // ============================================
  // ALERT TOOLS
  // ============================================
  {
    id: 'alert.create',
    name: 'create_alert',
    description: 'Crea una alerta de precio',
    category: 'alerts',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 20 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string' },
        condition: { type: 'string', enum: ['>=', '<=', '=='] },
        price: { type: 'number' },
        message: { type: 'string', description: 'Mensaje opcional' },
      },
      required: ['symbol', 'condition', 'price'],
    },
    handler: 'alertService.create',
  },
  {
    id: 'alert.list',
    name: 'list_alerts',
    description: 'Lista las alertas activas del usuario',
    category: 'alerts',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 30 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string', description: 'Filtrar por símbolo (opcional)' },
      },
    },
    handler: 'alertService.list',
  },
  {
    id: 'alert.delete',
    name: 'delete_alert',
    description: 'Elimina una alerta',
    category: 'alerts',
    requiredPlan: 'pro',
    rateLimit: { requestsPerMinute: 20 },
    parameters: {
      type: 'object',
      properties: {
        alert_id: { type: 'string' },
      },
      required: ['alert_id'],
    },
    handler: 'alertService.delete',
  },

  // ============================================
  // CALCULATE TOOLS
  // ============================================
  {
    id: 'calculate.position_size',
    name: 'calculate_position_size',
    description: 'Calcula el tamaño de posición basado en riesgo',
    category: 'calculate',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 100 },
    parameters: {
      type: 'object',
      properties: {
        capital: { type: 'number', description: 'Capital disponible' },
        entry_price: { type: 'number' },
        stop_loss_price: { type: 'number' },
        risk_percent: { type: 'number', description: 'Porcentaje de riesgo (ej: 0.02 para 2%)' },
      },
      required: ['capital', 'entry_price', 'stop_loss_price', 'risk_percent'],
    },
    handler: 'calculateService.positionSize',
  },
  {
    id: 'calculate.risk_reward',
    name: 'calculate_risk_reward',
    description: 'Calcula el ratio riesgo/beneficio de una operación',
    category: 'calculate',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 100 },
    parameters: {
      type: 'object',
      properties: {
        entry_price: { type: 'number' },
        stop_loss_price: { type: 'number' },
        take_profit_price: { type: 'number' },
      },
      required: ['entry_price', 'stop_loss_price', 'take_profit_price'],
    },
    handler: 'calculateService.riskReward',
  },

  // ============================================
  // WATCHLIST TOOLS
  // ============================================
  {
    id: 'watchlist.get',
    name: 'get_watchlist',
    description: 'Obtiene la watchlist del usuario',
    category: 'market',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 60 },
    parameters: {
      type: 'object',
      properties: {},
    },
    handler: 'watchlistService.get',
  },
  {
    id: 'watchlist.add',
    name: 'add_to_watchlist',
    description: 'Agrega un símbolo a la watchlist',
    category: 'market',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 30 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string' },
      },
      required: ['symbol'],
    },
    handler: 'watchlistService.add',
  },
  {
    id: 'watchlist.remove',
    name: 'remove_from_watchlist',
    description: 'Elimina un símbolo de la watchlist',
    category: 'market',
    requiredPlan: 'free',
    rateLimit: { requestsPerMinute: 30 },
    parameters: {
      type: 'object',
      properties: {
        symbol: { type: 'string' },
      },
      required: ['symbol'],
    },
    handler: 'watchlistService.remove',
  },
];
```

---

## Tool Executor

```typescript
// src/modules/copilot/tools/tool-executor.ts

interface ToolExecutionContext {
  userId: string;
  userPlan: string;
  conversationId: string;
  toolCallId: string;
}

interface ToolExecutionResult {
  success: boolean;
  data?: any;
  error?: {
    code: string;
    message: string;
  };
  metadata: {
    executionTime: number;
    cached: boolean;
  };
}

@Injectable()
export class ToolExecutor {
  constructor(
    private readonly registry: ToolRegistry,
    private readonly permissionChecker: PermissionChecker,
    private readonly rateLimiter: RateLimiter,
    private readonly cache: CacheService,
    private readonly logger: LoggerService,
    // Service adapters
    private readonly marketService: MarketService,
    private readonly portfolioService: PortfolioService,
    private readonly newsService: NewsService,
    private readonly mlService: MLService,
    private readonly tradingService: TradingService,
    private readonly alertService: AlertService,
    private readonly calculateService: CalculateService,
    private readonly watchlistService: WatchlistService,
  ) {}

  async execute(
    toolName: string,
    parameters: Record<string, any>,
    context: ToolExecutionContext,
  ): Promise<ToolExecutionResult> {
    const startTime = Date.now();

    // 1. Get tool definition
    const tool = this.registry.get(toolName);
    if (!tool) {
      return {
        success: false,
        error: { code: 'TOOL_NOT_FOUND', message: `Tool ${toolName} not found` },
        metadata: { executionTime: 0, cached: false },
      };
    }

    // 2. Check permissions
    const hasPermission = await this.permissionChecker.check(
      context.userId,
      context.userPlan,
      tool.requiredPlan,
    );

    if (!hasPermission) {
      return {
        success: false,
        error: {
          code: 'PLAN_REQUIRED',
          message: `Tool ${toolName} requires ${tool.requiredPlan} plan`,
        },
        metadata: { executionTime: Date.now() - startTime, cached: false },
      };
    }

    // 3. Check rate limit
    const rateLimitOk = await this.rateLimiter.check(
      context.userId,
      tool.id,
      tool.rateLimit,
    );

    if (!rateLimitOk) {
      return {
        success: false,
        error: {
          code: 'RATE_LIMIT',
          message: `Rate limit exceeded for ${toolName}`,
        },
        metadata: { executionTime: Date.now() - startTime, cached: false },
      };
    }

    // 4. Validate parameters
    if (tool.validator) {
      const validationResult = await this.validateParameters(
        tool.validator,
        parameters,
        context,
      );
      if (!validationResult.valid) {
        return {
          success: false,
          error: {
            code: 'VALIDATION_ERROR',
            message: validationResult.message,
          },
          metadata: { executionTime: Date.now() - startTime, cached: false },
        };
      }
    }

    // 5. Check cache (for read-only tools)
    if (this.isCacheable(tool)) {
      const cacheKey = this.buildCacheKey(tool.id, parameters, context.userId);
      const cached = await this.cache.get(cacheKey);
      if (cached) {
        return {
          success: true,
          data: cached,
          metadata: { executionTime: Date.now() - startTime, cached: true },
        };
      }
    }

    // 6. Execute tool
    try {
      const result = await this.executeHandler(tool, parameters, context);

      // 7. Post-process if needed
      let finalResult = result;
      if (tool.postProcessor) {
        finalResult = await this.postProcess(tool.postProcessor, result, context);
      }

      // 8. Cache result if applicable
      if (this.isCacheable(tool)) {
        const cacheKey = this.buildCacheKey(tool.id, parameters, context.userId);
        await this.cache.set(cacheKey, finalResult, this.getCacheTTL(tool));
      }

      // 9. Log execution
      await this.logger.logToolExecution({
        userId: context.userId,
        toolId: tool.id,
        parameters,
        success: true,
        executionTime: Date.now() - startTime,
      });

      return {
        success: true,
        data: finalResult,
        metadata: { executionTime: Date.now() - startTime, cached: false },
      };
    } catch (error) {
      await this.logger.logToolExecution({
        userId: context.userId,
        toolId: tool.id,
        parameters,
        success: false,
        error: error.message,
        executionTime: Date.now() - startTime,
      });

      return {
        success: false,
        error: {
          code: 'EXECUTION_ERROR',
          message: error.message,
        },
        metadata: { executionTime: Date.now() - startTime, cached: false },
      };
    }
  }

  private async executeHandler(
    tool: ToolDefinition,
    parameters: Record<string, any>,
    context: ToolExecutionContext,
  ): Promise<any> {
    const [serviceName, methodName] = tool.handler.split('.');
    const service = this.getService(serviceName);

    if (!service || !service[methodName]) {
      throw new Error(`Handler ${tool.handler} not found`);
    }

    // Add context to parameters for handlers that need it
    const enrichedParams = {
      ...parameters,
      _context: {
        userId: context.userId,
        userPlan: context.userPlan,
      },
    };

    return service[methodName](enrichedParams);
  }

  private getService(serviceName: string): any {
    const services: Record<string, any> = {
      marketService: this.marketService,
      portfolioService: this.portfolioService,
      newsService: this.newsService,
      mlService: this.mlService,
      tradingService: this.tradingService,
      alertService: this.alertService,
      calculateService: this.calculateService,
      watchlistService: this.watchlistService,
    };
    return services[serviceName];
  }

  private isCacheable(tool: ToolDefinition): boolean {
    const nonCacheableCategories = ['trading', 'alerts'];
    return !nonCacheableCategories.includes(tool.category);
  }

  private getCacheTTL(tool: ToolDefinition): number {
    const ttls: Record<string, number> = {
      market: 5,      // 5 seconds
      news: 300,      // 5 minutes
      ml: 60,         // 1 minute
      portfolio: 10,  // 10 seconds
      calculate: 0,   // No cache (pure function)
    };
    return ttls[tool.category] || 30;
  }

  private buildCacheKey(
    toolId: string,
    params: Record<string, any>,
    userId: string,
  ): string {
    const paramsHash = JSON.stringify(params);
    return `tool:${toolId}:${userId}:${paramsHash}`;
  }
}
```

---

## Rate Limiter

```typescript
// src/modules/copilot/tools/rate-limiter.ts

@Injectable()
export class RateLimiter {
  constructor(private readonly redis: RedisService) {}

  async check(
    userId: string,
    toolId: string,
    config: RateLimitConfig,
  ): Promise<boolean> {
    const now = Date.now();
    const minute = Math.floor(now / 60000);
    const hour = Math.floor(now / 3600000);
    const day = Math.floor(now / 86400000);

    // Check per-minute limit
    if (config.requestsPerMinute) {
      const key = `ratelimit:${toolId}:${userId}:min:${minute}`;
      const count = await this.redis.incr(key);
      if (count === 1) {
        await this.redis.expire(key, 60);
      }
      if (count > config.requestsPerMinute) {
        return false;
      }
    }

    // Check per-hour limit
    if (config.requestsPerHour) {
      const key = `ratelimit:${toolId}:${userId}:hour:${hour}`;
      const count = await this.redis.incr(key);
      if (count === 1) {
        await this.redis.expire(key, 3600);
      }
      if (count > config.requestsPerHour) {
        return false;
      }
    }

    // Check per-day limit
    if (config.requestsPerDay) {
      const key = `ratelimit:${toolId}:${userId}:day:${day}`;
      const count = await this.redis.incr(key);
      if (count === 1) {
        await this.redis.expire(key, 86400);
      }
      if (count > config.requestsPerDay) {
        return false;
      }
    }

    return true;
  }

  async getRemainingQuota(
    userId: string,
    toolId: string,
    config: RateLimitConfig,
  ): Promise<RemainingQuota> {
    const now = Date.now();
    const minute = Math.floor(now / 60000);

    const key = `ratelimit:${toolId}:${userId}:min:${minute}`;
    const used = parseInt(await this.redis.get(key) || '0', 10);

    return {
      remaining: Math.max(0, config.requestsPerMinute - used),
      resetAt: (minute + 1) * 60000,
    };
  }
}
```

---

## Permission Checker

```typescript
// src/modules/copilot/tools/permission-checker.ts

@Injectable()
export class PermissionChecker {
  private readonly planHierarchy = {
    free: 0,
    pro: 1,
    premium: 2,
  };

  async check(
    userId: string,
    userPlan: string,
    requiredPlan: string,
  ): Promise<boolean> {
    const userLevel = this.planHierarchy[userPlan] ?? 0;
    const requiredLevel = this.planHierarchy[requiredPlan] ?? 0;

    return userLevel >= requiredLevel;
  }

  getUpgradeMessage(currentPlan: string, requiredPlan: string): string {
    if (currentPlan === 'free' && requiredPlan === 'pro') {
      return 'Esta función requiere el plan Pro. Actualiza para acceder a señales ML, paper trading y más.';
    }
    if (requiredPlan === 'premium') {
      return 'Esta función requiere el plan Premium. Actualiza para acceso completo a todas las funcionalidades.';
    }
    return `Esta función requiere el plan ${requiredPlan}.`;
  }
}
```

---

## Diagrama de Flujo de Ejecución

```
┌─────────────────────────────────────────────────────────────┐
│ LLM genera tool_call: get_ml_signals({ symbol: "AAPL" })    │
└──────────────────────────┬──────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│ 1. Tool Registry: Buscar definición de "get_ml_signals"     │
│    → Encontrado: ml.get_prediction                          │
└──────────────────────────┬──────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. Permission Checker: ¿Usuario tiene plan Pro+?            │
│    → User plan: "pro" >= required: "pro" ✓                  │
└──────────────────────────┬──────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Rate Limiter: ¿Dentro del límite (30/min)?               │
│    → Requests this minute: 5 < 30 ✓                         │
└──────────────────────────┬──────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Cache Check: ¿Resultado cacheado?                        │
│    → Cache miss (TTL expirado)                              │
└──────────────────────────┬──────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Execute Handler: mlService.getPrediction({ symbol })     │
│    → Llamada al ML Engine                                   │
│    → Response: { prediction: "bullish", confidence: 0.72 }  │
└──────────────────────────┬──────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│ 6. Cache Store: Guardar resultado (TTL: 60s)                │
└──────────────────────────┬──────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│ 7. Log Execution: Registrar uso de tool                     │
└──────────────────────────┬──────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│ 8. Return to LLM: { success: true, data: {...} }            │
└─────────────────────────────────────────────────────────────┘
```

---

## Dependencias

### Servicios Internos
- MarketService (OQI-003)
- PortfolioService (OQI-004)
- MLService (OQI-006)
- AlertService (OQI-003)
- TradingService (OQI-003)

### Infraestructura
- Redis (rate limiting, cache)
- PostgreSQL (logs de ejecución)

---

## Referencias

- [RF-LLM-005: Tool Integration](../requerimientos/RF-LLM-005-tool-integration.md)
- [ET-LLM-002: Agente de Análisis](./ET-LLM-002-agente-analisis.md)

---

*Especificación técnica - Sistema NEXUS*
*OrbiQuant IA Trading Platform*
