Descripción
Endpoint multiturno que implementa el chat de la secretaria del buzón de Zoho. Usa Gemma 4 con decodificación restringida (patrón Auto-Plan/Oráculo): el modelo decide tres acciones (responder|buscar|leer) en JSON, el Worker las ejecuta contra la API real de Zoho, y re-pregunta hasta que responde o se agotan 3 rondas.
Firma
POST /api/correo/ask
Content-Type: application/json
{
"question": string, // ≤2000 chars, obligatorio
"history": Array<{ // historial de turnos previos (efímeros, no persistidos)
role: "user" | "assistant",
content: string // ≤2000 chars por mensaje
}> // máx 12 últimos mensajes
}
Respuesta (200 OK):
{
"answer": string, // respuesta final del modelo
"tool_rounds": number, // cuántas rondas de búsqueda/lectura hizo
"duration_ms": number, // latencia total
"model": "gemma-4-26b-a4b-it"
}
Errores:
403 Forbidden: no autenticado (no hay CF Access session).400 Bad Request: JSON inválido, faltaquestion, o supera tamaño.413 Payload Too Large:question> 2000 chars.429 Too Many Requests: rate-limit excedido (20 consultas/min).503 Service Unavailable:GOOGLE_AI_API_KEYno configurada.500 Internal Error: timeout, error de Zoho sin recuperación, etc.
Identidad
Obligatorio: solo navegador (CF Access).
La autenticación se resuelve con resolveTeamMember(request) del módulo _lib/staff. Tokens de servicio no juegan — no es posible llamar a este endpoint desde un script o herramienta MCP.
Contexto Principal
Carga el informe más reciente del miembro (correo_digests, tabla de F0):
SELECT generated_at, resumen, ruido_count, payload
FROM correo_digests
WHERE owner = ?
ORDER BY created_at DESC, id DESC
LIMIT 1
Si existe, el prompt incluye:
Última pasada de {generated_at} · {resumen} · ruido filtrado: {ruido_count}
Ítems: {payload}
Si no existe (F0 nunca ha corrido), dice: (sin informe todavía — la primera pasada de /correo aún no ha corrido).
Decodificación Restringida
El modelo recibe un prompt con 3 acciones posibles definidas en el responseSchema de Gemma 4:
const requestBody = JSON.stringify({
contents: [{ role: 'user', parts: [{ text: prompt }] }],
generationConfig: {
temperature: 1.0,
topP: 0.95,
topK: 64,
maxOutputTokens: 1024,
responseMimeType: 'application/json',
responseSchema: {
type: 'OBJECT',
properties: {
action: { type: 'STRING', enum: ['responder', 'buscar', 'leer'] },
answer: { type: 'STRING' },
query: { type: 'STRING' },
from: { type: 'STRING' },
subject: { type: 'STRING' },
messageId: { type: 'STRING' },
folderId: { type: 'STRING' },
},
required: ['action'],
},
},
});
El modelo siempre devuelve un JSON válido con action y los parámetros que apliquen. Si JSON se rompe pese al constrained decoding, el Worker lo trata como una respuesta plana (responder).
Herramientas (Tool Rounds)
toolBuscar: Búsqueda en el Buzón
Llama a /api/accounts/{accountId}/messages/search de Zoho:
- Parámetros (en el JSON del modelo):
query: búsqueda libre en todo el contenido (entire:...).from: filtro por remitente (sender:...).subject: filtro por asunto (subject:...).
- Resultado: lista de hasta 10 mensajes con
messageId,folderId,from,subject,fecha,resumen(primeros 150 chars).
toolLeer: Lectura de Cuerpo
Llama a /api/accounts/{accountId}/folders/{folderId}/messages/{messageId}/content:
- Parámetros:
messageId,folderId(deben venir de una búsqueda previa). - Conversión HTML→texto: elimina estilos, scripts, etiquetas; convierte saltos de línea; acota a 4000 chars.
- Resultado: texto plano limpio y legible.
Rate-Limit y Guardarraíles
- Rate-limit: máx 20 consultas/min por miembro (tabla
correo_chat_queries). Si se excede, 429. - Tamaño de pregunta: ≤2000 chars.
- Tamaño de cuerpo: ≤4000 chars (trunca HTML convertido).
- Historial: mantiene máx 12 últimos turnos (FIFO: descarta los más viejos).
- Rondas de herramienta: máx 3 búsquedas/lecturas. Si no cierra una respuesta en 3 rondas, devuelve lo mejor del último estado.
- Timeout: 40 segundos por llamada a AI Studio. Reintenta una vez más (2 intentos totales) por cold start (footgun s88).
Telemetría
Tras cada pregunta (éxito o fracaso), registra una fila en correo_chat_queries:
INSERT INTO correo_chat_queries (owner, question, tool_rounds, duration_ms)
VALUES (?, ?, ?, ?)
Best-effort: no interrumpe la respuesta si el insert falla. Es el dato real de la apuesta #9 (WAGERS.md).
Conversación Efímera
- No persiste historiales en D1 o almacén duradero.
- El cliente mantiene el
historyen memoria (React state, lista de burbujas de chat). - Cada pregunta nueva envía los turnos previos en el body (
history: [...]) para contexto. El servidor NO los verifica ni sincroniza. - Ventaja de seguridad: los cuerpos de Zoho (privados, datos de clientes) viajan solo al navegador, jamás a base de datos (grill Q6).
Componente React (CorreoChat.tsx)
UI para el chat (panel en /correo):
- Input de pregunta (≤2000 chars).
- Listado de burbujas (usuario/asistente).
- Estado
busydurante la llamada. - Auto-scroll al recibir respuesta.
- Muestra contador de rondas de búsqueda al final de la respuesta.
Prompt Canónico
El Worker construye un prompt multilingüe (adapta al idioma del usuario) que:
- Define la identidad: “eres la secretaria del buzón de [miembro]”.
- Especifica 3 acciones y reglas (máx 3 rondas, sin inventar datos).
- Incluye el contexto del informe F0.
- Incluye el historial previo de turnos (si existe).
- Incluye los resultados de búsquedas/lecturas de rondas anteriores en esta pregunta.
- Formula la pregunta nueva.
Ejemplo (simplificado):
Eres la secretaria del buzón de correo de Edu (equipo CreaRack / Esferic Labs).
Hablas llano y al grano, como una compañera de confianza.
Tienes tres acciones posibles — responde SIEMPRE con un único JSON:
- {"action":"responder","answer":"..."}
- {"action":"buscar","query":"..."} | {"action":"buscar","from":"..."}
- {"action":"leer","messageId":"...","folderId":"..."}
Máximo 3 consultas al buzón. Si con lo que hay no basta, responde con lo que sepas.
Último informe (contexto principal):
Pasada de 2026-08-12 14:00:00 · 12 correos nuevos desde ayer, ruido filtrado: 3
Ítems: ...
Pregunta de Edu: ¿qué me decía el último aviso de Hetzner?
Véase también
- [[feature—workspace—correo-menu-f0-f1-f2]]
- [[entity—correo—table—chat-queries]]
- [[entity—correo—endpoint—peticiones]]
- [[entity—workspace—endpoint—oraculo-ask]]
- [[concept—saas—multi-tenancy]]
- [[decision—20260403—multi-tenancy-rls]]