erp-core/docs/04-modelado/requerimientos-funcionales/mgn-018/RF-MGN-018-004-acciones-herramientas.md

20 KiB

RF-MGN-018-004: Acciones y Herramientas

Módulo: MGN-018 - AI Agents & Chatbots Prioridad: P2 Story Points: 13 Estado: Definido Fecha: 2025-12-05

Descripción

El sistema debe permitir que los agentes de IA ejecuten acciones en el sistema ERP mediante herramientas (tools/functions). Las herramientas permiten al agente consultar información (pedidos, productos), crear registros (leads, tickets), y ejecutar operaciones específicas del negocio. Los tenants pueden habilitar herramientas predefinidas y crear herramientas personalizadas.

Actores

  • Actor Principal: AI Agent (invoca herramientas)
  • Actores Secundarios:
    • Sistema (ejecuta acciones)
    • Tenant Admin (configura herramientas)
    • APIs externas (integraciones)

Precondiciones

  1. Agente configurado con herramientas habilitadas
  2. Permisos adecuados para cada acción
  3. Feature flags correspondientes activos

Flujo Principal - Ejecutar Herramienta

  1. LLM determina que necesita ejecutar una herramienta
  2. LLM genera tool call con argumentos
  3. Sistema valida que herramienta está habilitada para el agente
  4. Sistema valida argumentos según schema
  5. Sistema verifica permisos
  6. Sistema ejecuta herramienta
  7. Sistema captura resultado o error
  8. Sistema retorna resultado al LLM
  9. LLM genera respuesta final para usuario
  10. Sistema registra ejecución en logs

Herramientas Predefinidas

1. Consulta de Pedidos

interface CheckOrderStatusTool {
  name: 'check_order_status';
  description: 'Consulta el estado de un pedido por número de orden o datos del cliente';

  parameters: {
    order_number?: string;      // Número de orden
    customer_phone?: string;    // Teléfono del cliente
    customer_email?: string;    // Email del cliente
  };

  returns: {
    found: boolean;
    order?: {
      order_number: string;
      status: string;
      status_description: string;
      created_at: string;
      total: number;
      items: Array<{ name: string; quantity: number; }>;
      shipping?: {
        carrier: string;
        tracking_number: string;
        estimated_delivery: string;
      };
    };
    message: string;
  };
}

2. Consulta de Productos

interface SearchProductsTool {
  name: 'search_products';
  description: 'Busca productos en el catálogo por nombre, categoría o características';

  parameters: {
    query: string;              // Búsqueda de texto
    category?: string;          // Filtro por categoría
    min_price?: number;
    max_price?: number;
    in_stock_only?: boolean;
    limit?: number;             // Default: 5
  };

  returns: {
    products: Array<{
      id: string;
      name: string;
      description: string;
      price: number;
      stock_available: number;
      image_url?: string;
      category: string;
    }>;
    total_count: number;
  };
}

3. Crear Lead

interface CreateLeadTool {
  name: 'create_lead';
  description: 'Crea un nuevo lead/prospecto en el CRM';

  parameters: {
    name: string;               // Nombre del prospecto
    phone?: string;
    email?: string;
    company?: string;
    interest?: string;          // Producto/servicio de interés
    notes?: string;             // Notas adicionales
    source?: string;            // Default: 'ai_agent'
  };

  returns: {
    success: boolean;
    lead_id?: string;
    message: string;
  };
}

4. Crear Ticket de Soporte

interface CreateTicketTool {
  name: 'create_support_ticket';
  description: 'Crea un ticket de soporte para seguimiento por el equipo';

  parameters: {
    subject: string;
    description: string;
    priority?: 'low' | 'medium' | 'high';
    category?: string;
    customer_name?: string;
    customer_contact?: string;
  };

  returns: {
    success: boolean;
    ticket_id?: string;
    ticket_number?: string;
    message: string;
  };
}

5. Agendar Cita/Llamada

interface ScheduleAppointmentTool {
  name: 'schedule_appointment';
  description: 'Agenda una cita o llamada con el equipo de ventas';

  parameters: {
    customer_name: string;
    customer_phone?: string;
    customer_email?: string;
    preferred_date?: string;    // ISO date
    preferred_time?: string;    // HH:mm
    appointment_type: 'call' | 'meeting' | 'demo';
    notes?: string;
  };

  returns: {
    success: boolean;
    appointment_id?: string;
    confirmed_datetime?: string;
    message: string;
  };
}

