Arquitectura Tecnica de Canales
Esta documentacion describe en detalle como funciona internamente el sistema de canales de comunicacion, incluyendo el ruteo de mensajes, buffering de emails y la integracion con agentes de automatizacion.
Vision General del Sistema
Componentes Principales
| Componente | Responsabilidad |
|---|---|
| InboxService | Ruteo de mensajes, gestion de hilos, canales |
| CandidateEmailService | Envio de emails, buffering, templates |
| EmailService | Envio bajo nivel via AWS SES |
| TwilioService | WhatsApp y SMS via Twilio |
| WebSocketService | Notificaciones en tiempo real |
| AgentGraph | Orquestacion de agentes de automatizacion |
Flujo de Canales
┌──────────────────────────────────────┐
│ MENSAJE ENTRANTE │
└──────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────┐ ┌───────────┐ ┌──────────┐
│ EMAIL │ │ WHATSAPP │ │ PLATFORM │
│ (SES) │ │ (Twilio) │ │ (WebSocket)│
└────┬────┘ └─────┬─────┘ └─────┬────┘
│ │ │
└─────────────────┼─────────────────┘
▼
┌──────────────────────────────────────┐
│ InboxService │
│ - Identifica candidato │
│ - Busca/crea hilo │
│ - Detecta bot activo │
│ - Rutea a agente o humano │
└──────────────────────────────────────┘
Canales Soportados
Tabla de Canales
| Canal | Codigo | Proveedor | Direccion | Bot Support |
|---|---|---|---|---|
email | AWS SES | Bidireccional | Si | |
whatsapp | Twilio | Bidireccional | Si | |
| SMS | sms | Twilio | Bidireccional | Si |
| Plataforma | platform | WebSocket | Bidireccional | Si |
| Chatbot | chatbot | WebSocket | Bidireccional | Si |
| Comentario | comment | Interno | Solo salida | No |
| Sistema | system | Interno | Solo salida | No |
Ruteo de Mensajes por Canal
Logica de Seleccion de Canal
El sistema determina el canal de respuesta usando configuredChannel:
// En InboxService y BotConversationGraph
const configuredChannel = botState?.channel || "chatbot";
if (configuredChannel === "whatsapp" && thread.candidate?.phone) {
// Enviar via WhatsApp
} else if (configuredChannel === "email" && thread.candidate?.email) {
// Enviar via Email (con buffering)
} else {
// Default: platform/chatbot
}
Prioridad de Canal
- Canal configurado en botState - Establecido al iniciar el agente
- Canal del ultimo mensaje - Si no hay botState.channel
- Fallback a chatbot - Si no hay informacion de canal
Establecimiento del Canal
Cuando un agente inicia, AgentExecutionService establece el canal:
// En AgentExecutionService.executeReclutadorAction()
(thread.botState as any).channel = channel; // "email", "whatsapp", o "chatbot"
Buffering de Emails para Bots
Problema que Resuelve
Cuando un bot envia multiples mensajes consecutivos (ej: saludo + pregunta), sin buffering cada mensaje seria un email separado. El buffering los combina en uno solo.
Arquitectura del Buffer
┌─────────────────────────────────────────────────────────────────────┐
│ CandidateEmailService │
│ │
│ emailBuffer: Map<executionId, QueuedBotEmail[]> │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Execution 500 │ │ Execution 501 │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │ Msg 1: Hola │ │ │ │ Msg 1: Info │ │ │
│ │ │ Msg 2: ? │ │ │ └─────────────┘ │ │
│ │ │ Msg 3: Test │ │ └─────────────────┘ │
│ │ └─────────────┘ │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Metodos del Buffer
| Metodo | Descripcion |
|---|---|
queueBotEmail(params, executionId) | Agrega mensaje al buffer |
hasQueuedEmails(executionId) | Verifica si hay mensajes pendientes |
flushBotEmails(executionId) | Envia todos los mensajes como un email |
transferQueuedEmails(from, to) | Transfiere buffer entre ejecuciones |
clearQueuedEmails(executionId) | Limpia buffer sin enviar |
Flujo de Buffering
1. Bot genera mensaje 1
└── queueBotEmail(msg1, exec500) → buffer[500] = [msg1]
2. Bot genera mensaje 2
└── queueBotEmail(msg2, exec500) → buffer[500] = [msg1, msg2]
3. Bot entra en estado de espera
└── flushBotEmails(exec500)
├── Combina [msg1, msg2] en un solo email
├── Envia email con template multi-mensaje
└── Crea UN ThreadMessage con contenido concatenado
Contenido del ThreadMessage
Cuando hay multiples mensajes, el contenido se guarda concatenado:
[Maria]: ¡Hola Juan! Soy Maria de ACME Corp...
[Maria]: ¿Cual es tu expectativa salarial?
Transferencia Parent-Child
Escenario
Cuando un agente padre invoca un agente hijo (ej: trigger_agent), los emails del hijo deben agregarse al buffer del padre para enviarlos juntos.
Flujo de Transferencia
┌──────────────────────────────────────────────────────────────┐
│ Agente Padre (Exec 500) │
│ ┌──────────────┐ │
│ │ Buffer: [A] │ ◄─────────────────────────────────────┐ │
│ └──────────────┘ │ │
│ │ │ │
│ ▼ │ │
│ trigger_agent(waitForCompletion: true) │ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────────────────────────────────────────┐ │ │
│ │ Agente Hijo (Exec 501) │ │ │
│ │ ┌──────────────┐ │ │ │
│ │ │ Buffer: [B,C]│ │ │ │
│ │ └──────────────┘ │ │ │
│ │ │ │ │ │
│ │ ▼ (al completar) │ │ │
│ │ transferQueuedEmails(501, 500) ─────────────────┼──┘ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Buffer final padre: [A, B, C] │
│ │ │
│ ▼ (al entrar en wait o completar) │
│ flushBotEmails(500) → Email con 3 mensajes │
└──────────────────────────────────────────────────────────────┘
Codigo de Transferencia
En AgentGraph.completeNode() y executeActionNode():
const eventData = execution?.eventData as any;
const parentExecutionId = eventData?.parentExecutionId;
const canInheritLock = eventData?.canInheritLock; // true si waitForCompletion=true
if (parentExecutionId && canInheritLock) {
// Transferir al padre
candidateEmailService.transferQueuedEmails(state.executionId, parentExecutionId);
} else {
// Flush independiente
await candidateEmailService.flushBotEmails(state.executionId);
}
Historial de Conversacion
Guardado en Execution
Los mensajes se guardan en execution.conversationHistory para mostrar en el modal de detalles:
// En processTemplateBasedConversation
if (botState.executionId) {
const execution = await executionRepo.findOne({ where: { id: botState.executionId } });
// Mensaje del usuario
execution.addMessage({
role: "user",
content,
channel: channel as any,
timestamp: new Date().toISOString(),
});
// Respuesta del bot
if (result.response) {
execution.addMessage({
role: "agent",
content: result.response,
channel: channel as any,
timestamp: new Date().toISOString(),
});
}
await executionRepo.save(execution);
}
Estructura del Historial
{
"conversationHistory": [
{
"role": "agent",
"content": "¡Hola! Soy Maria...",
"channel": "email",
"timestamp": "2026-01-13T10:00:00Z"
},
{
"role": "user",
"content": "$50,000 mensuales",
"channel": "email",
"timestamp": "2026-01-13T10:05:00Z"
},
{
"role": "agent",
"content": "¿Cuando podrias comenzar?",
"channel": "email",
"timestamp": "2026-01-13T10:05:01Z"
}
]
}
Estados de Ejecucion y Canales
Estados Relevantes
| Estado | Descripcion | Flush de Emails |
|---|---|---|
| RUNNING | Ejecutando acciones | No flush |
| WAITING | Esperando respuesta/delay | Si flush |
| COMPLETED | Flujo terminado | Si flush |
| FAILED | Error en ejecucion | Clear buffer |
Distincion Importante
- WAITING: Para acciones no-conversacionales (delay, wait_for_response de agente regular)
- RUNNING: Para bots conversacionales (reclutador) que estan activamente chateando
const isConversationalBot = action.actionType === "reclutador";
if (!isConversationalBot) {
// Set WAITING - triggers flush
await executionRepo.update(state.executionId, { status: ExecutionStatus.WAITING });
} else {
// Stay RUNNING - no flush yet
}
Flujo Completo de Email con Bot
Secuencia Detallada
1. TRIGGER: Candidato aplica a vacante
└── EventBus emite APPLICATION_CREATED
2. AGENT START: Agente se activa
└── AgentExecutionService crea ejecucion
└── Establece botState.channel = "email"
3. BOT GREETING:
└── BotConversationGraph genera saludo
└── InboxService detecta channel = "email"
└── queueBotEmail(saludo, execId)
4. BOT QUESTION:
└── BotConversationGraph genera pregunta
└── queueBotEmail(pregunta, execId)
5. WAIT FOR RESPONSE:
└── shouldWait = true
└── AgentGraph.executeActionNode detecta wait
└── flushBotEmails(execId)
└── Email enviado con [saludo, pregunta]
└── ThreadMessage creado con contenido concatenado
6. USER REPLY (via email):
└── SES recibe email
└── SESInboundService procesa
└── InboxService.routeMessageToReclutadorBot
└── Agregacion de 2s (BullMQ)
7. BOT PROCESSES:
└── processTemplateBasedConversation
└── Guarda en execution.conversationHistory
└── queueBotEmail(respuesta, execId)
8. CONTINUE OR COMPLETE:
└── Si hay mas preguntas: volver a 4
└── Si completo: flushBotEmails + resumeExecution
Debugging y Logs
Logs Importantes
[CandidateEmailService] Queued email for execution 500, queue size: 1
[CandidateEmailService] Queued email for execution 500, queue size: 2
[CandidateEmailService] flushBotEmails called for execution 500
[CandidateEmailService] Flushing 2 queued emails for execution 500:
[1] "¡Hola Juan! Soy Maria..."
[2] "¿Cual es tu expectativa salarial?"
Archivo de Log
Los eventos de buffering se registran en /tmp/email-buffer.log:
2026-01-13T10:00:00.123Z [CandidateEmailService] Queued email for execution 500...
2026-01-13T10:00:00.456Z [CandidateEmailService] flushBotEmails called...
Compatibilidad entre Canales
Garantias de Compatibilidad
| Aspecto | Comportamiento |
|---|---|
Usa sendWhatsApp() directamente, sin buffering | |
| Platform | Usa createMessage() directamente, sin buffering |
Usa buffering via queueBotEmail() y flushBotEmails() | |
| Fallback | Si canal no definido, usa platform/chatbot |
Por Que Solo Email Tiene Buffering
- Email: Multiples mensajes rapidos = spam en inbox del candidato
- WhatsApp: Mensajes separados son naturales en chat
- Platform: WebSocket en tiempo real, mejor experiencia con mensajes individuales
Proximos Pasos
- Canales de Comunicacion - Vista general de canales
- Email - Detalle del canal email
- Integracion de Bots - Bots en el inbox
- Agentes en Inbox - Supervision de agentes