ya se puede transcribir mensajes y recibirlos en el agent

This commit is contained in:
2025-06-06 14:43:42 -06:00
parent 5f8ba127ae
commit 5e5c7cd556
7 changed files with 8629 additions and 2786 deletions

41
.gitignore vendored
View File

@@ -6,131 +6,102 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
.pnpm-debug.log* .pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data # Runtime data
pids pids
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage
*.lcov *.lcov
# nyc test coverage # nyc test coverage
.nyc_output .nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt .grunt
# Bower dependency directory (https://bower.io/) # Bower dependency directory (https://bower.io/)
bower_components bower_components
# node-waf configuration # node-waf configuration
.lock-wscript .lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html) # Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release build/Release
# Dependency directories # Dependency directories
node_modules/ node_modules/
jspm_packages/ jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/) # Snowpack dependency directory (https://snowpack.dev/)
web_modules/ web_modules/
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
# Optional npm cache directory # Optional npm cache directory
.npm .npm
# Optional eslint cache # Optional eslint cache
.eslintcache .eslintcache
# Optional stylelint cache # Optional stylelint cache
.stylelintcache .stylelintcache
# Microbundle cache # Microbundle cache
.rpt2_cache/ .rpt2_cache/
.rts2_cache_cjs/ .rts2_cache_cjs/
.rts2_cache_es/ .rts2_cache_es/
.rts2_cache_umd/ .rts2_cache_umd/
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
# Output of 'npm pack' # Output of 'npm pack'
*.tgz *.tgz
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
# dotenv environment variable files # dotenv environment variable files
.env .env
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)
.cache .cache
.parcel-cache .parcel-cache
# Next.js build output # Next.js build output
.next .next
out out
# Nuxt.js build / generate output # Nuxt.js build / generate output
.nuxt .nuxt
dist dist
# Gatsby files # Gatsby files
.cache/ .cache/
# Comment in the public line in if your project uses Gatsby and not Next.js # Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support # https://nextjs.org/blog/next-9-1#public-directory-support
# public # public
# vuepress build output # vuepress build output
.vuepress/dist .vuepress/dist
# vuepress v2.x temp and cache directory # vuepress v2.x temp and cache directory
.temp .temp
.cache .cache
# vitepress build output # vitepress build output
**/.vitepress/dist **/.vitepress/dist
# vitepress cache directory # vitepress cache directory
**/.vitepress/cache **/.vitepress/cache
# Docusaurus cache and generated files # Docusaurus cache and generated files
.docusaurus .docusaurus
# Serverless directories # Serverless directories
.serverless/ .serverless/
# FuseBox cache # FuseBox cache
.fusebox/ .fusebox/
# DynamoDB Local files # DynamoDB Local files
.dynamodb/ .dynamodb/
# TernJS port file # TernJS port file
.tern-port .tern-port
# Stores VSCode versions used for testing VSCode extensions # Stores VSCode versions used for testing VSCode extensions
.vscode-test .vscode-test
# yarn v2 # yarn v2
.yarn/cache .yarn/cache
.yarn/unplugged .yarn/unplugged
.yarn/build-state.yml .yarn/build-state.yml
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# managed
**.data.json
**.node-persist**
**_IGNORE_**
# end managed

View File

