diff --git a/mvp/b2c/src/app/api/retell/webhook/route.ts b/mvp/b2c/src/app/api/retell/webhook/route.ts index 1f3acaf..cff9469 100644 --- a/mvp/b2c/src/app/api/retell/webhook/route.ts +++ b/mvp/b2c/src/app/api/retell/webhook/route.ts @@ -2,6 +2,7 @@ import { desc, eq, like } from 'drizzle-orm'; import { db } from '@/db'; import { leads, leadPipelineEventos } from '@/db/schema'; import { obtenerLlamada, descargarGrabacion } from '@/lib/voice/retell'; +import { analizarTranscripcion } from '@/lib/funnel/analizar-conversacion'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -77,5 +78,16 @@ export async function POST(req: Request): Promise { }, }); - return ok({ matched: true, leadId, transcript: Boolean(detalle.transcript), grabacion: Boolean(audioUrl) }); + // Mismo cerebro de captura que WhatsApp: extrae los campos clave de la transcripción de la llamada. + const analisis = detalle.transcript + ? await analizarTranscripcion(leadId, detalle.transcript, 'llamada') + : { ok: false, error: 'sin transcripción' }; + + return ok({ + matched: true, + leadId, + transcript: Boolean(detalle.transcript), + grabacion: Boolean(audioUrl), + analizado: analisis.ok, + }); } diff --git a/mvp/b2c/src/lib/funnel/analizar-conversacion.ts b/mvp/b2c/src/lib/funnel/analizar-conversacion.ts index 78042f3..60ce63f 100644 --- a/mvp/b2c/src/lib/funnel/analizar-conversacion.ts +++ b/mvp/b2c/src/lib/funnel/analizar-conversacion.ts @@ -10,9 +10,12 @@ export interface AnalisisResultado { error?: string; } +export type OrigenAnalisis = 'whatsapp' | 'llamada' | 'formulario'; + const SYSTEM = `Eres un analista que extrae datos estructurados de una conversación de cualificación de -una reforma entre una agente (LUISA) y un CLIENTE. Lee toda la conversación y devuelve SOLO un objeto -JSON válido (sin texto alrededor, sin markdown) con estas claves; usa null si el dato no aparece: +una reforma entre una agente y un CLIENTE (puede ser un chat de WhatsApp o la transcripción de una +llamada). Lee toda la conversación y devuelve SOLO un objeto JSON válido (sin texto alrededor, sin +markdown) con estas claves; usa null si el dato no aparece: { "tipoReforma": "cocina|bano|salon|comedor|integral|otro", @@ -30,25 +33,19 @@ JSON válido (sin texto alrededor, sin markdown) con estas claves; usa null si e "resumen": una frase con el resumen del lead }`; -// Lee la conversación completa del lead, extrae los campos clave con un LLM y los persiste en el lead. -// Idempotente: se puede llamar en cualquier momento (al cerrar la cualificación o a posteriori). -export async function analizarConversacion(leadId: string): Promise { +// Núcleo agnóstico del canal: dada una transcripción (de WhatsApp, llamada o formulario), extrae los +// campos clave con un LLM y los persiste en el lead. Idempotente. +export async function analizarTranscripcion( + leadId: string, + transcript: string, + origen: OrigenAnalisis, +): Promise { if (!openrouterConfigurado()) return { ok: false, error: 'OPENROUTER_API_KEY no configurada.' }; + if (!transcript || !transcript.trim()) return { ok: false, error: 'Transcripción vacía.' }; const [lead] = await db.select({ id: leads.id }).from(leads).where(eq(leads.id, leadId)).limit(1); if (!lead) return { ok: false, error: 'Lead no encontrado.' }; - const turnos = await db - .select({ rol: conversacionWhatsapp.rol, mensaje: conversacionWhatsapp.mensaje }) - .from(conversacionWhatsapp) - .where(eq(conversacionWhatsapp.leadId, leadId)) - .orderBy(asc(conversacionWhatsapp.createdAt)); - if (turnos.length === 0) return { ok: false, error: 'El lead no tiene conversación.' }; - - const transcript = turnos - .map((t) => `${t.rol === 'user' ? 'CLIENTE' : 'LUISA'}: ${t.mensaje}`) - .join('\n'); - let ex: Record; try { ex = (await chatJSON(SYSTEM, transcript)) as Record; @@ -60,7 +57,7 @@ export async function analizarConversacion(leadId: string): Promise (typeof v === 'string' && v.trim() ? v.trim() : undefined); - const set: Record = { botStep: 'presupuesto', updatedAt: new Date() }; + const set: Record = { updatedAt: new Date() }; const tipoReforma = enumOk(ex.tipoReforma, ['cocina', 'bano', 'salon', 'comedor', 'integral', 'otro']); const calidadGlobal = enumOk(ex.calidadGlobal, ['basica', 'media', 'premium']); const urgencia = enumOk(ex.urgencia, ['alta', 'media', 'baja']); @@ -77,13 +74,32 @@ export async function analizarConversacion(leadId: string): Promise { + const turnos = await db + .select({ rol: conversacionWhatsapp.rol, mensaje: conversacionWhatsapp.mensaje }) + .from(conversacionWhatsapp) + .where(eq(conversacionWhatsapp.leadId, leadId)) + .orderBy(asc(conversacionWhatsapp.createdAt)); + if (turnos.length === 0) return { ok: false, error: 'El lead no tiene conversación.' }; + + const transcript = turnos + .map((t) => `${t.rol === 'user' ? 'CLIENTE' : 'LUISA'}: ${t.mensaje}`) + .join('\n'); + + const r = await analizarTranscripcion(leadId, transcript, 'whatsapp'); + return { ...r, turnos: turnos.length }; }