Logia de agente de Whatsapp

This commit is contained in:
unknown
2026-05-31 22:02:58 -04:00
parent aa7555b49d
commit ef78d9a14c
28 changed files with 12736 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { MediaService } from './media.service';
@Module({
providers: [MediaService],
exports: [MediaService],
})
export class MediaModule {}

View File

@@ -0,0 +1,171 @@
import { Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
import { EstadoLead } from '../leads/lead.entity';
@Injectable()
export class MediaService {
private readonly logger = new Logger(MediaService.name);
private readonly OPENROUTER_URL =
'https://openrouter.ai/api/v1/chat/completions';
private get headers() {
return {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://reformix.es',
'X-Title': 'Reformix Luisa Bot',
};
}
/**
* Transcribe un audio enviándolo a Claude 4.5 como base64.
* Baileys entrega el buffer del audio; lo convertimos a base64.
*
* @param audioBuffer Buffer del audio recibido por Baileys
* @param mimeType MIME type del audio (ej: audio/ogg; codecs=opus)
* @returns Texto transcrito, o el fallback si falla
*/
async transcribirAudio(
audioBuffer: Buffer,
mimeType = 'audio/ogg',
): Promise<string> {
const FALLBACK =
'No pude escuchar bien el audio. ¿Puedes escribirme lo que me querías contar?';
try {
const base64Audio = audioBuffer.toString('base64');
const response = await axios.post(
this.OPENROUTER_URL,
{
model: process.env.MODEL || 'anthropic/claude-sonnet-4-5',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: 'Por favor, transcribe exactamente lo que se dice en este audio. Devuelve solo la transcripción, sin añadir nada más.',
},
{
type: 'image_url', // OpenRouter usa image_url para base64 de audio también
image_url: {
url: `data:${mimeType};base64,${base64Audio}`,
},
},
],
},
],
max_tokens: 512,
},
{ headers: this.headers },
);
const transcripcion: string =
response.data.choices?.[0]?.message?.content?.trim();
if (!transcripcion) {
this.logger.warn('Claude devolvió respuesta vacía para el audio');
return FALLBACK;
}
this.logger.log(
`Audio transcrito correctamente (${transcripcion.length} chars)`,
);
return transcripcion;
} catch (error) {
this.logger.error(
`Error transcribiendo audio: ${error.message}`,
error.response?.data,
);
return FALLBACK;
}
}
/**
* Infiere información de una imagen según el estado actual del lead.
* Útil para capturar espacios, materiales, estilos, etc.
*
* @param imagenBuffer Buffer de la imagen recibida por Baileys
* @param mimeType MIME type (ej: image/jpeg)
* @param estadoActual Estado del lead para adaptar el prompt de visión
* @returns Texto inferido, o el fallback si falla
*/
async inferirImagen(
imagenBuffer: Buffer,
mimeType = 'image/jpeg',
estadoActual: EstadoLead = 'en_proceso',
): Promise<string> {
const FALLBACK =
'Recibí tu imagen pero no pude analizarla bien. ¿Puedes describirme lo que muestra?';
const promptPorEstado: Record<string, string> = {
nuevo:
'Describe brevemente qué tipo de espacio se ve en esta imagen y sus características principales.',
en_proceso:
'Describe el espacio que aparece en la imagen: tipo de habitación, materiales, estado actual, tamaño aproximado.',
recopilando_datos:
'Analiza esta imagen de un espacio para reformar. Indica: tipo de espacio, metros cuadrados aproximados, materiales visibles, estilo actual, estado de conservación.',
completado:
'Describe lo que ves en esta imagen relacionado con reformas o diseño de interiores.',
no_viable:
'Describe brevemente qué muestra esta imagen.',
perdido:
'Describe brevemente qué muestra esta imagen.',
};
const promptDeVisión =
promptPorEstado[estadoActual] ||
'Describe qué ves en esta imagen en el contexto de una reforma de hogar.';
try {
const base64Imagen = imagenBuffer.toString('base64');
const response = await axios.post(
this.OPENROUTER_URL,
{
model: process.env.MODEL || 'anthropic/claude-sonnet-4-5',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: promptDeVisión,
},
{
type: 'image_url',
image_url: {
url: `data:${mimeType};base64,${base64Imagen}`,
},
},
],
},
],
max_tokens: 512,
},
{ headers: this.headers },
);
const inferencia: string =
response.data.choices?.[0]?.message?.content?.trim();
if (!inferencia) {
this.logger.warn('Claude devolvió respuesta vacía para la imagen');
return FALLBACK;
}
this.logger.log(
`Imagen inferida correctamente (${inferencia.length} chars)`,
);
return inferencia;
} catch (error) {
this.logger.error(
`Error analizando imagen: ${error.message}`,
error.response?.data,
);
return FALLBACK;
}
}
}