Volver a la wiki

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

Descripción

Handler de Cloudflare Workers que implementa el chat conversacional F1 del Menú Correo. Usa Gemma 4 (Google AI Studio) con decodificación restringida para elegir acciones (responder, buscar en Zoho Mail, leer cuerpos de mensaje). El chat es efímero: vive solo en la memoria del navegador, sin persistencia en D1 salvo telemetría anónima.

Ubicación: functions/api/correo/ask.ts (330 LOC).

Firma y entrada

export const onRequestPost: PagesFunction<CorreoEnv> = async ({ request, env }) => { ... }

Parámetros (body JSON)

CampoTipoRequeridoLímiteDescripción
questionstring✓2000 charsPregunta del usuario sobre su correo/digest
historyArray-12 msgsHistórico de conversación (role: ‘user’ | ‘assistant’, content)

Respuesta exitosa (200)

{
  "answer": "Según el informe, el último aviso de Hetzner fue...",
  "tool_rounds": 2,
  "duration_ms": 1234,
  "model": "gemma-4-26b-a4b-it"
}

Respuestas de error

CódigoRazón
403Usuario no autenticado (CF Access requerido)
400Body JSON inválido o question ausente
413Pregunta > 2000 chars
429Rate-limit: >20 queries/minuto
503GOOGLE_AI_API_KEY no configurada
500Error interno del handler o AI Studio

Lógica de ejecución

1. Autenticación y validación

2. Construcción del contexto

3. Llamada a Gemma 4 (decodificación restringida)

Modelo: gemma-4-26b-a4b-it vía Google AI Studio.

Schema JSON forzado:

{
  "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"]
}

Prompt (pseudocódigo):

Eres la secretaria del buzón de [usuario]. Hablas llano y al grano.
Tienes 3 acciones:
- {"action":"responder","answer":"..."} — cuando puedas contestar.
- {"action":"buscar","query|from|subject":"..."} — buscar en el buzón real.
- {"action":"leer","messageId":"...","folderId":"..."} — leer el cuerpo de un mensaje.

Contexto (último digest):
[resumen + ítems + contexto histórico]

Pregunta del usuario: [question]

Responde SIEMPRE con JSON válido. Sin scratchpad.

Timeout: 40s por intento. Si falla por timeout, reintentos automáticos (máx 2 intentos, footgun s88: cold-start en AI Studio puede exceder el primer timeout).

4. Decodificación de la respuesta

Si action == "responder": devuelve answer al cliente y termina.

Si action == "buscar" (máx 3 rondas totales):

Si action == "leer" (máx 3 rondas totales):

Si se alcanzan 3 rondas: devuelve la respuesta más reciente, aunque esté incompleta.

5. Telemetría (best-effort)

Inserta en correo_chat_queries:

Si falla la inserción, se continúa sin error (best-effort).

Componentes internos

htmlToText(html: string): string

Convierte HTML a texto plano legible:

resolveAccountId(env, member, host): Promise<string>

Obtiene el accountId de Zoho Mail del miembro:

toolBuscar(env, member, args): Promise<string>

Búsqueda en el buzón Zoho:

toolLeer(env, member, args): Promise<string>

Lectura de un mensaje concreto:

callGemma(apiKey: string, prompt: string): Promise<ModelAction>

Ejecuta el modelo Gemma 4:

Guardarraíles

LímiteValorRazón
MAX_QUESTION_LEN2000Evitar prompts enormes
MAX_HISTORY_MESSAGES12Contexto acotado sin explotar tokens
MAX_TOOL_ROUNDS3Máx búsquedas/lecturas por pregunta
MAX_BODY_CHARS4000Limitar tamaño de cuerpos leídos
RATE_LIMIT_PER_MIN20Prevenir abuse vía telemetría
TIMEOUT_MS40000Detectar stalls, reintentar

Flujo de ejemplo

  1. Usuario: “¿Qué decía el último aviso de Hetzner?”
  2. Gemma (round 1): {"action":"buscar","from":"alerts@hetzner.de"}
  3. Worker ejecuta toolBuscar, obtiene 3 mensajes de Hetzner, actualiza toolLog.
  4. Gemma (round 2): {"action":"leer","messageId":"msg_123","folderId":"inbox"}
  5. Worker ejecuta toolLeer, obtiene cuerpo (ej. “Your server was rebooted…”).
  6. Gemma (round 3): {"action":"responder","answer":"El último aviso de Hetzner decía que tu servidor se reinició por mantenimiento."}
  7. Devuelve al cliente: { answer: "...", tool_rounds: 2, duration_ms: 1200 }.

Dependencias externas

Datos sensibles

Véase también

Subir