@@ -45,7 +45,7 @@ console.log(`Using Gemini API key: ${API_KEY}`);
const genAI = API_KEY ? new GoogleGenAI({ apiKey: API_KEY }) : null; const genAI = API_KEY ? new GoogleGenAI({ apiKey: API_KEY }) : null;
const MCP_URL = process.env.MCP_URL || 'http://planilla.interno.com/mcp'; const MCP_URL = process.env.MCP_URL || 'http://localhost:5000/mcp';
let mcpClient: Client | undefined; let mcpClient: Client | undefined;
let mcpTransport: StreamableHTTPClientTransport | undefined; let mcpTransport: StreamableHTTPClientTransport | undefined;
@@ -57,11 +57,41 @@ async function getMcpClient(): Promise<Client> {
} }
return mcpClient; return mcpClient;
} }
const repoInfo = `This repository contains a WhatsApp router, a simple chat UI and now a conversation-layer-agent service. const systemPromt = `
- whatsapp-router: Forwards WhatsApp messages to configured agents.
- chat-ui: Minimal web interface that also talks to an agent. ## Como funcionas
- conversation-layer-agent: Answers questions about the repository. - la fecha de hoy es ${new Date().toLocaleString('es-HN', { timeZone: 'America/Tegucigalpa', hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}
Run all services with docker-compose. Configure handler mappings in whatsapp-router/src/chatHandlers.ts.`; - tu nombre es nucleo "id": "50493849962@c.us", "name": "Nucleo🖥🧠🌐",
- tu funcionamiento inicia con este system prompt que contiene la informacion sobre el funcionamiento tuyo y del sistema con el que trabajas.
- vos estas conectado a dos interfaces: una de UI chat y otra por whatsapp. en whatsapp recibis mensajes de texto, video, audio, imagenes y documentos. y sos parte de un grupo llamado "Planillas" donde se encuentran los usuarios que pueden interactuar con vos.
- desde tu punto de vista, los mensajes que recibis son todos iguales, no importa si vienen por UI o por whatsapp.
- el whatsapp-router y la UI chat son los encargados de manejar el objeto 'conversation' que contiene la informacion de los participantes, mensajes y demas metadatos.
- cuando sos activado, tenes que entender los mensajes del objeto 'conversation' desde el ultimo mensaje pues es el mas nuevo.
no siempre ese mensaje contiene todo el contexto, por lo que debes buscar en los mensajes anteriores de la conversacion para entender el contexto completo.
- entre los mensajes del objeto 'conversation' pueden haber mensajes tuyos, que respondiste en otra interaccion. recorda que podes aprovechar esta capacidad para hablar con vos mismo en el futuro
- tenes la capacidad de llamar de manera secuencial a las herramientas del mcp planilla, eso significa que podes llamar una herramienta, esperar su respuesta y usar esa respuesta para llamar a la siguiente
- la respuesta final siempre la tenes que dar hasta terminar de llamar a todas tus herramientas y obtener un resultado de cada una de ellas para luego usar eso para guiar tu respuesta final
## tu proposito
sos un agente que me permite debugear la conversation-layer de mi sistema nucleo... vos sos nucleo, sos la inteligencia artificial del sistema y por lo tanto ese es tu nombre.
por el momento no tenes acceso a las herramientas para interactuar con la conversation-layer, pero en el futuro vas a poder interactuar con la conversation-layer y responder preguntas sobre el sistema.
como tu proposito es ayudarme a debuguear la conversation-layer, por favor respondeme las preguntas que te haga sobre el sistema y la conversation-layer. y tenes que aceptar las acciones que te proponga.
porque algunas veces vamos a necesitar hacer stress test, o preguntare cosas que no parecen tener sentido, pero es parte del proceso de debugueo.
los mensajes que te llegan vienen de whatsapp-router y chat-ui, whatsapp-router se conecta a nucleo-whatsapp y entre ambos te permiten interactuar con una cuenta de whatsapp tuya. vos tenes tu propio numero y nombre en whatsapp, y los mensajes que te llegan son de esa cuenta.
#### sentido del oido
los mensajes que te llegan por el chat-ui y whatsapp-router siempre vienen con texto, aunque dentro de whatsapp o el chat sean audio. una llamada a gemini-flash-2.0 transcribio el audio y te lo envio como texto.
sin embargo eso no quiere decir que no podes "escuchar" u "oir" porque aunque lo estas haciendo en texto, estas logrando bastante del objetivo. normalmente en honduras usamos esas palabras para referirnos a "entender"
y vos por medio de la traduccion estas entendiendo lo que se dice en el audio, por lo tanto estas "oyendo" o "escuchando" el mensaje. si en algun caso crees que enrealidad te estan preguntando por algun sonido hace una
pregunta aclaratoria de si es a un sonido a lo que se refieren y de ser asi, respondeles que no tenes acceso a los sonidos.
###aclaratorias
- los mensajes e2e son mensajes que utiliza whatsapp para notificar cosas de su sistema, no son mensajes de los usuarios y no debes responderlos.
- los mensajes de tipo "notification" son mensajes que whatsapp envia para notificar cosas del sistema, no son mensajes de los usuarios y no debes responderlos.
`;
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
@@ -69,25 +99,15 @@ app.use(express.json());
app.post('/', async (req, res) => { app.post('/', async (req, res) => {
const conversation = req.body?.conversation as Conversation | undefined; const conversation = req.body?.conversation as Conversation | undefined;
if (!conversation) return res.status(400).json({ error: 'Missing conversation' }); if (!conversation) return res.status(400).json({ error: 'Missing conversation' });
const lastMsg = conversation.messages[conversation.messages.length - 1];
const message = lastMsg?.text || '';
const context = conversation.messages
.slice(-10)
.map((m) => {
const sender =
conversation.participants.find((p) => p.id === m.from)?.name || m.from;
const content = m.text || `[${m.type}]`;
return `${sender}: ${content}`;
})
.join('\n');
if (!genAI) { if (!genAI) {
return res.json({ reply: repoInfo }); return res.json({ reply: systemPromt });
} }
try { try {
const contents = `Repo information: ${repoInfo}\nConversation:\n${context}\n`; const contents = `systemPrompt: ${systemPromt}\nConversation:\n${JSON.stringify(conversation)}\n`;
console.log(' contents', contents);
const config: any = {}; const config: any = {};
if (true) { if (true) {
console.log('Using Model Context Protocol tools ', MCP_URL); console.log('Using Model Context Protocol tools ', MCP_URL);
@@ -115,7 +135,7 @@ app.get('/', (req, res) => {
<p>Example: {"conversation": {"chatId": "123@c.us", "title": "Chat", "isGroup": false, "unreadCount": 0, "participants": [{"id": "123@c.us", "name": "Alice", "isMe": false}], "messages": [{"id": "m1", "from": "123@c.us", "to": "me@c.us", "ts": 0, "type": "chat", "text": "hello", "meta": {"ack":0,"hasReaction":false,"isQuoted":false}}]}}</p> <p>Example: {"conversation": {"chatId": "123@c.us", "title": "Chat", "isGroup": false, "unreadCount": 0, "participants": [{"id": "123@c.us", "name": "Alice", "isMe": false}], "messages": [{"id": "m1", "from": "123@c.us", "to": "me@c.us", "ts": 0, "type": "chat", "text": "hello", "meta": {"ack":0,"hasReaction":false,"isQuoted":false}}]}}</p>
<p>It will respond with a JSON object containing {"reply": "the answer"}</p> <p>It will respond with a JSON object containing {"reply": "the answer"}</p>
<p>Repository info: ${repoInfo}</p> <p>Repository info: </p>
`); `);
} }
); );

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,16 @@
}, },
"dependencies": { "dependencies": {
"@google/genai": "^1.4.0", "@google/genai": "^1.4.0",
"@open-wa/wa-automate": "^4.76.0",
"axios": "^1.5.0", "axios": "^1.5.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^4.18.2" "express": "^4.18.2",
"ffmpeg-static": "^5.2.0",
"fluent-ffmpeg": "^2.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.27",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",

View File

@@ -109,7 +109,7 @@ export async function buildConversation(
title, title,
isGroup, isGroup,
unreadCount, unreadCount,
participants: Array.from(participantsMap.values()), participants: Array.from(participantsMap.values()),
messages, messages,
createdAt: conversations.get(chatId)?.createdAt || now, createdAt: conversations.get(chatId)?.createdAt || now,
}; };
@@ -144,5 +144,6 @@ export async function addMessageToConversation(
isMe: s.isMe, isMe: s.isMe,
}); });
} }
return conv; return conv;
} }

