Logia de agente de Whatsapp
This commit is contained in:
8
mvp/Whatsapp-bot/src/media/media.module.ts
Normal file
8
mvp/Whatsapp-bot/src/media/media.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MediaService } from './media.service';
|
||||
|
||||
@Module({
|
||||
providers: [MediaService],
|
||||
exports: [MediaService],
|
||||
})
|
||||
export class MediaModule {}
|
||||
171
mvp/Whatsapp-bot/src/media/media.service.ts
Normal file
171
mvp/Whatsapp-bot/src/media/media.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user