20 KiB
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
- Agente configurado con herramientas habilitadas
- Permisos adecuados para cada acción
- Feature flags correspondientes activos
Flujo Principal - Ejecutar Herramienta
- LLM determina que necesita ejecutar una herramienta
- LLM genera tool call con argumentos
- Sistema valida que herramienta está habilitada para el agente
- Sistema valida argumentos según schema
- Sistema verifica permisos
- Sistema ejecuta herramienta
- Sistema captura resultado o error
- Sistema retorna resultado al LLM
- LLM genera respuesta final para usuario
- 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)