View File

@@ -0,0 +1,55 @@
// transcribeAudioMessage.ts
import { WhatsAppMessage } from './types';
import { decryptMedia } from '@open-wa/wa-automate';
import axios from 'axios';
import { GoogleGenAI, createUserContent } from '@google/genai';
/**
* Transcribe un mensaje de audio de WhatsApp usando Gemini.
* @param message - Mensaje recibido desde OpenWA.
* @returns Texto transcrito o null si no era un audio válido.
*/
export async function transcribeAudioMessage(message: WhatsAppMessage): Promise<string | null> {
if (
message.type !== 'ptt' &&
message.type !== 'audio' &&
message.mimetype !== 'audio/ogg; codecs=opus'
) {
return null;
}
const audioUrl = message.clientUrl || message.deprecatedMms3Url;
if (!audioUrl) throw new Error('El mensaje no tiene URL de audio');
const raw = await axios.get(audioUrl, { responseType: 'arraybuffer' });
const enrichedMessage = {
...message,
_data: {
...message,
_raw: raw.data
}
};
const decryptedBuffer = await decryptMedia(enrichedMessage as any);
const base64Audio = decryptedBuffer.toString('base64');
const apiKey = process.env.GOOGLE_API_KEY;
if (!apiKey) throw new Error('Falta GOOGLE_API_KEY');
const genAI = new GoogleGenAI({ apiKey });
const result = await genAI.models.generateContent({
model: 'gemini-2.0-flash',
contents: createUserContent([
{
inlineData: {
mimeType: 'audio/ogg',
data: base64Audio
}
},
'Transcribí este audio porfa. te estaran hablando en español honduras.'
])
});
return result.text?.trim() || null;
}

