Merge branch 'main' of https://github.com/josedario87/conversation-layer
Some checks failed
Deploy conversation layer / deploy (push) Failing after 1m13s

This commit is contained in:
2025-06-06 14:48:01 -06:00
8 changed files with 10451 additions and 142 deletions

41
.gitignore vendored
View File

@@ -6,131 +6,102 @@ yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# 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
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.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 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 mcpTransport: StreamableHTTPClientTransport | undefined;
@@ -57,96 +57,57 @@ async function getMcpClient(): Promise<Client> {
}
return mcpClient;
}
/**
* 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.
const systemPromt = `
🗂 ESTRUCTURA CLAVE
├─ docker-compose.yml # Levanta todo el stack
├─ whatsapp-router/
│ ├─ src/chatHandlers.ts # Mapeo chatId → handler; ¡editá aquí para nuevos agentes!
│ └─ … # Lógica de ruteo y validaciones
├─ conversation-layer-agent/
│ ├─ src/index.ts # Entrada principal del agente
└─ prompts/system.ts # Prompt base; importa repoInfo
└─ chat-ui/ # Frontend Vite + React (TypeScript)
└─ …
## Como funcionas
- 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🖥🧠🌐",
- 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
⚙️ VARIABLES DE ENTORNO (ejemplo .env)
OPEN_WA_URL=http://openwa:8080
LLM_AGENT_URL=http://conversation-layer-agent:8000
PORT=3001 # Puerto del router
NODE_ENV=development # Cambiá a production para desactivar logs verbosos
## 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.
🚀 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\`.
🔄 FLUJO DE MENSAJES
OpenWA → whatsapp-router (/webhook) → handler ↔ conversation-layer-agent ↔ chat-ui
#### 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.
🤖 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)
🔐 SEGURIDAD
- Los tokens/API keys van en variables de entorno; nunca los subas al repo.
- 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.
###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();
app.use(express.json());
app.post('/', async (req, res) => {
const conversation = req.body?.conversation as Conversation | undefined;
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) {
return res.json({ reply: repoInfo });
return res.json({ reply: systemPromt });
}
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 = {};
// if (message.toLowerCase().includes('/planilla')) {
if (true) {
@@ -175,7 +136,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>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

@@ -9,12 +9,17 @@
"start": "node dist/index.js"
},
"dependencies": {
"@google/genai": "^1.4.0",
"@open-wa/wa-automate": "^4.76.0",
"axios": "^1.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": {
"@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.27",
"@types/node": "^20.11.19",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",

View File

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

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

View File

@@ -1,8 +1,10 @@
import express, { Application } from 'express';
import axios from 'axios';
import { GoogleGenAI } from '@google/genai';
import { getHandler } from './chatHandlers';
import { addMessageToConversation } from './store/conversation';
import { WhatsAppMessage, Conversation } from './types';
import { transcribeAudioMessage } from './transcribeAudioMessage';
export interface WebhookConfig {
API_URL: string;
@@ -31,6 +33,12 @@ export function registerWebhookRoutes(
if (message) {
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}`);
}
@@ -38,6 +46,39 @@ export function registerWebhookRoutes(
if (!message) return res.sendStatus(200);
if (!openWaUrl) throw new Error('Service URLs not configured');
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;
if (chatId) {
try {