import express from 'express'; import { GoogleGenAI, mcpToTool } from '@google/genai'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import dotenv from 'dotenv'; dotenv.config(); interface Participant { id: string; name: string; isMe: boolean; isAdmin?: boolean; } interface Msg { id: string; from: string; to: string; ts: number; type: 'chat' | 'image' | 'audio' | 'sticker' | 'doc'; text?: string; mediaUrl?: string; mentions?: string[]; meta: { ack: number; hasReaction: boolean; isQuoted: boolean; }; } interface Conversation { chatId: string; title: string; isGroup: boolean; unreadCount: number; participants: Participant[]; messages: Msg[]; createdAt: number; } const PORT = Number(process.env.PORT) || 8001; const API_KEY = process.env.GEMINI_API_KEY || ''; 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'; let mcpClient: Client | undefined; let mcpTransport: StreamableHTTPClientTransport | undefined; async function getMcpClient(): Promise { if (!mcpClient) { mcpClient = new Client({ name: 'planilla-client', version: '1.0.0' }); mcpTransport = new StreamableHTTPClientTransport(new URL(MCP_URL)); await mcpClient.connect(mcpTransport); } 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. 🗂 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) └─ … ⚙️ 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 🚀 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 🤖 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. `; 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 }); } try { const contents = `Repo information: ${repoInfo}\nConversation:\n${context}\n`; const config: any = {}; // if (message.toLowerCase().includes('/planilla')) { if (true) { console.log('Using Model Context Protocol tools ', MCP_URL); const client = await getMcpClient(); config.tools = [mcpToTool(client)]; } const result = await genAI.models.generateContent({ model: 'gemini-2.0-flash', contents, config, }); const reply = (result.text || '').trim(); res.json({ reply }); } catch (err: any) { console.error('Gemini error', err.message); res.status(500).json({ error: 'Failed to generate reply' }); } }); app.get('/', (req, res) => { res.send(`

Conversation Layer Agent

This service answers questions about the repository.

Send a POST request to / with a JSON body containing {"conversation": {...}}

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}}]}}

It will respond with a JSON object containing {"reply": "the answer"}

Repository info: ${repoInfo}

`); } ); app.listen(PORT, () => { console.log(`conversation-layer-agent listening on ${PORT}`); });