- AppShell: corregir condición de carrera REST/WS que perdía tokens de agent_stream_chunk
- init_state atómico + eliminación de doble fuente REST/WS para actualización en tiempo real
- conversation_ended e idempotencia de eventos en máquina de estados por conversación
- Seguridad: migrar JWT de query param a In-Band Auth (primer mensaje {action:auth}) con timeout 5s y cierre 1008
- Multi-stream buffer: reemplazar buffer plano por TTL LRU (200 entradas, 60s TTL) para evitar pisado de tokens entre agentes
- agent_stream_completed ya no borra buffer incondicionalmente — delega purge a la política LRU
- Timer: corregir display de 00:00 en estado PENDING con visualización inmediata + cleanup en stop()
- Tests: 8 tests multi-stream, tests In-Band Auth, tests idempotencia y máquina de estados, tests Timer
- Resultado: 86/86 tests pasan | TypeScript 0 errores
20 KiB
Handoff Document — Backend Python (Claro Cases)
Destinatario: Agente de IA del equipo backend Python.
Objetivo: Implementar el servidor REST + WebSocket que alimenta el dashboard HITL de Claro Cases.
Versión del contrato: 1.0 — Julio 2026
1. Resumen Arquitectónico del Frontend
Propósito del Proyecto
Dashboard Human-in-the-Loop (HITL) que permite a asesores humanos:
- Gestionar peticiones HITL recibidas de un agente virtual durante conversaciones con clientes.
- Monitorear en tiempo real todas las conversaciones activas, con capacidad de inyectar notas internas.
Stack del Frontend
| Componente | Tecnología |
|---|---|
| Framework | React 19 + TypeScript |
| Build tool | Vite 6 |
| Estado global | Zustand |
| Ruteo | React Router (/cases, /monitor) |
| Estilos | Tailwind CSS v4 (CSS-first con @theme) |
| Validación | Zod |
| Comunicación | REST (canal autoritativo) + WebSocket (difusión/streaming) |
| Mock development | MSW (Mock Service Worker) — solo en modo dev |
Arquitectura de Comunicación
┌──────────────────────────────────────┐
│ Frontend React │
│ │
│ /cases ───▶ REST POST /cases/:id/ │──▶ Backend
│ resolve (ESCRITURA) │ Python
│ │
│ /monitor ◀─── WebSocket /ws/ │◀──
│ dashboard (DIFUSIÓN) │
└──────────────────────────────────────┘
Regla de oro: REST es el único canal autoritativo de escritura. WebSocket es exclusivamente para difusión de eventos y streaming en tiempo real desde el servidor hacia el cliente. El cliente solo envía por WebSocket el evento internal_note (notas internas del asesor).
2. Contratos de la API REST
Base URL: http://<host>:<port>/api/v1
2.1 Listar Casos (con filtros y paginación)
GET /api/v1/cases?status=<status>&applicative=<app>&search=<query>&offset=<n>&limit=<n>
Query Parameters (todos opcionales):
| Parámetro | Tipo | Descripción | Ejemplo |
|---|---|---|---|
status |
string | Filtrar por estado | PENDING, IN_PROGRESS, RESOLVED, FAILED |
applicative |
string | Filtrar por aplicativo | AC+, ASCARD, DiMe, Formatos SGCS, Mi asistencia 360, Paradigma, RR, Phone Protect |
search |
string | Búsqueda textual (título, ID externo, cédula, tipo solicitud) | Pérez |
offset |
integer | Offset de paginación (default 0) | 0 |
limit |
integer | Límite de items (default 20) | 20 |
Respuesta (200 OK):
{
"items": [
{
"id": 1,
"title": "Validación de Proporcionales - Móvil",
"description": "Validar si el cliente tiene cobros proporcionales...",
"status": "PENDING",
"externalId": "EXT-001",
"cedula": "1020304050",
"tipoSolicitud": "Validar_Proporcionales_Movil",
"applicative": "AC+",
"uiPattern": "CONFIRMATION_WITH_VALUE",
"payload": {
"nombre": "Juan Pérez",
"telefono": "3101234567",
"linea": "3008001234"
},
"handlingTime": 0,
"createdAt": "2026-07-23T15:00:00.000Z"
}
],
"total": 53
}
Campos del objeto CaseRequest:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
id |
number | ✅ | ID único del caso |
title |
string | ✅ | Título descriptivo |
description |
string | ✅ | Descripción detallada |
status |
string | ✅ | PENDING, IN_PROGRESS, RESOLVED, FAILED |
externalId |
string | ❌ | ID externo de referencia (ej. número de ticket) |
cedula |
string | ❌ | Documento de identidad del cliente |
tipoSolicitud |
string | ✅ | Debe coincidir con un toolName del CSV (ver sección 5) |
applicative |
string | ✅ | Aplicativo origen: AC+, ASCARD, DiMe, RR, etc. |
uiPattern |
string | ✅ | Patrón de UI: SIMPLE_CONFIRMATION, CONFIRMATION_WITH_VALUE, MULTI_FIELD_FORM, DATE_SIMPLE, FREE_TEXT, READ_ONLY |
payload |
object | ✅ | Datos adicionales del caso (estructura libre) |
handlingTime |
number | ✅ | Tiempo de gestión en segundos (0 si no iniciado) |
createdAt |
string | ✅ | Timestamp ISO-8601 UTC |
2.2 Obtener Caso Individual
GET /api/v1/cases/:id
Respuesta (200 OK): Objeto CaseRequest (misma estructura que arriba).
Error (404): Si el caso no existe.
2.3 Resolver Caso (CANAL AUTORITATIVO)
POST /api/v1/cases/:id/resolve
Content-Type: application/json
Request Body:
{
"action": "approved",
"payload": {
"confirmacion": true,
"valor": 15000
},
"note": "Cliente verificó con documento de identidad"
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
action |
string | ✅ | "approved" o "rejected" |
payload |
object | ✅ | Datos de resolución (estructura depende del uiPattern del caso) |
note |
string | ❌ | Nota opcional del asesor |
⚠️ El backend DEBE:
- Derivar
advisorIddel token de autenticación de la sesión HTTP (Bearer token o cookie). El cliente NO envíaadvisorId. - Actualizar el
statusdel caso aRESOLVED. - Actualizar
handlingTimecon la diferencia entrestartedAtyresolvedAt(timestamps propios del backend). - Fusionar el
payloadde resolución con elpayloadexistente del caso. - Tras resolver, emitir el evento
hitl_resolvedpor WebSocket a todos los clientes conectados (broadcast).
Respuesta (200 OK): Objeto CaseRequest actualizado.
2.4 Obtener Conversaciones Activas
GET /api/v1/conversations/active
Respuesta (200 OK):
[
{
"id": "conv-1",
"clientId": "CLI-001",
"agentId": "AGENT-01",
"status": "active",
"messages": [
{
"id": "m1",
"conversationId": "conv-1",
"role": "user",
"content": "Hola, necesito ayuda con mi factura",
"timestamp": "2026-07-23T15:00:00.000Z",
"isStreaming": false,
"metadata": {}
}
],
"createdAt": "2026-07-23T15:00:00.000Z"
}
]
2.5 Obtener Conversación Individual
GET /api/v1/conversations/:id
Respuesta (200 OK): Objeto Conversation con todos sus mensajes.
3. Protocolo y Eventos WebSocket
URL: ws://<host>:<port>/ws/dashboard
3.1 Envelope Estándar
Todo mensaje WebSocket (en ambas direcciones) debe usar el siguiente envelope JSON:
{
"type": "string",
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"occurredAt": "2026-07-23T15:00:00.000Z",
"payload": { }
}
| Campo | Tipo | Descripción |
|---|---|---|
type |
string | Tipo de evento (ver tablas abajo) |
eventId |
string (UUID v4) | ID único del evento para deduplicación |
occurredAt |
string (ISO-8601 UTC) | Timestamp del lado emisor |
payload |
object | Carga específica del evento |
3.2 Ciclo de Vida de Conexión y Reconexión
- Handshake inicial: El frontend se conecta a
ws://<host>/ws/dashboard. init_state: Al establecer la conexión, el backend DEBE enviar inmediatamente un eventoinit_statecon el estado completo actual (conversaciones activas + casos pendientes).- Reconexión: El frontend implementa backoff exponencial (1s → 2s → 4s → 8s → 16s → máx 30s, factor 2x). Al reconectar, el backend envía nuevamente
init_statey el frontend reemplaza su estado local completo. - Heartbeat: Se recomienda que el backend envíe pings periódicos (cada 30s) para detectar desconexiones.
3.3 Eventos Servidor → Cliente
type |
Payload | Cuándo se emite |
|---|---|---|
init_state |
{ conversations: Conversation[], activeCases: CaseRequest[] } |
Al conectar o reconectar |
conversation_started |
{ conversation: Conversation } |
Nueva conversación iniciada |
conversation_ended |
{ conversationId: string, endedAt: string } |
Conversación finalizada |
user_message |
{ conversationId: string, message: Message } |
Mensaje completo del usuario |
agent_stream_started |
{ conversationId: string, messageId: string } |
El agente comienza a generar respuesta |
agent_stream_chunk |
{ conversationId: string, messageId: string, token: string, index: number } |
Token individual (índice garantiza orden) |
agent_stream_completed |
{ conversationId: string, messageId: string, fullContent: string } |
Streaming finalizado; fullContent es el texto completo |
agent_status_update |
{ agentId: string, status: "ONLINE" | "BUSY" | "OFFLINE" } |
Cambio de estado del agente |
hitl_request |
{ case: CaseRequest, conversationId: string } |
Se requiere intervención humana |
hitl_resolved |
{ caseId: number, resolution: object } |
Caso resuelto (broadcast a todos los asesores) |
error |
{ code: string, message: string, details?: object } |
Error del servidor notificable |
Ejemplo: Streaming de mensaje del agente
Servidor → Cliente:
1. { "type": "agent_stream_started", "payload": { "conversationId": "conv-1", "messageId": "m10" } }
2. { "type": "agent_stream_chunk", "payload": { "conversationId": "conv-1", "messageId": "m10", "token": "Cl", "index": 0 } }
3. { "type": "agent_stream_chunk", "payload": { "conversationId": "conv-1", "messageId": "m10", "token": "aro", "index": 1 } }
4. { "type": "agent_stream_chunk", "payload": { "conversationId": "conv-1", "messageId": "m10", "token": ", ", "index": 2 } }
5. { "type": "agent_stream_chunk", "payload": { "conversationId": "conv-1", "messageId": "m10", "token": "con", "index": 3 } }
...
N. { "type": "agent_stream_completed", "payload": { "conversationId": "conv-1", "messageId": "m10", "fullContent": "Claro, con gusto le ayudo..." } }
⚠️ Importante: Los tokens deben enviarse con index secuencial (0, 1, 2, ...) para que el frontend pueda reconstruir el orden incluso si los chunks llegan desordenados por la red.
Ejemplo: Solicitud HITL
{
"type": "hitl_request",
"eventId": "a1b2c3d4-...",
"occurredAt": "2026-07-23T15:01:00.000Z",
"payload": {
"case": {
"id": 42,
"title": "Validación de Identidad - Cliente",
"description": "Validar la identidad del cliente...",
"status": "PENDING",
"externalId": "EXT-042",
"cedula": "1020304050",
"tipoSolicitud": "Validar_Identidad_Movil",
"applicative": "AC+",
"uiPattern": "SIMPLE_CONFIRMATION",
"payload": { "nombre": "Juan Pérez", "telefono": "3101234567" },
"handlingTime": 0,
"createdAt": "2026-07-23T15:01:00.000Z"
},
"conversationId": "conv-1"
}
}
Al recibir hitl_request, el frontend automáticamente:
- Inserta el caso en la lista del dashboard
- Reproduce una alerta sonora (Web Audio API)
- Muestra una notificación de escritorio HTML5
- Hace parpadear el título de la pestaña si el navegador no está enfocado
3.4 Eventos Cliente → Servidor
type |
Payload | Cuándo se envía |
|---|---|---|
internal_note |
{ conversationId: string, content: string } |
Asesor inyecta nota interna desde el monitor |
Ejemplo:
{
"type": "internal_note",
"eventId": "f9e8d7c6-...",
"occurredAt": "2026-07-23T15:02:00.000Z",
"payload": {
"conversationId": "conv-1",
"content": "Cliente tiene historial de reclamos similares. Verificar antes de aprobar."
}
}
⚠️ El backend DEBE:
- Derivar
advisorIddel contexto de la conexión WebSocket autenticada. - Ignorar cualquier campo
advisorIdque pudiera venir en el payload (el cliente no lo envía, pero por seguridad). - Insertar la nota como un mensaje con
role: "internal"en la conversación indicada. - Re-difundir el mensaje como
conversation_update(o evento equivalente) a los demás clientes conectados.
4. Autenticación e Identidad del Asesor
Regla de Negocio
La identidad del asesor (
advisorId) NUNCA es enviada por el frontend.
El backend es el único responsable de derivarla desde el contexto autenticado de la conexión.
Implementación esperada en el backend
| Canal | Mecanismo de autenticación | Cómo derivar advisorId |
|---|---|---|
| REST | Bearer token en header Authorization: Bearer <jwt> o cookie de sesión |
Extraer advisorId del payload del JWT o consultar la sesión |
| WebSocket | Token enviado como query param al conectar: ws://host/ws/dashboard?token=<jwt> |
Validar JWT durante el handshake; almacenar advisorId en el contexto de la conexión |
Endpoints REST que requieren autenticación
GET /api/v1/cases— cualquier asesor autenticadoGET /api/v1/cases/:id— cualquier asesor autenticadoPOST /api/v1/cases/:id/resolve— requiere autenticación; el backend registra qué asesor resolvió el casoGET /api/v1/conversations/active— cualquier asesor autenticado
5. Taxonomía de Casos y Patrones de UI
El frontend clasifica los casos en 6 patrones de UI que determinan qué formulario se renderiza al asesor:
uiPattern |
Descripción | Ejemplo de respuesta esperada |
|---|---|---|
SIMPLE_CONFIRMATION |
Confirmación binaria Sí/No | { "confirmacion": true } |
CONFIRMATION_WITH_VALUE |
Confirmación + valor monetario | { "confirmacion": true, "valor": 15000 } |
MULTI_FIELD_FORM |
Formulario con múltiples campos | { "numero_cuotas": 12, "valor_cuota": 85000, "dia_corte": 15, "dia_limite_pago": 25 } |
DATE_SIMPLE |
Fecha única | { "fecha": "15-07-2026" } |
FREE_TEXT |
Texto libre | { "respuesta": "Cargo corresponde a roaming internacional" } |
READ_ONLY |
Solo informativo (sin campos) | {} |
Lista completa de tipos de caso (tipoSolicitud)
Los 53 tipoSolicitud válidos están definidos en el CSV Consulta de aplicativos - Claro - Facturación.csv y mapeados en src/data/caseTypeDefinitions.ts. El backend DEBE enviar un tipoSolicitud que coincida exactamente con uno de estos toolName:
AC+ (13 casos): Validar_Proporcionales_Movil, Tickler_AC+_CreerEnElCliente, Validar_Suspensiones_Movil, Validar_Identidad_Movil, Validar_Moras_Movil, Validar_Fecha_Corte_Movil, Validar_Fecha_Limite_Movil, Activar_Roaming, Desactivar_Roaming, Validar_Finalizacion_Campaña_Movil, Validar_Cambio_Plan_Movil, Consulta_Ultima_Factura_Movil
ASCARD (9 casos): Plan_De_Pagos_EF, Tasa_De_Interes_EF, Pago_Minimo_EF, Plan_Total_EF, Refinanciacion_EF, Paz_Salvo_EF, Unificar_Factura_EF, Desbloqueo_EF, IMEI_EF
DiMe (8 casos): Validar_Creer_Cliente, Activa_Creer_Cliente, Activa_Creer_Cliente_Hogar
Formatos SGCS (2 casos): Cambio_Ciclos_Movil
Mi asistencia 360 (2 casos): Escalar_Pagos_No_Abonados
Paradigma (2 casos): Validar_Aumento_Tarifario
RR (12 casos): Validar_Seguros_Hogar, Validar_Aumento_Tarifario_Hogar, Validar_Campaña_Hogar, Cambio_Plan_Hogar, Validar_Clausula_Hogar, Cobros_Adicionales_Hogar, Validar_Identidad_Hogar, Validar_Moras_Hogar, Validar_Fecha_Corte_Hogar, Validar_Fecha_Limite_Hogar, Validar_Proporcionales_Hogar, Validar_Suspensiones_Hogar, Validar_Venta_Tecnología, Validar_OTT_1, Validar_OTT_2
Nota: Algunos
toolNamese repiten con diferentespecialist/objective. Para la UI, solo importa eltoolName. El mapeo completo (conuiPattern,formFields, yvalidationSchemaZod) está ensrc/data/caseTypeDefinitions.ts.
Campos del payload de resolución (por uiPattern)
Cuando el frontend envía POST /cases/:id/resolve, el payload tiene esta estructura según el uiPattern:
uiPattern |
Estructura del payload |
|---|---|
SIMPLE_CONFIRMATION |
{ "confirmacion": boolean } |
CONFIRMATION_WITH_VALUE |
{ "confirmacion": boolean, "valor": number } |
MULTI_FIELD_FORM |
Estructura variable según el tipoSolicitud (ver caseTypeDefinitions.ts para cada caso) |
DATE_SIMPLE |
{ "fecha": "dd-mm-aaaa" } |
FREE_TEXT |
{ "respuesta": string } |
READ_ONLY |
{} |
6. Notas Técnicas para el Backend
6.1 Timestamps
- Todos los timestamps deben estar en ISO-8601 UTC (ej.
"2026-07-23T15:00:00.000Z"). - El campo
occurredAtdel envelope WebSocket usa el mismo formato.
6.2 Manejo de handlingTime
- El backend es la fuente de verdad para
handlingTime. - Cuando el asesor comienza a gestionar un caso, el backend registra
startedAt. - Al recibir
POST /cases/:id/resolve, el backend calculahandlingTime = resolvedAt - startedAt(en segundos). - El frontend muestra un cronómetro en UI como referencia visual, pero no es autoritativo.
6.3 Broadcast de hitl_resolved
- Al resolver un caso vía REST, el backend DEBE emitir
hitl_resolvedpor WebSocket a todos los clientes conectados (no solo al que resolvió). - Esto permite que otros asesores vean que el caso ya fue atendido.
6.4 Persistencia
- El backend debe persistir todos los casos y conversaciones en base de datos.
- El
payloadde los casos se almacena como JSON. - Las notas internas (
internal_note) se persisten como mensajes en la conversación conrole: "internal".
6.5 MSW (Desarrollo Frontend Independiente)
- El frontend incluye una capa MSW que simula todos los endpoints REST y datos mock.
- El backend puede desarrollarse en paralelo sin depender del frontend, ya que los contratos están completamente especificados aquí.
- Variable de entorno del frontend:
VITE_ENABLE_MSW=trueactiva los mocks;falseo ausente usa el backend real.
7. Checklist de Implementación para Backend
- Endpoint
GET /api/v1/casescon filtrosstatus,applicative,search,offset,limit - Endpoint
GET /api/v1/cases/:id - Endpoint
POST /api/v1/cases/:id/resolve(canal autoritativo) - Endpoint
GET /api/v1/conversations/active - Endpoint
GET /api/v1/conversations/:id - Servidor WebSocket en
/ws/dashboard - Envelope JSON estándar
{ type, eventId, occurredAt, payload } - Evento
init_stateal conectar/reconectar - Eventos de streaming:
agent_stream_started,agent_stream_chunk(conindex),agent_stream_completed - Evento
hitl_requestal requerir intervención humana - Evento
hitl_resolveden broadcast tras resolución REST - Evento
internal_noterecibido del cliente → persistir como mensajerole: internal→ re-difundir - Eventos
conversation_started,conversation_ended,user_message,agent_status_update - Autenticación REST vía Bearer token / cookie de sesión
- Autenticación WebSocket vía query param
?token=<jwt> - Derivar
advisorIddel contexto autenticado (NUNCA del payload del cliente) - Calcular
handlingTimecomoresolvedAt - startedAt(segundos) - Timestamps en ISO-8601 UTC
tipoSolicituden casos coincide con lostoolNamedel CSVuiPatternen casos coincide con uno de los 6 valores del enum
8. Referencia Rápida de Archivos del Frontend
| Archivo | Contenido relevante para backend |
|---|---|
src/types/index.ts |
Interfaces CaseRequest, Conversation, Message |
src/types/wsProtocol.ts |
Schemas Zod de todos los eventos WS + envelope |
src/data/caseTypeDefinitions.ts |
53 tipos de caso con uiPattern, formFields, validationSchema |
src/services/api.ts |
Cliente REST (endpoints y formatos esperados) |
src/services/wsClient.ts |
Cliente WebSocket (reconexión, envelope) |
src/mocks/handlers.ts |
Datos mock de ejemplo (10 casos, 3 conversaciones) |
SPECIFICATION.md |
Especificación completa del proyecto |