6. Consultar Disponibilidad

interface CheckAvailabilityTool {
  name: 'check_availability';
  description: 'Verifica disponibilidad de productos o slots de citas';

  parameters: {
    type: 'product' | 'appointment';
    product_id?: string;
    date_from?: string;
    date_to?: string;
  };

  returns: {
    available: boolean;
    details: any;
    message: string;
  };
}

7. Enviar Notificación Interna

interface NotifyTeamTool {
  name: 'notify_team';
  description: 'Envía notificación al equipo para casos urgentes';

  parameters: {
    team: string;               // 'sales', 'support', 'management'
    message: string;
    priority: 'normal' | 'high' | 'urgent';
    context?: object;
  };

  returns: {
    success: boolean;
    notification_id?: string;
  };
}

Herramientas Personalizadas

Definición de Tool Personalizada

interface CustomToolDefinition {
  id: string;
  tenant_id: string;
  name: string;
  description: string;

  // Schema de parámetros (JSON Schema)
  parameters_schema: {
    type: 'object';
    properties: Record<string, JSONSchemaProperty>;
    required?: string[];
  };

  // Tipo de ejecución
  execution_type: 'api_call' | 'webhook' | 'internal_function';

  // Configuración según tipo
  execution_config: {
    // Para api_call
    url?: string;
    method?: 'GET' | 'POST' | 'PUT';
    headers?: Record<string, string>;
    body_template?: string;

    // Para webhook
    webhook_url?: string;
    webhook_secret?: string;

    // Para internal_function
    function_name?: string;
  };

  // Transformación de respuesta
  response_transform?: string;  // Template o JMESPath

  // Límites
  timeout_ms: number;
  rate_limit_per_minute: number;

  is_active: boolean;
}

Ejemplo: Tool de API Externa

{
  "id": "custom_shipping_check",
  "name": "check_shipping_rate",
  "description": "Consulta tarifas de envío con proveedor externo",

  "parameters_schema": {
    "type": "object",
    "properties": {
      "postal_code": {
        "type": "string",
        "description": "Código postal de destino"
      },
      "weight_kg": {
        "type": "number",
        "description": "Peso del paquete en kg"
      }
    },
    "required": ["postal_code", "weight_kg"]
  },

  "execution_type": "api_call",
  "execution_config": {
    "url": "https://api.shipping-provider.com/rates",
    "method": "POST",
    "headers": {
      "Authorization": "Bearer {{SHIPPING_API_KEY}}",
      "Content-Type": "application/json"
    },
    "body_template": "{\"destination\": \"{{postal_code}}\", \"weight\": {{weight_kg}}}"
  },

  "response_transform": "{rate: response.rates[0].price, carrier: response.rates[0].carrier, days: response.rates[0].delivery_days}",

  "timeout_ms": 5000,
  "rate_limit_per_minute": 10
}

Tool Call Flow (OpenAI Format)

// Request al LLM con tools
const completion = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: messages,
  tools: [
    {
      type: "function",
      function: {
        name: "check_order_status",
        description: "Consulta el estado de un pedido",
        parameters: {
          type: "object",
          properties: {
            order_number: { type: "string", description: "Número de orden" }
          },
          required: ["order_number"]
        }
      }
    }
  ]
});

// Si el LLM decide usar tool
if (completion.choices[0].message.tool_calls) {
  for (const toolCall of completion.choices[0].message.tool_calls) {
    // Ejecutar tool
    const result = await executeToolCall(toolCall);

    // Agregar resultado a mensajes
    messages.push({
      role: "tool",
      tool_call_id: toolCall.id,
      content: JSON.stringify(result)
    });
  }

  // Segunda llamada al LLM con resultados
  const finalResponse = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: messages
  });
}

Reglas de Negocio

  • RN-1: Solo herramientas habilitadas por agente pueden ejecutarse
  • RN-2: Herramientas personalizadas requieren plan Enterprise
  • RN-3: Timeout máximo por herramienta: 30 segundos
  • RN-4: Máximo 5 tool calls por turno de conversación
  • RN-5: Resultados de tools se loguean (sin datos sensibles)
  • RN-6: Webhooks deben responder en máximo 10 segundos
  • RN-7: APIs externas usan rate limiting por tenant