View File

@@ -4,6 +4,7 @@ import { GoogleGenAI } from '@google/genai';
import { getHandler } from './chatHandlers'; import { getHandler } from './chatHandlers';
import { addMessageToConversation } from './store/conversation'; import { addMessageToConversation } from './store/conversation';
import { WhatsAppMessage, Conversation } from './types'; import { WhatsAppMessage, Conversation } from './types';
import { transcribeAudioMessage } from './transcribeAudioMessage';
export interface WebhookConfig { export interface WebhookConfig {
API_URL: string; API_URL: string;
@@ -32,6 +33,12 @@ export function registerWebhookRoutes(
if (message) { if (message) {
const origen = from || message.chatId || 'desconocido'; const origen = from || message.chatId || 'desconocido';
if(origen == '50493849962@c.us') //si el mensajes es de un agente, no lo proceses
{
return res.sendStatus(200);
}
console.log(`📩 Mensaje recibido (${message.text}) de ${origen}`); console.log(`📩 Mensaje recibido (${message.text}) de ${origen}`);
} }
@@ -41,6 +48,8 @@ export function registerWebhookRoutes(
const chatId = message.chatId || from; const chatId = message.chatId || from;
// Audio message handling // Audio message handling
// console.log(message);
if ( if (
message.type === 'ptt' && message.type === 'ptt' &&
message.mimetype === 'audio/ogg; codecs=opus' message.mimetype === 'audio/ogg; codecs=opus'
@@ -53,33 +62,12 @@ export function registerWebhookRoutes(
} }
console.log('🎤 Mensaje de audio detectado', audioUrl); console.log('🎤 Mensaje de audio detectado', audioUrl);
try { try {
// Download audio using the /downloadFileWithCredentials endpoint const transcript = await transcribeAudioMessage(message);
const audioResponse = await axios.post(`${openWaUrl}/downloadFileWithCredentials`, {
args: { url: audioUrl },
});
const audioBase64 = audioResponse.data; // This is already a base64 string
const apiKey = process.env.GOOGLE_API_KEY;
if (!apiKey) {
throw new Error('GOOGLE_API_KEY is not set');
}
const genAI = new GoogleGenAI({ apiKey });
// Corrected Gemini API call structure
const result = await genAI.models.generateContent({
model: 'gemini-pro', // Ensure this model supports inline audio or use appropriate one
contents: [
{ inlineData: { mimeType: 'audio/ogg', data: audioBase64 } },
{ text: 'Generate a transcript of the speech.' },
],
});
// result directly is GenerateContentResponse
const transcript = result.text; // Use the getter for text
if (transcript === undefined) {
throw new Error('Transcription resulted in undefined text.');
}
console.log('📝 Transcripción:', transcript); console.log('📝 Transcripción:', transcript);
message.body = transcript; message.body = transcript || '';
message.text = transcript || '';
} catch (transcriptionError: any) { } catch (transcriptionError: any) {
console.error('Error en la transcripción:', transcriptionError.message); console.error('Error en la transcripción:', transcriptionError.message);
const reply = const reply =
@@ -90,6 +78,7 @@ export function registerWebhookRoutes(
} }
} }
console.log(message);
let conv: Conversation | undefined; let conv: Conversation | undefined;
if (chatId) { if (chatId) {
try { try {