UNPKG

@grec0/mcp-s2s-asterisk

Version:

MCP server para asistente telefónico conversacional con Asterisk S2S

262 lines (261 loc) 9.96 kB
// 📞 Cliente HTTP para Asistente Telefónico Conversacional import axios from 'axios'; import { PhoneAssistantError } from '../types/realtime-assistant.js'; /** * Cliente para interactuar con la API del Asistente Telefónico */ export class PhoneClient { client; config; constructor(config) { this.config = config; this.client = axios.create({ baseURL: config.baseUrl, timeout: config.timeout, headers: { 'Content-Type': 'application/json', 'X-API-Key': config.apiKey, 'User-Agent': 'MCP-PhoneAssistant/1.0' } }); // Interceptor para manejo de errores this.client.interceptors.response.use((response) => response, (error) => this.handleAxiosError(error)); } /** * Invocar al asistente telefónico para realizar una llamada conversacional */ async makePhoneCall(request) { try { console.log(`🔄 Iniciando llamada telefónica a: ${request.usuario} (${request.telefono})`); console.log(`📋 Propósito: ${request.proposito}`); // Construir las instrucciones completas combinando propósito y contexto let instrucciones = `${request.proposito}.`; if (request.contexto) { instrucciones += ` ${request.contexto}`; } // Construir la herramienta HTTP para el callback del MCP const mcpCallbackTool = this.buildMcpCallbackTool(); // Convertir herramientas al formato esperado por la API const tools = [...request.herramientas, mcpCallbackTool].map(tool => ({ name: tool.name, description: tool.description, type: "http", url: tool.endpoint, method: tool.method || "POST", headers: tool.authentication ? { [tool.authentication.header || 'Authorization']: tool.authentication.key || '' } : {}, parameters: tool.parameters || [] })); // Estructura de datos que espera la API real const apiRequest = { usuario: request.usuario, telefono: request.telefono, timeout: request.timeout, instrucciones: instrucciones, tools: tools }; // Configurar headers incluyendo X-API-Key const headers = { 'Content-Type': 'application/json', 'X-API-Key': this.config.apiKey, 'User-Agent': 'MCP-PhoneAssistant/1.0' }; const response = await this.client.post('/instrucciones', apiRequest, { headers }); console.log(`✅ Llamada telefónica iniciada. Respuesta:`, response.data); return { success: true, callId: response.data.call_id || this.generateCallId(), message: response.data.mensaje || 'Llamada telefónica iniciada correctamente', estimatedDuration: request.timeout }; } catch (error) { console.error('❌ Error al iniciar llamada telefónica:', error); throw this.createPhoneError('CALL_FAILED', 'Error al iniciar la llamada telefónica', error); } } /** * Obtener el estado de una llamada */ async getCallStatus(callId) { try { const response = await this.client.get(`/call/${callId}/status`); return response.data; } catch (error) { console.error(`❌ Error al obtener estado de llamada ${callId}:`, error); throw this.createPhoneError('STATUS_FAILED', 'Error al obtener estado de llamada', error); } } /** * Cancelar una llamada en curso */ async cancelCall(callId) { try { await this.client.post(`/call/${callId}/cancel`); console.log(`🚫 Llamada ${callId} cancelada`); return true; } catch (error) { console.error(`❌ Error al cancelar llamada ${callId}:`, error); throw this.createPhoneError('CANCEL_FAILED', 'Error al cancelar llamada', error); } } /** * Construir la herramienta HTTP para el callback del MCP */ buildMcpCallbackTool() { return { name: 'responder_al_mcp', description: 'Envía el resultado de la conversación telefónica de vuelta al MCP', endpoint: `${this.config.mcpCallbackUrl}/api/phone/conversation-result`, method: 'POST', parameters: [ { name: 'callId', type: 'string', description: 'ID único de la llamada', required: true }, { name: 'usuario', type: 'string', description: 'Nombre del usuario que recibió la llamada', required: true }, { name: 'telefono', type: 'string', description: 'Número de teléfono llamado', required: true }, { name: 'status', type: 'string', description: 'Estado final de la llamada (completed, failed, timeout, cancelled)', required: true }, { name: 'duration', type: 'number', description: 'Duración de la llamada en segundos', required: true }, { name: 'resumen_conversacion', type: 'string', description: 'Resumen natural y conversacional de lo que se habló', required: true }, { name: 'resultado_accion', type: 'string', description: 'Qué se logró o decidió en la llamada', required: false }, { name: 'informacion_obtenida', type: 'string', description: 'Información estructurada extraída (JSON string)', required: false }, { name: 'transcripcion', type: 'string', description: 'Transcripción completa en JSON si está disponible', required: false }, { name: 'metadata', type: 'string', description: 'Metadatos adicionales en JSON', required: false } ], authentication: { type: 'api_key', header: 'X-MCP-API-Key', key: process.env.MCP_CALLBACK_API_KEY || 'mcp-default-key' } }; } /** * Generar un ID único para la llamada */ generateCallId() { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 15); return `call_${timestamp}_${random}`; } /** * Manejar errores de Axios */ handleAxiosError(error) { if (error.response) { // El servidor respondió con un código de error throw this.createPhoneError(`HTTP_${error.response.status}`, `Error HTTP ${error.response.status}: ${error.response.statusText}`, error.response.data); } else if (error.request) { // La petición fue hecha pero no se recibió respuesta throw this.createPhoneError('NO_RESPONSE', 'No se pudo conectar con el asistente telefónico', error.message); } else { // Error en la configuración de la petición throw this.createPhoneError('REQUEST_ERROR', 'Error en la configuración de la petición', error.message); } } /** * Crear un error personalizado del asistente telefónico */ createPhoneError(code, message, details) { return new PhoneAssistantError(message, code, details); } /** * Realizar reintentos con backoff exponencial */ async withRetry(operation, maxRetries = this.config.retries) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; if (attempt === maxRetries) { break; } const delay = Math.pow(2, attempt) * 1000; // Backoff exponencial console.log(`⏱️ Reintento ${attempt}/${maxRetries} en ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } /** * Validar la conectividad con el asistente telefónico */ async healthCheck() { try { const response = await this.client.get('/health'); return response.status === 200; } catch (error) { console.error('❌ Health check falló:', error); return false; } } /** * Obtener métricas del asistente telefónico */ async getMetrics() { try { const response = await this.client.get('/metrics'); return response.data; } catch (error) { console.error('❌ Error al obtener métricas:', error); throw this.createPhoneError('METRICS_FAILED', 'Error al obtener métricas', error); } } }