ML Engine Updates: - Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records - Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence) - Backtest results: +176.71R profit with aggressive_filter strategy Documentation Consolidation: - Created docs/99-analisis/_MAP.md index with 13 new analysis documents - Consolidated inventories: removed duplicates from orchestration/inventarios/ - Updated ML_INVENTORY.yml with BTCUSD metrics and training results - Added execution reports: FASE11-BTCUSD, correction issues, alignment validation Architecture & Integration: - Updated all module documentation with NEXUS v3.4 frontmatter - Fixed _MAP.md indexes across all folders - Updated orchestration plans and traces Files: 229 changed, 5064 insertions(+), 1872 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1027 lines
34 KiB
Markdown
1027 lines
34 KiB
Markdown
---
|
|
id: "ET-LLM-005"
|
|
title: "Arquitectura del Sistema de Tools"
|
|
type: "Technical Specification"
|
|
status: "Done"
|
|
priority: "Alta"
|
|
epic: "OQI-007"
|
|
project: "trading-platform"
|
|
version: "1.0.0"
|
|
created_date: "2025-12-05"
|
|
updated_date: "2026-01-04"
|
|
---
|
|
|
|
# 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*
|
|
*Trading Platform*
|