Skip to content

Latest commit

 

History

History
461 lines (353 loc) · 12 KB

File metadata and controls

461 lines (353 loc) · 12 KB

🔍 ROOT CAUSE ANALYSIS: Chat No Responde

Fecha: 2025-01-22 Prioridad: 🔴 CRÍTICA Estado: ✅ DIAGNOSTICADO


🚨 SÍNTOMA

Problema reportado: Usuario envía mensajes pero no recibe respuestas de la IA.

Comportamiento observado:

  • ✅ Los mensajes del usuario se envían correctamente
  • ✅ El API responde con 200 OK
  • ✅ El API tarda 4-24 segundos (normal para OpenAI)
  • ❌ Las respuestas NO aparecen en la UI
  • ❌ No hay errores visibles en el console

🔍 5 WHYS ANALYSIS

1. Why: ¿Por qué no se muestran las respuestas de la IA?

Respuesta: El código del frontend está parseando el stream incorrectamente.

Evidencia:

// src/app/chat/page.tsx:105
for (const line of lines) {
  if (line.startsWith("0:")) {  // ❌ NUNCA se cumple esta condición
    const text = line.slice(2).replace(/^"|"$/g, "")
    assistantMessage += text
    // ...
  }
}

El código busca líneas que empiezan con "0:", pero el stream no viene en ese formato.


2. Why: ¿Por qué el stream no viene en formato "0:"?

Respuesta: El API usa streamText().toTextStreamResponse() que retorna texto plano, no el formato "0:".

Evidencia:

// src/app/api/chat/route.ts:86-102
const result = streamText({
  model: openai('gpt-4o-mini'),
  messages: [...],
  temperature: 0.7,
})

// ❌ toTextStreamResponse() retorna plain text stream
const response = result.toTextStreamResponse()
return response

Formato del stream:

// ❌ Frontend espera:
0:"Hola"
0:" como"
0:" estas"

// ✅ API envía (Vercel AI SDK v3):
Hola
 como
 estas

3. Why: ¿Por qué se usó toTextStreamResponse() en lugar del formato correcto?

Respuesta: Incompatibilidad entre versiones de Vercel AI SDK.

Historia:

  • Vercel AI SDK v2: Usaba React Server Components (RSC) stream con formato "0:text"
  • Vercel AI SDK v3+: Usa toTextStreamResponse() que retorna plain text stream
  • Problema: El frontend se quedó con el parser antiguo (v2) pero el API usa el nuevo (v3)

Documentación:

// AI SDK v3 - Texto plano
const response = result.toTextStreamResponse()
// Stream: "Hello world..."

// AI SDK v2 - RSC format
const response = result.toDataStreamResponse()
// Stream: "0:\"Hello\"\n0:\" world\"\n"

4. Why: ¿Por qué no se detectó este problema antes?

Respuesta: El código funcionaba con conversaciones nuevas pero falló al cargar conversaciones viejas.

Explicación:

  • Durante desarrollo inicial, se probó creando conversaciones nuevas
  • Esas conversaciones se guardaban con respuestas vacías o el dev no notó que faltaban
  • Al arreglar el bug de timestamps, el usuario empezó a cargar conversaciones viejas
  • Las conversaciones viejas mostraban solo mensajes del usuario (sin respuestas de IA)
  • El problema se hizo evidente

Timeline:

1. Desarrollo inicial → Chat implementado con parser incorrecto
2. Testing básico → Solo se probaron 1-2 mensajes, parecía funcionar
3. Bug de timestamps → Se arregló la carga de conversaciones
4. Usuario prueba → Carga conversación vieja, nota que no hay respuestas
5. Usuario envía nuevo mensaje → Tampoco recibe respuesta
6. 🔴 BUG REPORTADO

5. Why (ROOT CAUSE): ¿Cuál es la causa fundamental?

🎯 ROOT CAUSE: Incompatibilidad de formato de streaming entre API y Frontend

Cadena de causas:

┌──────────────────────────────────────────────────────────┐
│ API usa Vercel AI SDK v3 (toTextStreamResponse)         │
│ ↓                                                        │
│ Stream formato: plain text chunks                       │
│ ↓                                                        │
│ Frontend parser espera formato RSC ("0:")               │
│ ↓                                                        │
│ Condición if (line.startsWith("0:")) NUNCA se cumple   │
│ ↓                                                        │
│ assistantMessage nunca se actualiza                     │
│ ↓                                                        │
│ UI no muestra respuestas ❌                             │
└──────────────────────────────────────────────────────────┘