Criterios de Aceptación

  • Todas las herramientas predefinidas funcionan
  • LLM puede invocar herramientas según contexto
  • Validación de parámetros antes de ejecución
  • Manejo de errores y timeouts
  • Herramientas personalizadas configurables
  • Ejecución de webhooks externos funciona
  • Logs de todas las ejecuciones
  • Rate limiting por herramienta
  • Admin puede habilitar/deshabilitar por agente

Entidades Involucradas

ai_agents.tool_definitions

CREATE TABLE ai_agents.tool_definitions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID, -- NULL para tools del sistema

    -- Identificación
    name VARCHAR(100) NOT NULL,
    display_name VARCHAR(200),
    description TEXT NOT NULL,
    category VARCHAR(50), -- orders, products, crm, support, custom

    -- Schema
    parameters_schema JSONB NOT NULL,
    -- JSON Schema para validación

    -- Ejecución
    execution_type VARCHAR(50) NOT NULL,
    -- internal, api_call, webhook
    execution_config JSONB NOT NULL,

    -- Respuesta
    response_schema JSONB,
    response_transform TEXT,

    -- Configuración
    timeout_ms INT DEFAULT 10000,
    rate_limit_per_minute INT DEFAULT 60,
    requires_confirmation BOOLEAN DEFAULT false,

    -- Estado
    is_system BOOLEAN DEFAULT false,
    is_active BOOLEAN DEFAULT true,

    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT uq_tool_name_tenant UNIQUE (tenant_id, name)
);

-- Tools del sistema
INSERT INTO ai_agents.tool_definitions (name, description, category, parameters_schema, execution_type, execution_config, is_system) VALUES
('check_order_status', 'Consulta el estado de un pedido', 'orders', '{
  "type": "object",
  "properties": {
    "order_number": {"type": "string", "description": "Número de orden"},
    "customer_phone": {"type": "string", "description": "Teléfono del cliente"}
  }
}', 'internal', '{"function": "orders.getStatus"}', true),

('search_products', 'Busca productos en el catálogo', 'products', '{
  "type": "object",
  "properties": {
    "query": {"type": "string", "description": "Texto de búsqueda"},
    "category": {"type": "string"},
    "limit": {"type": "integer", "default": 5}
  },
  "required": ["query"]
}', 'internal', '{"function": "products.search"}', true),

('create_lead', 'Crea un lead en el CRM', 'crm', '{
  "type": "object",
  "properties": {
    "name": {"type": "string"},
    "phone": {"type": "string"},
    "email": {"type": "string"},
    "interest": {"type": "string"}
  },
  "required": ["name"]
}', 'internal', '{"function": "crm.createLead"}', true),

('create_support_ticket', 'Crea un ticket de soporte', 'support', '{
  "type": "object",
  "properties": {
    "subject": {"type": "string"},
    "description": {"type": "string"},
    "priority": {"type": "string", "enum": ["low", "medium", "high"]}
  },
  "required": ["subject", "description"]
}', 'internal', '{"function": "support.createTicket"}', true);

ai_agents.agent_tools

CREATE TABLE ai_agents.agent_tools (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id UUID NOT NULL REFERENCES ai_agents.agents(id) ON DELETE CASCADE,
    tool_id UUID NOT NULL REFERENCES ai_agents.tool_definitions(id),

    -- Configuración específica para este agente
    is_enabled BOOLEAN DEFAULT true,
    custom_config JSONB DEFAULT '{}',
    -- Override de parámetros, límites, etc.

    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT uq_agent_tool UNIQUE (agent_id, tool_id)
);

ai_agents.tool_executions

CREATE TABLE ai_agents.tool_executions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    agent_id UUID NOT NULL,
    conversation_id UUID NOT NULL,
    message_id UUID NOT NULL,
    tool_id UUID NOT NULL,

    -- Invocación
    tool_name VARCHAR(100) NOT NULL,
    input_params JSONB NOT NULL,

    -- Resultado
    success BOOLEAN NOT NULL,
    output JSONB,
    error_message TEXT,

    -- Timing
    started_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    completed_at TIMESTAMPTZ,
    duration_ms INT,

    -- Metadata
    execution_type VARCHAR(50),
    external_request_id VARCHAR(100)
);

CREATE INDEX idx_tool_exec_tenant ON ai_agents.tool_executions(tenant_id);
CREATE INDEX idx_tool_exec_conversation ON ai_agents.tool_executions(conversation_id);
CREATE INDEX idx_tool_exec_created ON ai_agents.tool_executions(started_at);

