@grec0/mcp-s2s-asterisk
Version:
MCP server para asistente telefónico conversacional con Asterisk S2S
262 lines (261 loc) • 9.96 kB
JavaScript
// 📞 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);
}
}
}