CreaRack-SL

Endpoint POST /api/correo/ask · Chat de la Secretaria (F1)

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, falta question, 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_KEY no 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 history en 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 busy durante 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:

  1. Define la identidad: “eres la secretaria del buzón de [miembro]”.
  2. Especifica 3 acciones y reglas (máx 3 rondas, sin inventar datos).
  3. Incluye el contexto del informe F0.
  4. Incluye el historial previo de turnos (si existe).
  5. Incluye los resultados de búsquedas/lecturas de rondas anteriores en esta pregunta.
  6. 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]]