Introducción a WLMP
WLMP (WillyLabs Messaging Protocol) es el protocolo de aplicación bidireccional y asíncrono diseñado para orquestar la comunicación entre el frontend web y Dobbi, el asistente de IA de la suite WillyLabs.
Inspirado en la elegancia del Jupyter Messaging Protocol (JMP) y operando sobre WebSockets, WLMP desacopla la generación de contenido del renderizado visual, permitiendo a la IA dictar flujos de texto en tiempo real (streaming), alterar dinámicamente la disposición de la interfaz (Server-Driven UI), emitir estados de ejecución y solicitar datos estructurados al usuario.
1. Arquitectura Base y Transporte
WLMP opera exclusivamente sobre WebSockets (wss://) para garantizar latencia ultrabaja y comunicación full-duplex (bidireccional simultánea).
Todo mensaje transmitido en WLMP es un objeto JSON estandarizado (el "Sobre" o Envelope). El sistema se basa en un paradigma de Petición-Respuesta Asíncrona, donde una solicitud del cliente puede desencadenar una cascada de múltiples mensajes de respuesta por parte de Dobbi a lo largo del tiempo.
1.1. Estructura del Sobre (Envelope)
Cada payload enviado por el socket debe cumplir estrictamente con esta estructura JSON:
{ "header": { "msg_id": "string (UUIDv4)", "session": "string (ID único de la sesión WebSocket)", "msg_type": "string (Tipo de mensaje. Ej: display_data, stream)", "date": "string (ISO 8601 UTC)", "version": "1.0" }, "parent_header": { "msg_id": "string (UUIDv4 del mensaje que originó esta respuesta. Null si es inicio de hilo)" }, "metadata": { "auth_mode": "string (anonymous | client)", "user_id": "string | null", "tenant_id": "string | null (Ej: willy_labs)" }, "content": { "display_target": "string (main_panel | side_panel | modal | floating_widget)" } }
Descripción de los bloques:
header: Identifica el mensaje y su intención (msg_type).parent_header: Mantiene la trazabilidad. Permite al cliente saber qué bloque de la UI debe actualizarse relacionando la respuesta con la petición original.metadata: Control de contexto.auth_mode: "anonymous"le indica a Dobbi que debe restringir herramientas (solo Q&A público).auth_mode: "client"activa herramientas avanzadas (RAG privado, consulta a WillyHUB, historiales).content: El cuerpo de la acción. El atributodisplay_targetes crucial para indicar al frontend en qué zona de la pantalla debe impactar el contenido.
2. Tipos de Mensajes (msg_type)
El protocolo define un conjunto cerrado de tipos de mensajes para garantizar que el frontend actúe como un despachador (dispatcher) predecible.
2.1. Cliente → Servidor (Peticiones del Usuario)
| msg_type | Descripción |
|---|---|
execute_request |
El usuario envía un prompt, comando de voz transcrito o archivo adjunto. |
form_reply |
El usuario envía un payload estructurado JSON como respuesta a un wizard de Dobbi. |
interrupt_request |
Señal de aborto. El usuario cancela la generación o la tarea en curso. |
2.2. Servidor → Cliente (Respuestas de Dobbi)
| msg_type | Descripción |
|---|---|
stream |
Chunk de texto en tiempo real. Usado para el efecto máquina de escribir (texto/markdown). |
display_data |
Renderizado de componentes ricos. Contiene un diccionario de tipos MIME (Tablas, gráficos, Mermaid). |
ui_command |
Directivas de mutación de layout (abrir paneles, redimensionar columnas, animaciones). |
task_update |
Emisión del estado mental y ejecución de herramientas del agente (para barra de progreso o timeline). |
form_request |
Dobbi pausa su ejecución y envía un JSON Schema para que el cliente dibuje un formulario. |
execute_result |
Mensaje de cierre. Indica que Dobbi ha finalizado el ciclo de procesamiento para un parent_header dado. |
error |
Notificación formal de fallo en la ejecución o validación. |
3. Sistema de Visualización y Tipos MIME
En WLMP, Dobbi no envía código HTML (a menos que sea estrictamente necesario). Dobbi envía datos usando tipos MIME estándar y personalizados (application/vnd.willy.*). El frontend lee el diccionario dentro de display_data y selecciona la representación visual más rica que soporta.
Estructura del content para display_data:
{"content": { "display_target": "side_panel", "data": { "text/markdown": "Aquí tienes los resultados tabulados:", "application/vnd.willy.table+json": { "columns": ["ID", "Métrica", "Valor"], "rows": [[1, "CPU", "45%"], [2, "RAM", "80%"]] }, "application/vnd.willy.chart+json": { "type": "bar", "datasets": [{"label": "Uso", "data": [45, 80]}] } } }}
3.1. Catálogo de Tipos MIME Soportados
text/markdown: Texto enriquecido general.text/html: Solo usado para renderizados legacy o iframes estrictos.application/vnd.willy.mermaid+text: Código fuente para que el cliente renderice diagramas de flujo.application/vnd.willy.table+json: Datos tabulares nativos (ideal para inyectar en DataTables o Grid.js).application/vnd.willy.chart+json: Esquemas para gráficos (Chart.js, ECharts, D3).application/vnd.willy.metrics+json: Tarjetas tipo KPI (Ej: "Ventas Totales: $10,000").
4. Mutación de Layout (Server-Driven UI)
Para que Dobbi ordene alteraciones estructurales en el navegador, se utiliza el tipo ui_command. El frontend debe implementar un gestor de estado (ej. Redux, Zustand o señales de React/Vue) que reaccione a estas órdenes.
{ "header": { "msg_type": "ui_command"}, "content": { "action": "layout_mutation", "params": { "command": "open_sidebar", "config": { "width": "50%", "overlay": false, "transition": "slide_in_right" } } } }
Acciones Soportadas (action):
layout_mutation: Mover, abrir o cerrar contenedores principales (main_panel,side_panel).modal_trigger: Levantar una ventana modal emergente.scroll_to: Forzar el scroll del cliente hacia un ID específico o final del hilo.clear_target: Limpiar el contenido de undisplay_targetespecífico.
5. Máquina de Estados del Agente (Lifecycle)
Dobbi procesa tareas complejas mediante agentes. Para evitar que el usuario se impaciente, Dobbi emite señales continuas de su estado interno a través de task_update.
Estados posibles: pending | running | success | error
{ "header": { "msg_type": "task_update"}, "content": { "task_id": "step_001_db", "description": "Consultando base de datos WillyHUB Data360...", "state": "running", "progress": 30 } }
El cliente acumula estos eventos para construir un componente tipo "Stepper" o "Consola de Logs de IA" que muestra el pensamiento de Dobbi en tiempo real.
6. Formularios Interactivos (Wizards)
Cuando Dobbi requiere datos que no puede inferir, delega la creación de UI al frontend mediante form_request.
1. Petición del Servidor (form_request):
{ "header": { "msg_type": "form_request", "msg_id": "req_88"}, "content": { "display_target": "main_panel", "form_id": "filter_date", "title": "Por favor, especifica el rango de fechas", "schema": { "type": "object", "properties": { "start_date": { "type": "string", "format": "date" }, "end_date": { "type": "string", "format": "date" } }, "required": ["start_date", "end_date"] } } }
2. Respuesta del Cliente (form_reply):
El cliente renderiza los inputs, valida y responde asociando el parent_header.
{ "header": { "msg_type": "form_reply", "msg_id": "rep_99" }, "parent_header": { "msg_id": "req_88" }, "content": { "form_id": "filter_date", "data": { "start_date": "2026-01-01", "end_date": "2026-06-15" } } }
7. Implementación de Referencia (Backend Python)
Para facilitar la adopción en la arquitectura backend de WillyLabs (típicamente FastAPI/WebSockets), se expone esta estructura fundacional en Python orientada a objetos.
import json import uuid import datetime from typing import Dict, Any, Optional class WLMPMessage: """Clase constructora para los sobres (envelopes) del protocolo WLMP.""" def __init__( self, msg_type: str, content: Dict[str, Any], session_id: str, auth_mode: str = "anonymous", user_id: Optional[str] = None, parent_id: Optional[str] = None ): self.header = { "msg_id": str(uuid.uuid4()), "session": session_id, "msg_type": msg_type, "date": datetime.datetime.now(datetime.timezone.utc).isoformat(), "version": "1.0" } self.parent_header = {"msg_id": parent_id} if parent_id else {} self.metadata = { "auth_mode": auth_mode, "user_id": user_id, "tenant_id": "willy_labs" } self.content = content def to_dict(self) -> Dict[str, Any]: return { "header": self.header, "parent_header": self.parent_header, "metadata": self.metadata, "content": self.content } def to_json(self) -> str: return json.dumps(self.to_dict()) class DobbiOrchestrator: """Ejemplo de orquestador para gestionar la comunicación WebSocket.""" def __init__(self, websocket, session_id: str, user_id: str): self.ws = websocket self.session_id = session_id self.user_id = user_id self.auth_mode = "client" if user_id else "anonymous" async def emit_task_update(self, parent_id: str, task_id: str, description: str, state: str): content = { "task_id": task_id, "description": description, "state": state } msg = WLMPMessage( msg_type="task_update", content=content, session_id=self.session_id, auth_mode=self.auth_mode, user_id=self.user_id, parent_id=parent_id ) await self.ws.send_text(msg.to_json()) async def emit_complex_ui(self, parent_id: str): """Flujo de ejemplo documentado: Markdown -> Abrir Panel -> Tabla""" # 1. Enviar texto a panel principal await self.ws.send_text(WLMPMessage( msg_type="display_data", content={ "display_target": "main_panel", "data": {"text/markdown": "Procesando su solicitud de reporte analítico..."} }, session_id=self.session_id, auth_mode=self.auth_mode, parent_id=parent_id ).to_json()) # 2. Comandar mutación de interfaz await self.ws.send_text(WLMPMessage( msg_type="ui_command", content={ "action": "layout_mutation", "params": {"command": "open_sidebar", "config": {"width": "40%"}} }, session_id=self.session_id, auth_mode=self.auth_mode, parent_id=parent_id ).to_json()) # 3. Inyectar datos en el panel lateral await self.ws.send_text(WLMPMessage( msg_type="display_data", content={ "display_target": "side_panel", "data": { "application/vnd.willy.table+json": { "columns": ["Servicio", "Estado"], "rows": [["WillySynaps", "OK"], ["WillyHUB", "OK"]] } } }, session_id=self.session_id, auth_mode=self.auth_mode, parent_id=parent_id ).to_json()) # 4. Finalizar petición await self.ws.send_text(WLMPMessage( msg_type="execute_result", content={"status": "completed"}, session_id=self.session_id, auth_mode=self.auth_mode, parent_id=parent_id ).to_json())
Con esta documentación, la arquitectura queda blindada. El protocolo es lo suficientemente rígido para mantener el orden, pero lo bastante extensible mediante el diccionario data de representaciones MIME para absorber cualquier evolución futura del ecosistema sin romper versiones anteriores.