Files
Claro-cases/BACKEND_HANDOFF.md
T
bryan_garcia 83e3ec2cff fix(dashboard): resolver bugs críticos de tiempo real en HITL — race conditions, In-Band Auth y multi-stream buffer
- 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
2026-07-29 04:32:27 -05:00

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:

  1. Gestionar peticiones HITL recibidas de un agente virtual durante conversaciones con clientes.
  2. 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:

  1. Derivar advisorId del token de autenticación de la sesión HTTP (Bearer token o cookie). El cliente NO envía advisorId.
  2. Actualizar el status del caso a RESOLVED.
  3. Actualizar handlingTime con la diferencia entre startedAt y resolvedAt (timestamps propios del backend).
  4. Fusionar el payload de resolución con el payload existente del caso.
  5. Tras resolver, emitir el evento hitl_resolved por 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

  1. Handshake inicial: El frontend se conecta a ws://<host>/ws/dashboard.
  2. init_state: Al establecer la conexión, el backend DEBE enviar inmediatamente un evento init_state con el estado completo actual (conversaciones activas + casos pendientes).
  3. 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_state y el frontend reemplaza su estado local completo.
  4. 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 advisorId del contexto de la conexión WebSocket autenticada.
  • Ignorar cualquier campo advisorId que 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 autenticado
  • GET /api/v1/cases/:id — cualquier asesor autenticado
  • POST /api/v1/cases/:id/resolverequiere autenticación; el backend registra qué asesor resolvió el caso
  • GET /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 toolName se repiten con diferente specialist/objective. Para la UI, solo importa el toolName. El mapeo completo (con uiPattern, formFields, y validationSchema Zod) está en src/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 occurredAt del 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 calcula handlingTime = 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_resolved por 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 payload de los casos se almacena como JSON.
  • Las notas internas (internal_note) se persisten como mensajes en la conversación con role: "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=true activa los mocks; false o ausente usa el backend real.

7. Checklist de Implementación para Backend

  • Endpoint GET /api/v1/cases con filtros status, 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_state al conectar/reconectar
  • Eventos de streaming: agent_stream_started, agent_stream_chunk (con index), agent_stream_completed
  • Evento hitl_request al requerir intervención humana
  • Evento hitl_resolved en broadcast tras resolución REST
  • Evento internal_note recibido del cliente → persistir como mensaje role: 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 advisorId del contexto autenticado (NUNCA del payload del cliente)
  • Calcular handlingTime como resolvedAt - startedAt (segundos)
  • Timestamps en ISO-8601 UTC
  • tipoSolicitud en casos coincide con los toolName del CSV
  • uiPattern en 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