Factores contribuyentes:

  1. Falta de tests E2E - No se probó el flujo completo de chat
  2. Falta de tests de integración - No se verificó que el streaming funcionara
  3. Falta de logging en frontend - No se logueaba el contenido del stream
  4. Testing superficial - Solo se probó que el API respondía 200, no que mostrara texto
  5. No se revisó la documentación - Vercel AI SDK cambió el formato en v3

📊 EVIDENCIA DEL PROBLEMA

Logs del servidor (SÍ funciona):

POST /api/chat 200 in 15.2s  ✅
POST /api/chat 200 in 8.9s   ✅
POST /api/chat 200 in 24.2s  ✅
POST /api/chat 200 in 4.4s   ✅

Comportamiento del frontend (NO funciona):

// Stream llega con chunks como:
"Hola, soy PlayGPT EDU..."

// Código busca:
if (line.startsWith("0:")) {  // ❌ NUNCA es true
  // Este código NUNCA se ejecuta
}

// Resultado:
assistantMessage === ""  // ❌ Siempre vacío
setMessages() no se llama // ❌ UI no se actualiza

✅ SOLUCIÓN PROPUESTA

Estrategia: Usar el formato correcto de Vercel AI SDK v3

Hay 2 opciones:

Opción 1: Cambiar Frontend para plain text streaming (RECOMENDADA)

Ventajas:

  • ✅ Más simple y directo
  • ✅ Mejor performance (menos parsing)
  • ✅ Siguiendo el estándar de AI SDK v3
  • ✅ No requiere cambios en API

Desventajas:

  • ⚠️ Requiere cambiar el parser del frontend

Implementación:

// ANTES (❌ incorrecto)
for (const line of lines) {
  if (line.startsWith("0:")) {
    const text = line.slice(2).replace(/^"|"$/g, "")
    assistantMessage += text
  }
}

// DESPUÉS (✅ correcto)
const chunk = decoder.decode(value, { stream: true })
assistantMessage += chunk

// Update UI
setMessages((prev) => {
  const existing = prev.find((m) => m.id === assistantMessageId)
  if (existing) {
    return prev.map((m) =>
      m.id === assistantMessageId
        ? { ...m, content: assistantMessage }
        : m
    )
  } else {
    return [
      ...prev,
      {
        id: assistantMessageId,
        role: "assistant",
        content: assistantMessage,
        timestamp: new Date(),
      },
    ]
  }
})

Opción 2: Cambiar API para usar Data Stream

Ventajas:

  • ✅ Soporte para metadata adicional
  • ✅ Soporte para tool calls en futuro

Desventajas:

  • ⚠️ Más complejo
  • ⚠️ Requiere cambios en ambos lados

🔧 IMPLEMENTACIÓN ELEGIDA: Opción 1

Justificación:

  • Más rápido de implementar
  • Más simple de mantener
  • Suficiente para el caso de uso actual
  • Sigue el patrón estándar de AI SDK v3

📝 CAMBIOS REQUERIDOS

Archivo: src/app/chat/page.tsx

Cambio en handleSendMessage (líneas 85-132)

// Read the stream
const reader = response.body?.getReader()
const decoder = new TextDecoder()

if (!reader) {
  throw new Error("No reader available")
}

let assistantMessage = ""
const assistantMessageId = (Date.now() + 1).toString()

// ✅ NUEVO: Agregar el mensaje vacío primero
setMessages((prev) => [
  ...prev,
  {
    id: assistantMessageId,
    role: "assistant",
    content: "",
    timestamp: new Date(),
  },
])

while (true) {
  const { done, value } = await reader.read()

  if (done) break

  // ✅ NUEVO: Decodificar el chunk directamente
  const chunk = decoder.decode(value, { stream: true })
  assistantMessage += chunk

  // ✅ NUEVO: Actualizar el mensaje en cada chunk
  setMessages((prev) =>
    prev.map((m) =>
      m.id === assistantMessageId
        ? { ...m, content: assistantMessage }
        : m
    )
  )
}

