This reverts commit b508bea3dd, reversing
changes made to 4c4052bb64.
This commit is contained in:
2025-06-06 14:49:34 -06:00
parent b508bea3dd
commit 54ef965009
8 changed files with 142 additions and 10451 deletions

41
.gitignore vendored
View File

@@ -6,102 +6,131 @@ 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://localhost:5000/mcp'; const MCP_URL = process.env.MCP_URL || 'http://planilla.interno.com/mcp';
let mcpClient: Client | undefined; let mcpClient: Client | undefined;
let mcpTransport: StreamableHTTPClientTransport | undefined; let mcpTransport: StreamableHTTPClientTransport | undefined;
@@ -57,57 +57,96 @@ async function getMcpClient(): Promise<Client> {
} }
return mcpClient; return mcpClient;
} }
const systemPromt = ` /**
* Descripción de alto nivel para que cualquier agente (humano o LLM) entienda y
* trabaje con el repositorio sin perder tiempo buscando contexto.
*/
const repoInfo = `
📦 RESUMEN
Este repo orquesta tres servicios complementarios:
1. whatsapp-router → Recibe webhooks de OpenWA y re-expide los mensajes al agente o la UI.
2. conversation-layer-agent → LLM que responde dudas sobre el código y ejecuta acciones.
3. chat-ui → Interfaz web mínima que conversa con el agente.
## Como funcionas 🗂 ESTRUCTURA CLAVE
- 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' })}
- tu nombre es nucleo "id": "50493849962@c.us", "name": "Nucleo🖥🧠🌐", ├─ docker-compose.yml # Levanta todo el stack
- tu funcionamiento inicia con este system prompt que contiene la informacion sobre el funcionamiento tuyo y del sistema con el que trabajas. ├─ whatsapp-router/
- 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. │ ├─ src/chatHandlers.ts # Mapeo chatId → handler; ¡editá aquí para nuevos agentes!
- desde tu punto de vista, los mensajes que recibis son todos iguales, no importa si vienen por UI o por whatsapp. │ └─ … # Lógica de ruteo y validaciones
- 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. ├─ conversation-layer-agent/
- cuando sos activado, tenes que entender los mensajes del objeto 'conversation' desde el ultimo mensaje pues es el mas nuevo. │ ├─ src/index.ts # Entrada principal del agente
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. └─ prompts/system.ts # Prompt base; importa repoInfo
- 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 └─ chat-ui/ # Frontend Vite + React (TypeScript)
- 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 ⚙️ VARIABLES DE ENTORNO (ejemplo .env)
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. OPEN_WA_URL=http://openwa:8080
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. LLM_AGENT_URL=http://conversation-layer-agent:8000
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. PORT=3001 # Puerto del router
porque algunas veces vamos a necesitar hacer stress test, o preguntare cosas que no parecen tener sentido, pero es parte del proceso de debugueo. NODE_ENV=development # Cambiá a production para desactivar logs verbosos
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.
🚀 CÓMO LEVANTAR
1) cp .env.example .env && edítalo según tu entorno
2) docker-compose up -d --build
3) Visita http://localhost:3000 (UI) o revisá logs con \`docker-compose logs -f\`.
#### sentido del oido 🔄 FLUJO DE MENSAJES
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. OpenWA → whatsapp-router (/webhook) → handler ↔ conversation-layer-agent ↔ chat-ui
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.
🤖 GUÍA RÁPIDA PARA AGENTES LLM
- Responde corto, en el tono del usuario (“vos”, español hondureño).
- Usa SOLO la info del objeto \`Conversation\`; no mantengas estado entre turnos.
- Si falta contexto, pedilo en una línea.
- No repitas instrucciones ni digas que sos IA.
- Devuelve \`{ reply: string, actions?: any[] }\` (JSON puro) para facilitar parsing.
🔧 COMANDOS ÚTILES
npm run dev # Hot-reload del router
npm test # Ejecuta los tests
docker exec -it openwa sh # Shell dentro del contenedor OpenWA
git remote -v # Confirma remotos (origin: Gitea, github: mirror)
###aclaratorias 🔐 SEGURIDAD
- 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 tokens/API keys van en variables de entorno; nunca los subas al repo.
- 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. - Usa certificados válidos o \`NODE_TLS_REJECT_UNAUTHORIZED=0\` SOLO en dev.
✍️ CONTRIBUCIONES
Push a rama feature/* → CI/CD en Gitea valida lint, tests y build.
Crea PR para revisión; no mezcles cambios de lógica y formato en el mismo commit.
📜 LICENCIA
GPL-3.0 — libre de usar, modificar y redistribuir mientras mantengas la misma licencia.
¡Listo! Con esto cualquier agente debería orientarse y empezar a trabajar sin drama.
`; `;
const app = express(); const app = express();
app.use(express.json()); 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: systemPromt }); return res.json({ reply: repoInfo });
} }
try { try {
const contents = `systemPrompt: ${systemPromt}\nConversation:\n${JSON.stringify(conversation)}\n`; const contents = `Repo information: ${repoInfo}\nConversation:\n${context}\n`;
console.log(' contents', contents);
const config: any = {}; const config: any = {};
// if (message.toLowerCase().includes('/planilla')) { // if (message.toLowerCase().includes('/planilla')) {
if (true) { if (true) {
@@ -136,7 +175,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: </p> <p>Repository info: ${repoInfo}</p>
`); `);
} }
); );

File diff suppressed because it is too large Load Diff

View File

@@ -9,17 +9,12 @@
"start": "node dist/index.js" "start": "node dist/index.js"
}, },
"dependencies": { "dependencies": {
"@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

@@ -79,11 +79,11 @@ export async function buildConversation(chatId: string, openWaUrl: string): Prom
const conversation: Conversation = { const conversation: Conversation = {
chatId, chatId,
title, title: chat?.formattedTitle ?? chat?.name ?? chatId,
isGroup, isGroup: Boolean(chat?.isGroup),
unreadCount, unreadCount: chat?.unreadCount ?? 0,
participants: Array.from(participantsMap.values()), participants: buildParticipants(raw, chat),
messages, messages: raw.slice(-20).map(toMsg).sort((a, b) => a.ts - b.ts),
createdAt: conversations.get(chatId)?.createdAt || now, createdAt: conversations.get(chatId)?.createdAt || now,
}; };
@@ -124,6 +124,5 @@ export async function addMessageToConversation(
isMe: Boolean(s.isMe), isMe: Boolean(s.isMe),
}); });
} }
return conv; return conv;
} }

View File

@@ -1,55 +0,0 @@
// 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

@@ -191,9 +191,6 @@ export interface WhatsAppMessage {
chatId: string; chatId: string;
mediaData: Record<string, unknown>; mediaData: Record<string, unknown>;
text: string; text: string;
clientUrl?: string;
deprecatedMms3Url?: string;
mimetype?: string;
} }
export interface Participant { export interface Participant {

View File

@@ -1,10 +1,8 @@
import express, { Application } from 'express'; import express, { Application } from 'express';
import axios from 'axios'; import axios from 'axios';
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;
@@ -33,12 +31,6 @@ 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}`);
} }
@@ -46,39 +38,6 @@ export function registerWebhookRoutes(
if (!message) return res.sendStatus(200); if (!message) return res.sendStatus(200);
if (!openWaUrl) throw new Error('Service URLs not configured'); if (!openWaUrl) throw new Error('Service URLs not configured');
const chatId = message.chatId || from; const chatId = message.chatId || from;
// Audio message handling
// console.log(message);
if (
message.type === 'ptt' &&
message.mimetype === 'audio/ogg; codecs=opus'
) {
const audioUrl = message.clientUrl || message.deprecatedMms3Url;
if (!audioUrl) {
console.error('No audio URL found for PTT message');
// Potentially send a message to user or just skip? For now, skip.
return res.sendStatus(200);
}
console.log('🎤 Mensaje de audio detectado', audioUrl);
try {
const transcript = await transcribeAudioMessage(message);
console.log('📝 Transcripción:', transcript);
message.body = transcript || '';
message.text = transcript || '';
} catch (transcriptionError: any) {
console.error('Error en la transcripción:', transcriptionError.message);
const reply =
"I received an audio message, but I couldn't transcribe it. Please send the transcript manually.";
await axios.post(`${openWaUrl}/sendText`, { args: { to: from, content: reply } });
// Stop processing this message as transcription failed and user has been notified.
return res.sendStatus(200);
}
}
console.log(message);
let conv: Conversation | undefined; let conv: Conversation | undefined;
if (chatId) { if (chatId) {
try { try {