Implementación de Tool Executor

class ToolExecutor {
  async execute(
    toolCall: ToolCall,
    context: ExecutionContext
  ): Promise<ToolResult> {
    const tool = await this.getToolDefinition(toolCall.name);

    // 1. Validar que tool está habilitada para el agente
    if (!await this.isToolEnabled(context.agent_id, tool.id)) {
      throw new ToolNotEnabledError(toolCall.name);
    }

    // 2. Validar parámetros
    this.validateParams(toolCall.arguments, tool.parameters_schema);

    // 3. Rate limiting
    await this.checkRateLimit(context.tenant_id, tool.id);

    // 4. Ejecutar según tipo
    const startTime = Date.now();
    let result: any;

    try {
      switch (tool.execution_type) {
        case 'internal':
          result = await this.executeInternal(tool, toolCall.arguments, context);
          break;
        case 'api_call':
          result = await this.executeApiCall(tool, toolCall.arguments, context);
          break;
        case 'webhook':
          result = await this.executeWebhook(tool, toolCall.arguments, context);
          break;
      }

      // 5. Transformar respuesta si necesario
      if (tool.response_transform) {
        result = this.transformResponse(result, tool.response_transform);
      }

      // 6. Log ejecución
      await this.logExecution({
        tenant_id: context.tenant_id,
        agent_id: context.agent_id,
        conversation_id: context.conversation_id,
        message_id: context.message_id,
        tool_id: tool.id,
        tool_name: tool.name,
        input_params: toolCall.arguments,
        success: true,
        output: result,
        duration_ms: Date.now() - startTime
      });

      return { success: true, data: result };

    } catch (error) {
      await this.logExecution({
        // ... campos
        success: false,
        error_message: error.message,
        duration_ms: Date.now() - startTime
      });

      return { success: false, error: error.message };
    }
  }

  private async executeInternal(
    tool: ToolDefinition,
    params: any,
    context: ExecutionContext
  ): Promise<any> {
    const functionName = tool.execution_config.function;

    // Map de funciones internas
    const internalFunctions: Record<string, Function> = {
      'orders.getStatus': this.ordersService.getStatus.bind(this.ordersService),
      'products.search': this.productsService.search.bind(this.productsService),
      'crm.createLead': this.crmService.createLead.bind(this.crmService),
      'support.createTicket': this.supportService.createTicket.bind(this.supportService),
    };

    const fn = internalFunctions[functionName];
    if (!fn) throw new Error(`Unknown function: ${functionName}`);

    return fn(params, context);
  }

  private async executeApiCall(
    tool: ToolDefinition,
    params: any,
    context: ExecutionContext
  ): Promise<any> {
    const config = tool.execution_config;

    // Interpolate template
    let url = this.interpolate(config.url, params);
    let body = config.body_template
      ? this.interpolate(config.body_template, params)
      : JSON.stringify(params);

    // Headers con secrets
    const headers = await this.resolveHeaders(config.headers, context.tenant_id);

    const response = await fetch(url, {
      method: config.method || 'POST',
      headers,
      body: config.method !== 'GET' ? body : undefined,
      signal: AbortSignal.timeout(tool.timeout_ms)
    });

    if (!response.ok) {
      throw new Error(`API call failed: ${response.status}`);
    }

    return response.json();
  }
}

Interfaz de Configuración

┌─────────────────────────────────────────────────────────────────┐
│  ⚙️ Herramientas del Agente: Asistente de Ventas                │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Herramientas del Sistema:                                       │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ ☑️ check_order_status    Consultar estado de pedidos        ││
│  │ ☑️ search_products       Buscar en catálogo de productos    ││
│  │ ☑️ create_lead           Crear prospectos en CRM            ││
│  │ ☐ create_support_ticket  Crear tickets de soporte           ││
│  │ ☐ schedule_appointment   Agendar citas/llamadas             ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  Herramientas Personalizadas:                                   │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ ☑️ check_shipping_rate   Consultar tarifas de envío         ││
│  │ ☐ verify_inventory       Verificar inventario en tiempo real││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  [+ Crear herramienta personalizada]                            │
│                                                                  │
│                                         [Cancelar]  [Guardar]   │
└─────────────────────────────────────────────────────────────────┘

Referencias

Dependencias

  • RF Requeridos: RF-018-003 (Procesamiento de Mensajes)
  • Bloqueante para: RF-018-005 (Entrenamiento)