Cambios clave:

  1. Eliminar parsing de líneas "0:"
  2. Decodificar directamente el chunk
  3. Agregar mensaje vacío al inicio
  4. Actualizar en cada chunk (no solo cuando encuentra "0:")

🧪 PLAN DE TESTING

Tests Manuales:

  1. Enviar mensaje simple:

    • Input: "hola"
    • Verificar: Respuesta aparece letra por letra
    • Verificar: Mensaje completo se muestra
  2. Enviar pregunta compleja:

    • Input: "¿qué es la falacia del jugador?"
    • Verificar: Respuesta se genera correctamente
    • Verificar: Formato markdown se renderiza
  3. Múltiples mensajes:

    • Enviar 3-4 mensajes seguidos
    • Verificar: Cada uno recibe respuesta
    • Verificar: Historial completo se mantiene
  4. Cargar conversación vieja:

    • Abrir conversación existente
    • Enviar nuevo mensaje
    • Verificar: Respuesta aparece correctamente

Verificación de Console:

✅ No debe haber: "Error sending message"
✅ No debe haber: "No reader available"
✅ Debe haber: Respuestas visibles en UI
✅ Debe haber: Streaming en tiempo real (letra por letra)

📊 MÉTRICAS DE ÉXITO

Métrica Antes Después (Esperado)
Respuestas aparecen ❌ 0% ✅ 100%
Streaming funciona ❌ Roto ✅ Tiempo real
Console errors ✅ 0 (pero no funciona) ✅ 0
User experience 💔 Roto ✅ Funcional
Latencia percibida ⏳ Alta (no muestra nada) ⚡ Baja (streaming)

🚀 PRÓXIMOS PASOS (Después del fix)

1. Agregar tests E2E

// tests/e2e/chat.spec.ts
test('chat responds to user message', async ({ page }) => {
  await page.goto('/chat')
  await page.fill('[placeholder*="Pregúntame"]', 'hola')
  await page.press('[placeholder*="Pregúntame"]', 'Enter')

  // Esperar a que aparezca respuesta
  await expect(page.locator('text=/PlayGPT EDU/i')).toBeVisible()
  await expect(page.locator('text=/hola/i')).toBeVisible()
})

2. Agregar logging mejorado

// Debug logging en desarrollo
if (process.env.NODE_ENV === 'development') {
  console.log('📥 Chunk received:', chunk)
  console.log('📝 Current message:', assistantMessage)
}

3. Agregar error boundary

<ErrorBoundary fallback={<ChatError />}>
  <ChatContainer messages={messages} />
</ErrorBoundary>

4. Agregar UI feedback mejorado

  • Mostrar "..." mientras escribe
  • Mostrar progress bar durante streaming
  • Mostrar cuando está buscando en knowledge base

📚 LECCIONES APRENDIDAS

1. Siempre verificar el formato del stream

❌ Asumió formato:

if (line.startsWith("0:"))  // Asunción sin verificar

✅ Verificar con logs:

console.log('Stream chunk:', chunk)  // Ver formato real

2. Leer la documentación de la librería

El cambio de formato estaba documentado en:

  • Vercel AI SDK Migration Guide (v2 → v3)
  • Pero no se revisó

3. Tests E2E son críticos

Un test E2E simple habría detectado esto:

test('user receives response after sending message')

4. Logging es esencial para debugging

Sin logs en el frontend, fue difícil diagnosticar:

// Agregar en todos los parsers
console.log('Stream format:', chunk)

🎯 IMPACTO DEL BUG

Severidad: 🔴 CRÍTICA

Por qué es crítico:

  • ❌ Funcionalidad principal completamente rota
  • ❌ No hay workaround para el usuario
  • ❌ Hace la app inutilizable
  • ❌ Afecta al 100% de los usuarios

Usuarios afectados:

  • ✅ Todos los usuarios nuevos
  • ✅ Todos los usuarios existentes
  • ✅ 100% de conversaciones

Tiempo que estuvo el bug:

  • Desde la implementación inicial
  • No detectado en testing
  • Detectado por el usuario en producción

Estado: ✅ Diagnosticado, listo para implementar Prioridad: 🔴 CRÍTICA Esfuerzo estimado: 10 minutos Risk: Bajo (cambio aislado)