Configuracion de prompts PENDIENTE
This commit is contained in:
@@ -74,6 +74,11 @@ export class ClaudeService {
|
||||
): Promise<ClaudeResponse> {
|
||||
const systemPrompt = this.leerPromptsSistema();
|
||||
|
||||
console.log('=== DEBUG SYSTEM PROMPT ===');
|
||||
console.log('Longitud systemPrompt:', systemPrompt.length);
|
||||
console.log('Primeros 500 caracteres:', systemPrompt.substring(0, 500));
|
||||
console.log('=== FIN DEBUG ===');
|
||||
|
||||
const contextoDeLead = `
|
||||
## Contexto del lead actual
|
||||
|
||||
@@ -109,16 +114,24 @@ Si puedes determinar si el lead es viable o no, pon true o false en "viable". Si
|
||||
{ role: 'user', content: mensajeActual },
|
||||
];
|
||||
|
||||
// Construimos el payload para enviar a OpenRouter
|
||||
const payload = {
|
||||
model: process.env.MODEL || 'anthropic/claude-sonnet-4-5',
|
||||
messages: messages,
|
||||
system: systemFinal,
|
||||
max_tokens: 1024,
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
// LOG DE LO QUE SE ENVÍA (exactamente lo que ve la IA)
|
||||
console.log('\n======= PETICIÓN A OPENROUTER =======');
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
console.log('=====================================\n');
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
{
|
||||
model: process.env.MODEL || 'anthropic/claude-sonnet-4-5',
|
||||
messages,
|
||||
system: systemFinal,
|
||||
max_tokens: 1024,
|
||||
temperature: 0.7,
|
||||
},
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||
@@ -129,24 +142,28 @@ Si puedes determinar si el lead es viable o no, pon true o false en "viable". Si
|
||||
},
|
||||
);
|
||||
|
||||
const contenidoCompleto: string =
|
||||
response.data.choices?.[0]?.message?.content || '';
|
||||
// LOG DE LA RESPUESTA CRUDA (lo que devuelve OpenRouter)
|
||||
console.log('\n======= RESPUESTA CRUDA DE OPENROUTER =======');
|
||||
console.log(JSON.stringify(response.data, null, 2));
|
||||
console.log('=============================================\n');
|
||||
|
||||
const contenidoCompleto: string = response.data.choices?.[0]?.message?.content || '';
|
||||
|
||||
// También imprimimos el contenido del mensaje para ver lo que generó la IA
|
||||
console.log('\n======= CONTENIDO DEL MENSAJE GENERADO =======');
|
||||
console.log(contenidoCompleto);
|
||||
console.log('================================================\n');
|
||||
|
||||
// Separar la respuesta visible del bloque de datos extraídos
|
||||
const regexDatos = /<DATOS_EXTRAIDOS>([\s\S]*?)<\/DATOS_EXTRAIDOS>/;
|
||||
const match = contenidoCompleto.match(regexDatos);
|
||||
|
||||
let respuesta = contenidoCompleto
|
||||
.replace(regexDatos, '')
|
||||
.trim();
|
||||
|
||||
let respuesta = contenidoCompleto.replace(regexDatos, '').trim();
|
||||
let entidad: Partial<Lead> = {};
|
||||
let viableFlag: boolean | undefined = undefined;
|
||||
|
||||
if (match) {
|
||||
try {
|
||||
const datos = JSON.parse(match[1].trim());
|
||||
// Solo incluir campos no nulos
|
||||
Object.entries(datos).forEach(([k, v]) => {
|
||||
if (v !== null && k !== 'viable') {
|
||||
(entidad as Record<string, unknown>)[k] = v;
|
||||
@@ -155,8 +172,8 @@ Si puedes determinar si el lead es viable o no, pon true o false en "viable". Si
|
||||
if (datos.viable !== null && datos.viable !== undefined) {
|
||||
viableFlag = datos.viable;
|
||||
}
|
||||
} catch {
|
||||
this.logger.warn('No se pudo parsear DATOS_EXTRAIDOS');
|
||||
} catch (e) {
|
||||
this.logger.warn('No se pudo parsear DATOS_EXTRAIDOS: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,21 @@ import {
|
||||
import makeWASocket, {
|
||||
DisconnectReason,
|
||||
useMultiFileAuthState,
|
||||
fetchLatestBaileysVersion,
|
||||
WASocket,
|
||||
downloadMediaMessage,
|
||||
proto,
|
||||
} from '@whiskeysockets/baileys';
|
||||
import { Boom } from '@hapi/boom';
|
||||
import * as path from 'path';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const pino = require('pino');
|
||||
const QRCode = require('qrcode-terminal');
|
||||
import { LeadsService } from '../leads/leads.service';
|
||||
import { ConversacionService } from '../conversacion/conversacion.service';
|
||||
import { ClaudeService } from '../claude/claude.service';
|
||||
import { MediaService } from '../media/media.service';
|
||||
import { Lead } from '../leads/lead.entity';
|
||||
import { wrapSocket } from 'baileys-antiban';
|
||||
|
||||
@Injectable()
|
||||
export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
||||
@@ -46,11 +48,16 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private async conectar() {
|
||||
const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
|
||||
const { version } = await fetchLatestBaileysVersion();
|
||||
|
||||
this.sock = makeWASocket({
|
||||
version,
|
||||
auth: state,
|
||||
printQRInTerminal: true,
|
||||
logger: pino({ level: 'silent' }) as any,
|
||||
printQRInTerminal: false,
|
||||
logger: pino({ level: 'info' }) as any,
|
||||
markOnlineOnConnect: false,
|
||||
generateHighQualityLinkPreview: false,
|
||||
syncFullHistory: false,
|
||||
});
|
||||
|
||||
this.sock.ev.on('creds.update', saveCreds);
|
||||
@@ -59,7 +66,8 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
||||
const { connection, lastDisconnect, qr } = update;
|
||||
|
||||
if (qr) {
|
||||
console.log('\n📲 Escanea el QR de arriba con WhatsApp\n');
|
||||
QRCode.generate(qr, { small: true });
|
||||
console.log('\n📲 Escanea este QR con WhatsApp\n');
|
||||
}
|
||||
|
||||
if (connection === 'close') {
|
||||
@@ -80,7 +88,7 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
} else if (connection === 'open') {
|
||||
this.logger.log(
|
||||
'✅ WhatsApp conectado. Luisa está lista para recibir mensajes.',
|
||||
'✅ WhatsApp conectado. Luisa esta lista para recibir mensajes.',
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -89,130 +97,102 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
||||
if (type !== 'notify') return;
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.key.fromMe) continue; // ignorar mensajes propios
|
||||
if (msg.key.fromMe) continue;
|
||||
if (!msg.key.remoteJid) continue;
|
||||
|
||||
if (msg.key.remoteJid.includes('@g.us')) continue;
|
||||
await this.procesarMensaje(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa un mensaje entrante de WhatsApp.
|
||||
* Identifica el tipo (texto, audio, imagen), normaliza el contenido,
|
||||
* consulta/crea el lead, llama a Claude y envía la respuesta.
|
||||
*/
|
||||
private async procesarMensaje(
|
||||
msg: proto.IWebMessageInfo,
|
||||
): Promise<void> {
|
||||
private async procesarMensaje(msg: any): Promise<void> {
|
||||
const jid = msg.key.remoteJid!;
|
||||
// Normalizar el número de teléfono (quitar el @s.whatsapp.net y el sufijo de grupo)
|
||||
const telefono = jid.replace('@s.whatsapp.net', '').replace('@g.us', '');
|
||||
|
||||
// Ignorar grupos
|
||||
if (jid.includes('@g.us')) return;
|
||||
|
||||
// Normalizar JID para envio (manejar formato LID de Baileys v7)
|
||||
const jidNormalizado = jid.includes('@lid')
|
||||
? `${jid.split('@')[0]}@s.whatsapp.net`
|
||||
: jid;
|
||||
|
||||
const telefono = jidNormalizado.replace('@s.whatsapp.net', '');
|
||||
|
||||
try {
|
||||
// 1. Identificar o crear el lead
|
||||
const lead = await this.leadsService.findOrCreate(telefono);
|
||||
|
||||
// Ignorar leads ya terminados
|
||||
if (['completado', 'no_viable', 'perdido'].includes(lead.estado_actual)) {
|
||||
this.logger.log(
|
||||
`Lead id=${lead.id} en estado=${lead.estado_actual}. Mensaje ignorado.`,
|
||||
);
|
||||
this.logger.log(`Lead id=${lead.id} en estado=${lead.estado_actual}. Ignorando.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Determinar el tipo de mensaje y normalizarlo a texto
|
||||
let textoNormalizado = '';
|
||||
const msgContent = msg.message;
|
||||
|
||||
if (!msgContent) return;
|
||||
|
||||
if (msgContent.conversation || msgContent.extendedTextMessage) {
|
||||
// Texto plano
|
||||
textoNormalizado =
|
||||
msgContent.conversation ||
|
||||
msgContent.extendedTextMessage?.text ||
|
||||
'';
|
||||
} else if (msgContent.audioMessage) {
|
||||
// Audio → Claude transcripción
|
||||
this.logger.log(`Audio recibido de lead id=${lead.id}. Transcribiendo...`);
|
||||
const buffer = await downloadMediaMessage(msg, 'buffer', {});
|
||||
const mimeType =
|
||||
msgContent.audioMessage.mimetype || 'audio/ogg; codecs=opus';
|
||||
const buffer = await downloadMediaMessage(msg as any, 'buffer', {});
|
||||
const mimeType = msgContent.audioMessage.mimetype || 'audio/ogg; codecs=opus';
|
||||
textoNormalizado = await this.mediaService.transcribirAudio(
|
||||
buffer as Buffer,
|
||||
mimeType,
|
||||
);
|
||||
} else if (msgContent.imageMessage) {
|
||||
// Imagen → Claude Vision
|
||||
this.logger.log(
|
||||
`Imagen recibida de lead id=${lead.id}. Analizando con Vision...`,
|
||||
);
|
||||
const buffer = await downloadMediaMessage(msg, 'buffer', {});
|
||||
this.logger.log(`Imagen recibida de lead id=${lead.id}. Analizando con Vision...`);
|
||||
const buffer = await downloadMediaMessage(msg as any, 'buffer', {});
|
||||
const mimeType = msgContent.imageMessage.mimetype || 'image/jpeg';
|
||||
textoNormalizado = await this.mediaService.inferirImagen(
|
||||
buffer as Buffer,
|
||||
mimeType,
|
||||
lead.estado_actual,
|
||||
);
|
||||
|
||||
// Si el lead envió un caption junto con la imagen, concatenarlo
|
||||
if (msgContent.imageMessage.caption) {
|
||||
textoNormalizado = `${msgContent.imageMessage.caption}\n\n[Contenido de la imagen: ${textoNormalizado}]`;
|
||||
}
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Tipo de mensaje no soportado de lead id=${lead.id}. Ignorando.`,
|
||||
);
|
||||
this.logger.log(`Tipo de mensaje no soportado de lead id=${lead.id}. Ignorando.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!textoNormalizado.trim()) return;
|
||||
|
||||
// 3. Guardar el mensaje del usuario en historial
|
||||
await this.conversacionService.guardarMensaje(
|
||||
lead.id,
|
||||
'user',
|
||||
textoNormalizado,
|
||||
);
|
||||
this.logger.log(`USUARIO [${telefono}]: ${textoNormalizado}`);
|
||||
|
||||
// 4. Construir historial y llamar a Claude
|
||||
const historial =
|
||||
await this.conversacionService.obtenerHistorialComoMessages(lead.id);
|
||||
await this.conversacionService.guardarMensaje(lead.id, 'user', textoNormalizado);
|
||||
|
||||
const historial = await this.conversacionService.obtenerHistorialComoMessages(lead.id);
|
||||
|
||||
const { respuesta, entidad, viable } = await this.claudeService.llamarClaude(
|
||||
lead,
|
||||
historial.slice(0, -1), // el último ya es el mensaje actual
|
||||
historial.slice(0, -1),
|
||||
textoNormalizado,
|
||||
);
|
||||
|
||||
// 5. Actualizar datos del lead con lo extraído por Claude
|
||||
this.logger.log(`LUISA [${telefono}]: ${respuesta}`);
|
||||
|
||||
if (entidad && Object.keys(entidad).length > 0) {
|
||||
await this.leadsService.updateDatos(lead.id, entidad);
|
||||
}
|
||||
|
||||
// 6. Manejar el flag viable
|
||||
if (viable !== undefined && viable !== null) {
|
||||
await this.leadsService.marcarViable(lead, viable);
|
||||
this.logger.log(
|
||||
`Lead id=${lead.id} marcado como viable=${viable}`,
|
||||
);
|
||||
this.logger.log(`Lead id=${lead.id} marcado como viable=${viable}`);
|
||||
} else {
|
||||
// Avanzar estado si sigue en_proceso
|
||||
if (lead.estado_actual === 'nuevo') {
|
||||
await this.leadsService.updateEstado(lead, 'en_proceso');
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Guardar respuesta de Claude en historial
|
||||
await this.conversacionService.guardarMensaje(
|
||||
lead.id,
|
||||
'assistant',
|
||||
respuesta,
|
||||
);
|
||||
|
||||
// 8. Enviar respuesta por WhatsApp
|
||||
await this.conversacionService.guardarMensaje(lead.id, 'assistant', respuesta);
|
||||
await this.enviarMensaje(jid, respuesta);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error procesando mensaje de ${telefono}: ${error.message}`,
|
||||
@@ -220,27 +200,20 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía un mensaje de texto por WhatsApp.
|
||||
*/
|
||||
async enviarMensaje(jid: string, texto: string): Promise<void> {
|
||||
if (!this.sock) {
|
||||
this.logger.error('Socket de WhatsApp no disponible');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.sock.sendMessage(jid, { text: texto });
|
||||
const safeSock = wrapSocket(this.sock);
|
||||
await safeSock.sendMessage(jid, { text: texto });
|
||||
this.logger.log(`Mensaje enviado a ${jid}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error enviando mensaje a ${jid}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía el mensaje de apertura de Luisa a un número de teléfono.
|
||||
* Lo usa el Scheduler para disparar el primer contacto.
|
||||
*/
|
||||
async enviarApertura(telefono: string, mensajeApertura: string): Promise<void> {
|
||||
const jid = `${telefono}@s.whatsapp.net`;
|
||||
await this.enviarMensaje(jid, mensajeApertura);
|
||||
@@ -249,4 +222,4 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
||||
isConectado(): boolean {
|
||||
return this.sock !== null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user