# 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://:/api/v1` ### 2.1 Listar Casos (con filtros y paginación) ``` GET /api/v1/cases?status=&applicative=&search=&offset=&limit= ``` **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`): ```json { "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**: ```json { "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`): ```json [ { "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://:/ws/dashboard` ### 3.1 Envelope Estándar Todo mensaje WebSocket (en ambas direcciones) **debe** usar el siguiente envelope JSON: ```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:///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 ```json { "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**: ```json { "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 ` 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=` | 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/resolve` — **requiere 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=` - [ ] 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 |