Configuracion de agente de whastapp paratrabajar con la estructura propuesta

This commit is contained in:
unknown
2026-06-07 17:51:53 -04:00
parent d3189d7277
commit fec365bb57
28 changed files with 5316 additions and 1748 deletions

View File

@@ -2,7 +2,6 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
import { Lead } from '../leads/lead.entity';
import { LeadsService } from '../leads/leads.service';
const DEFAULT_SYSTEM_PROMPT =
@@ -34,9 +33,23 @@ export interface ValidacionResultado {
viable?: boolean;
}
export interface LeadBasico {
id: string;
telefono: string;
nombre: string;
estado_actual: string;
espacio: string | null;
rango_m2: string | null;
estilo: string | null;
urgencia: string | null;
presupuesto_declarado: string | null;
viable: boolean | null;
email: string | null;
}
export interface ClaudeResponse {
respuesta: string;
entidad?: Partial<Lead>;
entidad?: Partial<LeadBasico>;
viable?: boolean;
nuevoEstado?: string;
}
@@ -47,73 +60,39 @@ export class ClaudeService implements OnModuleInit {
private readonly promptsDir = path.join(process.cwd(), 'prompts');
private systemPromptCache = '';
private reglasPromptCache = '';
private readonly reintentosPorLead = new Map<
string,
{ estado: string; count: number }
>();
private readonly reintentosPorLead = new Map<string, { estado: string; count: number }>();
constructor(private readonly leadsService: LeadsService) {}
onModuleInit() {
this.systemPromptCache = this.cargarPrompts([
'luisa_core.md',
'luisa_flujo.md',
'luisa_casos.md',
]);
this.systemPromptCache = this.cargarPrompts(['luisa_core.md', 'luisa_flujo.md', 'luisa_casos.md']);
this.reglasPromptCache = this.cargarPrompts(['luisa_core.md', 'luisa_casos.md']);
this.logger.log(
`Prompts cargados: system=${this.systemPromptCache.length} chars, reglas=${this.reglasPromptCache.length} chars`,
);
this.logger.log(`Prompts cargados: system=${this.systemPromptCache.length} chars, reglas=${this.reglasPromptCache.length} chars`);
}
private cargarPrompts(archivos: string[]): string {
const partes: string[] = [];
for (const archivo of archivos) {
const rutaCompleta = path.join(this.promptsDir, archivo);
try {
if (!fs.existsSync(rutaCompleta)) {
this.logger.warn(`Prompt no encontrado: ${archivo}`);
continue;
}
if (!fs.existsSync(rutaCompleta)) { this.logger.warn(`Prompt no encontrado: ${archivo}`); continue; }
const contenido = fs.readFileSync(rutaCompleta, 'utf-8');
if (contenido.trim()) {
partes.push(`\n\n## ${archivo}\n${contenido}`);
}
} catch {
this.logger.warn(`No se pudo leer el prompt: ${archivo}`);
}
if (contenido.trim()) partes.push(`\n\n## ${archivo}\n${contenido}`);
} catch { this.logger.warn(`No se pudo leer el prompt: ${archivo}`); }
}
const concatenado = partes.join('\n').trim();
return concatenado || DEFAULT_SYSTEM_PROMPT;
}
private leerPromptsSistema(): string {
return this.systemPromptCache || DEFAULT_SYSTEM_PROMPT;
}
private leerPromptsReglas(): string {
return this.reglasPromptCache || DEFAULT_SYSTEM_PROMPT;
return partes.join('\n').trim() || DEFAULT_SYSTEM_PROMPT;
}
private getModelo(clave: 'clasificador' | 'generador' | 'reglas'): string {
const defaults = {
clasificador: 'anthropic/claude-haiku-4-5',
generador: 'anthropic/claude-sonnet-4-5',
reglas: 'anthropic/claude-haiku-4-5',
};
const envMap = {
const envMap: Record<string, string | undefined> = {
clasificador: process.env.MODEL_CLASIFICADOR,
generador: process.env.MODEL_GENERADOR || process.env.MODEL,
reglas: process.env.MODEL_REGLAS || process.env.MODEL_CLASIFICADOR,
};
return envMap[clave] || defaults[clave];
return envMap[clave] || (clave === 'generador' ? 'anthropic/claude-sonnet-4-5' : 'anthropic/claude-haiku-4-5');
}
private serializarLead(lead: Lead): string {
private serializarLead(lead: LeadBasico): string {
return [
`- ID: ${lead.id}`,
`- Telefono: ${lead.telefono}`,
@@ -129,10 +108,6 @@ export class ClaudeService implements OnModuleInit {
].join('\n');
}
/**
* OpenRouter requiere system dentro de messages[] para modelos OpenAI.
* El campo system en la raiz del payload no siempre se aplica.
*/
private async llamarOpenRouter(
model: string,
system: string,
@@ -140,95 +115,47 @@ export class ClaudeService implements OnModuleInit {
options: { temperature?: number; jsonMode?: boolean } = {},
): Promise<string> {
const { temperature = 0.7, jsonMode = false } = options;
const payload: Record<string, unknown> = {
model,
messages: [{ role: 'system', content: system }, ...messages],
max_tokens: 1024,
temperature,
};
if (jsonMode) payload.response_format = { type: 'json_object' };
if (jsonMode) {
payload.response_format = { type: 'json_object' };
}
const response = await axios.post(
'https://openrouter.ai/api/v1/chat/completions',
payload,
{
headers: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://reformix.es',
'X-Title': 'Reformix Luisa Bot',
},
const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', payload, {
headers: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://reformix.es',
'X-Title': 'Reformix Luisa Bot',
},
);
const contenido = response.data.choices?.[0]?.message?.content || '';
const modeloUsado = response.data.model || model;
if (!contenido.trim()) {
this.logger.warn(
`OpenRouter devolvio contenido vacio (modelo=${modeloUsado})`,
);
}
return contenido;
});
return response.data.choices?.[0]?.message?.content || '';
}
private parsearJson<T>(texto: string): T | null {
const limpio = texto
.replace(/```json\s*/gi, '')
.replace(/```\s*/g, '')
.trim();
const limpio = texto.replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
const inicio = limpio.indexOf('{');
const fin = limpio.lastIndexOf('}');
if (inicio === -1 || fin === -1) return null;
try {
return JSON.parse(limpio.slice(inicio, fin + 1)) as T;
} catch {
return null;
}
try { return JSON.parse(limpio.slice(inicio, fin + 1)) as T; } catch { return null; }
}
private normalizarClasificacion(
raw: Partial<ClasificacionResultado>,
): ClasificacionResultado | null {
const intenciones = [
'respuesta',
'desvio',
'despedida',
'insulto',
'pregunta',
] as const;
private normalizarClasificacion(raw: Partial<ClasificacionResultado>): ClasificacionResultado | null {
const intenciones = ['respuesta', 'desvio', 'despedida', 'insulto', 'pregunta'] as const;
if (!raw || typeof raw.responde_pregunta !== 'boolean') return null;
const intencion = intenciones.includes(raw.intencion as typeof intenciones[number])
? (raw.intencion as ClasificacionResultado['intencion'])
: 'respuesta';
? (raw.intencion as ClasificacionResultado['intencion']) : 'respuesta';
return {
responde_pregunta: raw.responde_pregunta,
valor_extraido:
raw.valor_extraido === null || raw.valor_extraido === undefined
? null
: String(raw.valor_extraido),
valor_extraido: raw.valor_extraido === null || raw.valor_extraido === undefined ? null : String(raw.valor_extraido),
es_desvio: Boolean(raw.es_desvio),
intencion,
};
}
/**
* Capa 1 — Clasificador (Haiku): extrae intencion y valor del mensaje.
*/
private async clasificar(
mensaje: string,
estadoActual: string,
): Promise<ClasificacionResultado> {
private async clasificar(mensaje: string, estadoActual: string): Promise<ClasificacionResultado> {
const valoresPermitidos = this.leadsService.getValoresPermitidos(estadoActual);
const system = `Eres un clasificador de mensajes para un bot de cualificacion de leads de reformas.
Responde UNICAMENTE con un objeto JSON valido. Sin markdown, sin texto antes ni despues.
@@ -247,132 +174,63 @@ Formato exacto:
Valores validos de intencion: respuesta, desvio, despedida, insulto, pregunta
Reglas para valor_extraido:
- espacio: cocina, bano, salon, integral, otro
- espacio: cocina, bano, salon, comedor, integral, otro
- tamano: menos10, 10a20, 20a40, mas40
- estilo: funcional, cuidado, exclusivo
- urgencia: urgente, medio_plazo, frio
- urgencia: alta, media, baja
- presupuesto: numero o rango en euros tal como lo dijo el usuario
- apertura: null si solo confirma disponibilidad; extrae nombre si lo menciona
- Si el usuario pregunta algo (nombre, precios, etc.) usa intencion "pregunta" y responde_pregunta false
- Saludos casuales sin confirmar disponibilidad: intencion "desvio", es_desvio true
- Usuarios de Madrid/Espana: interpreta coloquialismos, jerga y dialecto peninsular (vale, tio, mola, guay, etc.) como respuesta valida si aportan el dato del estado
- Extrae el valor semantico aunque venga en lenguaje coloquial ("pa la cocina" -> espacio cocina, "unos 15 mil" -> presupuesto)`;
const intentos = [
{ jsonMode: true, temperature: 0.1 },
{ jsonMode: true, temperature: 0 },
];
- Extrae el valor semantico aunque venga en lenguaje coloquial`;
const intentos = [{ jsonMode: true, temperature: 0.1 }, { jsonMode: true, temperature: 0 }];
for (const opts of intentos) {
const contenido = await this.llamarOpenRouter(
this.getModelo('clasificador'),
system,
[{ role: 'user', content: mensaje }],
opts,
);
const parsed = this.normalizarClasificacion(
this.parsearJson<Partial<ClasificacionResultado>>(contenido) ?? {},
);
const contenido = await this.llamarOpenRouter(this.getModelo('clasificador'), system, [{ role: 'user', content: mensaje }], opts);
const parsed = this.normalizarClasificacion(this.parsearJson<Partial<ClasificacionResultado>>(contenido) ?? {});
if (parsed) return parsed;
this.logger.warn(
`Clasificador JSON invalido (intento, modelo=${this.getModelo('clasificador')}): ${contenido.slice(0, 200)}`,
);
this.logger.warn(`Clasificador JSON invalido (intento): ${contenido.slice(0, 200)}`);
}
this.logger.warn('Clasificador agotado reintentos, usando fallback conservador');
return {
responde_pregunta: false,
valor_extraido: null,
es_desvio: true,
intencion: 'desvio',
};
return { responde_pregunta: false, valor_extraido: null, es_desvio: true, intencion: 'desvio' };
}
/**
* Capa 2 — Validador en codigo: valida valor_extraido contra valores permitidos.
*/
private validar(
clasificacion: ClasificacionResultado,
estadoActual: string,
): ValidacionResultado {
private validar(clasificacion: ClasificacionResultado, estadoActual: string): ValidacionResultado {
const estado = this.leadsService.normalizarEstadoFlujo(estadoActual);
if (
clasificacion.es_desvio ||
clasificacion.intencion === 'desvio' ||
clasificacion.intencion === 'pregunta' ||
clasificacion.intencion === 'insulto' ||
clasificacion.intencion === 'despedida'
) {
if (clasificacion.es_desvio || clasificacion.intencion === 'desvio' || clasificacion.intencion === 'pregunta' ||
clasificacion.intencion === 'insulto' || clasificacion.intencion === 'despedida') {
return { valido: false, valorNormalizado: null };
}
if (estado === 'nuevo') {
return { valido: false, valorNormalizado: null };
}
if (estado === 'nuevo') return { valido: false, valorNormalizado: null };
if (estado === 'apertura') {
const valido =
clasificacion.responde_pregunta &&
clasificacion.intencion === 'respuesta' &&
!clasificacion.es_desvio;
return { valido, valorNormalizado: clasificacion.valor_extraido };
return { valido: clasificacion.responde_pregunta && clasificacion.intencion === 'respuesta' && !clasificacion.es_desvio, valorNormalizado: clasificacion.valor_extraido };
}
if (estado === 'presupuesto') {
const valor = clasificacion.valor_extraido?.trim();
if (!valor || !this.leadsService.esPresupuestoValido(valor)) {
return { valido: false, valorNormalizado: null };
}
const viable = this.leadsService.evaluarViabilidad(valor);
return { valido: true, valorNormalizado: valor, viable };
if (!valor || !this.leadsService.esPresupuestoValido(valor)) return { valido: false, valorNormalizado: null };
return { valido: true, valorNormalizado: valor, viable: this.leadsService.evaluarViabilidad(valor) };
}
const valoresPermitidos = this.leadsService.getValoresPermitidos(estado);
const valor = this.normalizarTexto(clasificacion.valor_extraido ?? '');
if (!valor) {
return { valido: false, valorNormalizado: null };
}
const coincide = valoresPermitidos.some(
(v) => v === valor || valor.includes(v) || v.includes(valor),
);
if (!coincide) {
return { valido: false, valorNormalizado: null };
}
const valorNormalizado =
valoresPermitidos.find(
(v) => v === valor || valor.includes(v) || v.includes(valor),
) ?? valor;
if (!valor) return { valido: false, valorNormalizado: null };
const coincide = valoresPermitidos.some((v) => v === valor || valor.includes(v) || v.includes(valor));
if (!coincide) return { valido: false, valorNormalizado: null };
const valorNormalizado = valoresPermitidos.find((v) => v === valor || valor.includes(v) || v.includes(valor)) ?? valor;
return { valido: true, valorNormalizado };
}
private normalizarTexto(valor: string): string {
return valor
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '');
return valor.trim().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '');
}
private claveReintento(leadId: number, estado: string): string {
return `${leadId}:${estado}`;
}
private claveReintento(leadId: string, estado: string): string { return `${leadId}:${estado}`; }
private obtenerReintentos(leadId: number, estado: string): number {
const clave = this.claveReintento(leadId, estado);
const entry = this.reintentosPorLead.get(clave);
private obtenerReintentos(leadId: string, estado: string): number {
const entry = this.reintentosPorLead.get(this.claveReintento(leadId, estado));
return entry?.estado === estado ? entry.count : 0;
}
private incrementarReintentos(leadId: number, estado: string): number {
private incrementarReintentos(leadId: string, estado: string): number {
const clave = this.claveReintento(leadId, estado);
const actual = this.obtenerReintentos(leadId, estado);
const count = actual + 1;
@@ -380,15 +238,12 @@ Reglas para valor_extraido:
return count;
}
private resetearReintentos(leadId: number, estado: string): void {
private resetearReintentos(leadId: string, estado: string): void {
this.reintentosPorLead.delete(this.claveReintento(leadId, estado));
}
/**
* Capa 3 — Generador (Sonnet): produce el borrador del mensaje de Luisa.
*/
private async generar(
lead: Lead,
lead: LeadBasico,
historial: Array<{ role: string; content: string }>,
mensajeActual: string,
clasificacion: ClasificacionResultado,
@@ -398,11 +253,7 @@ Reglas para valor_extraido:
siguienteEstado: string | null,
forzarApertura = false,
): Promise<string> {
const systemPrompt = this.leerPromptsSistema();
const estadoFlujo = this.leadsService.normalizarEstadoFlujo(
lead.estado_actual,
);
const estadoFlujo = this.leadsService.normalizarEstadoFlujo(lead.estado_actual);
const contextoGeneracion = `
## Contexto del lead
${this.serializarLead(lead)}
@@ -419,11 +270,10 @@ ${this.serializarLead(lead)}
## Instrucciones de respuesta
Eres Luisa de Reformix en Madrid. Sigue el system prompt al pie de la letra.
Habla espanol de Espana; suena natural y cercana. Adapta el registro al usuario (coloquial si el, formal si el).
Habla espanol de Espana; suena natural y cercana.
Devuelve SOLO el texto del mensaje a enviar por WhatsApp.
NO incluyas JSON, etiquetas XML ni bloques de datos extraidos.
NUNCA digas que eres IA, OpenAI, ChatGPT ni un asistente virtual.
Si preguntan tu nombre, di que eres Luisa de Reformix.
Si forzar mensaje de apertura es true, envia el mensaje de APERTURA del flujo.
Si validacion valida es false y reintentos < 2, pide amablemente que aclare su respuesta.
@@ -432,32 +282,16 @@ Si es_desvio es true o intencion es pregunta, responde brevemente como Luisa y r
Si avanzar estado es true, haz la pregunta correspondiente al siguiente estado.
Si el siguiente estado es fin_viable o fin_no_viable, usa el mensaje de cierre correspondiente.`;
const messages = [
...historial,
{ role: 'user', content: mensajeActual },
];
const contenido = await this.llamarOpenRouter(
this.getModelo('generador'),
`${systemPrompt}\n${contextoGeneracion}`,
messages,
const contenido = await this.llamarOpenRouter(this.getModelo('generador'),
`${this.systemPromptCache || DEFAULT_SYSTEM_PROMPT}\n${contextoGeneracion}`,
[...historial, { role: 'user', content: mensajeActual }],
{ temperature: 0.7 },
);
return contenido.trim();
}
/**
* Capa 4 — Reglas (Haiku): corrige el borrador para cumplir identidad y tono de Luisa.
*/
private async aplicarReglas(
borrador: string,
lead: Lead,
estadoFlujo: string,
clasificacion: ClasificacionResultado,
): Promise<string> {
const reglas = this.leerPromptsReglas();
private async aplicarReglas(borrador: string, lead: LeadBasico, estadoFlujo: string, clasificacion: ClasificacionResultado): Promise<string> {
const reglas = this.reglasPromptCache || DEFAULT_SYSTEM_PROMPT;
const system = `${reglas}
## Tu tarea
@@ -471,19 +305,16 @@ Devuelve SOLO el mensaje final listo para enviar, sin explicaciones ni JSON.
## Reglas de correccion obligatorias
- Debe sonar como Luisa de Reformix (Madrid), nunca como un asistente generico
- Espanol de Espana, natural; puede usar coloquialismos suaves (vale, mira, oye) si encaja con el tono del usuario
- Espanol de Espana, natural; puede usar coloquialismos suaves (vale, mira, oye) si encaja
- Maximo 2 lineas, un mensaje por turno, sin emojis, sin guiones largos
- NUNCA mencionar IA, OpenAI, ChatGPT, modelos de lenguaje ni asistentes virtuales
- Si preguntan el nombre: "Soy Luisa de Reformix"
- Si el borrador viola alguna regla, reescribelo completamente manteniendo la intencion`;
const contenido = await this.llamarOpenRouter(
this.getModelo('reglas'),
system,
const contenido = await this.llamarOpenRouter(this.getModelo('reglas'), system,
[{ role: 'user', content: `Borrador a corregir:\n${borrador}` }],
{ temperature: 0.3 },
);
return contenido.trim() || borrador;
}
@@ -491,7 +322,7 @@ Devuelve SOLO el mensaje final listo para enviar, sin explicaciones ni JSON.
return FRASES_IA_PROHIBIDAS.some((regex) => regex.test(texto));
}
private mensajeFallback(estadoFlujo: string, lead: Lead): string {
private mensajeFallback(estadoFlujo: string, lead: LeadBasico): string {
const nombre = lead.nombre ? lead.nombre : '';
const fallbacks: Record<string, string> = {
nuevo: `Hola${nombre ? ' ' + nombre : ''}, soy Luisa de Reformix; vi que dejaste tus datos en nuestra web y queria ayudarte a preparar tu presupuesto. Tienes unos minutos ahora?`,
@@ -502,91 +333,23 @@ Devuelve SOLO el mensaje final listo para enviar, sin explicaciones ni JSON.
urgencia: 'Y cuando tienes pensado arrancar, es algo proximo o todavia estas explorando?',
presupuesto: 'Ultima pregunta; tienes en mente un presupuesto aproximado para la reforma?',
};
return (
fallbacks[estadoFlujo] ??
'Soy Luisa de Reformix; sigamos con tu presupuesto de reforma.'
);
return fallbacks[estadoFlujo] ?? 'Soy Luisa de Reformix; sigamos con tu presupuesto de reforma.';
}
/**
* Orquesta las 4 capas: clasificar, validar, generar y aplicar reglas.
*/
async llamarClaude(
lead: Lead,
lead: LeadBasico,
historial: Array<{ role: string; content: string }>,
mensajeActual: string,
): Promise<ClaudeResponse> {
const esAperturaScheduler =
historial.length === 0 && mensajeActual.startsWith('APERTURA:');
if (esAperturaScheduler) {
const borrador = await this.generar(
lead,
historial,
mensajeActual,
{
responde_pregunta: true,
valor_extraido: null,
es_desvio: false,
intencion: 'respuesta',
},
{ valido: true, valorNormalizado: null },
0,
false,
'apertura',
true,
);
const respuesta = await this.aplicarReglas(
borrador,
lead,
'apertura',
{
responde_pregunta: true,
valor_extraido: null,
es_desvio: false,
intencion: 'respuesta',
},
);
return {
respuesta: this.contieneFraseProhibida(respuesta)
? this.mensajeFallback('apertura', lead)
: respuesta,
};
}
const estadoFlujo = this.leadsService.normalizarEstadoFlujo(
lead.estado_actual,
);
const estadoFlujo = this.leadsService.normalizarEstadoFlujo(lead.estado_actual);
if (estadoFlujo === 'nuevo') {
const clasificacion: ClasificacionResultado = {
responde_pregunta: false,
valor_extraido: null,
es_desvio: false,
intencion: 'respuesta',
};
const borrador = await this.generar(
lead,
historial,
mensajeActual,
clasificacion,
{ valido: false, valorNormalizado: null },
0,
false,
null,
true,
);
const respuesta = await this.aplicarReglas(
borrador,
lead,
'nuevo',
clasificacion,
);
const clasificacion: ClasificacionResultado = { responde_pregunta: false, valor_extraido: null, es_desvio: false, intencion: 'respuesta' };
const borrador = await this.generar(lead, historial, mensajeActual, clasificacion,
{ valido: false, valorNormalizado: null }, 0, false, null, true);
const respuesta = await this.aplicarReglas(borrador, lead, 'nuevo', clasificacion);
return {
respuesta: this.contieneFraseProhibida(respuesta)
? this.mensajeFallback('nuevo', lead)
: respuesta,
respuesta: this.contieneFraseProhibida(respuesta) ? this.mensajeFallback('nuevo', lead) : respuesta,
nuevoEstado: 'apertura',
};
}
@@ -597,72 +360,38 @@ Devuelve SOLO el mensaje final listo para enviar, sin explicaciones ni JSON.
let reintentos = this.obtenerReintentos(lead.id, estadoFlujo);
let avanzarEstado = false;
let siguienteEstado: string | null = null;
let entidad: Partial<Lead> = {};
let entidad: Partial<LeadBasico> = {};
let viable: boolean | undefined;
const puedeAvanzar =
validacion.valido &&
!clasificacion.es_desvio &&
clasificacion.intencion === 'respuesta';
const puedeAvanzar = validacion.valido && !clasificacion.es_desvio && clasificacion.intencion === 'respuesta';
if (puedeAvanzar) {
avanzarEstado = true;
this.resetearReintentos(lead.id, estadoFlujo);
if (validacion.valorNormalizado) {
const campo = this.leadsService.getCampoParaEstado(estadoFlujo);
if (campo) {
entidad = { [campo]: validacion.valorNormalizado };
} else if (
estadoFlujo === 'apertura' &&
clasificacion.valor_extraido?.trim()
) {
entidad = { nombre: clasificacion.valor_extraido.trim() };
(entidad as any)[campo] = validacion.valorNormalizado;
} else if (estadoFlujo === 'apertura' && clasificacion.valor_extraido?.trim()) {
entidad.nombre = clasificacion.valor_extraido.trim();
}
}
if (estadoFlujo === 'presupuesto') {
viable = validacion.viable;
siguienteEstado = this.leadsService.getSiguienteEstado(
estadoFlujo,
viable,
);
siguienteEstado = this.leadsService.getSiguienteEstado(estadoFlujo, viable);
} else {
siguienteEstado = this.leadsService.getSiguienteEstado(estadoFlujo);
}
} else if (
!validacion.valido &&
clasificacion.responde_pregunta &&
!clasificacion.es_desvio
) {
} else if (!validacion.valido && clasificacion.responde_pregunta && !clasificacion.es_desvio) {
reintentos = this.incrementarReintentos(lead.id, estadoFlujo);
if (reintentos > 2) {
reintentos = 2;
}
if (reintentos > 2) reintentos = 2;
}
const borrador = await this.generar(
lead,
historial,
mensajeActual,
clasificacion,
validacion,
reintentos,
avanzarEstado,
siguienteEstado,
);
let respuesta = await this.aplicarReglas(
borrador,
lead,
estadoFlujo,
clasificacion,
);
const borrador = await this.generar(lead, historial, mensajeActual, clasificacion, validacion, reintentos, avanzarEstado, siguienteEstado);
let respuesta = await this.aplicarReglas(borrador, lead, estadoFlujo, clasificacion);
if (this.contieneFraseProhibida(respuesta)) {
this.logger.warn(
`Respuesta final viola reglas de identidad, usando fallback para estado=${estadoFlujo}`,
);
this.logger.warn(`Respuesta final viola reglas, usando fallback para estado=${estadoFlujo}`);
respuesta = this.mensajeFallback(estadoFlujo, lead);
}