From fff921df0a051a4584c360ffb28beaa93e576cdf Mon Sep 17 00:00:00 2001 From: josedario87 Date: Thu, 5 Jun 2025 15:24:18 -0600 Subject: [PATCH] se incluye al propio agente usuario de la cuenta de whatsapp dentro del convo, se actualiza system prompt para el agent --- conversation-layer-agent/src/index.ts | 69 ++++++- whatsapp-router/src/store/conversation.ts | 212 ++++++++++------------ 2 files changed, 163 insertions(+), 118 deletions(-) diff --git a/conversation-layer-agent/src/index.ts b/conversation-layer-agent/src/index.ts index f49f192..273b1ac 100644 --- a/conversation-layer-agent/src/index.ts +++ b/conversation-layer-agent/src/index.ts @@ -57,11 +57,70 @@ async function getMcpClient(): Promise { } return mcpClient; } -const repoInfo = `This repository contains a WhatsApp router, a simple chat UI and now a conversation-layer-agent service. -- whatsapp-router: Forwards WhatsApp messages to configured agents. -- chat-ui: Minimal web interface that also talks to an agent. -- conversation-layer-agent: Answers questions about the repository. -Run all services with docker-compose. Configure handler mappings in whatsapp-router/src/chatHandlers.ts.`; +/** + * 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()); diff --git a/whatsapp-router/src/store/conversation.ts b/whatsapp-router/src/store/conversation.ts index 6c23e83..207b5d0 100644 --- a/whatsapp-router/src/store/conversation.ts +++ b/whatsapp-router/src/store/conversation.ts @@ -1,145 +1,131 @@ import axios from 'axios'; import { WhatsAppMessage, Conversation, Msg, Participant } from '../types'; +/** + * In‑memory cache of conversations indexed by chatId. + */ const conversations = new Map(); -async function loadMessages( - chatId: string, - openWaUrl: string -): Promise { - console.log(`[conversationStore] Loading messages for ${chatId}`); + + +// ─────────────────────────────────────────────────────── internal helpers ──── + +/** + * Fetches *all* messages for a chat using OpenWA and returns them as‑is. + */ +async function fetchChatMessages(chatId: string, openWaUrl: string): Promise { const { data } = await axios.post(`${openWaUrl}/loadAndGetAllMessagesInChat`, { - args: { - chatId, - includeMe: true, - includeNotifications: true, - }, + args: { chatId, includeMe: true, includeNotifications: true }, }); - const msgs: WhatsAppMessage[] = data?.response || data || []; - return msgs; + + return data?.response ?? data ?? []; } -function mapMessage(m: WhatsAppMessage): Msg { +/** + * Builds a complete {@link Conversation} object from raw WA messages. + */ +export async function buildConversation(chatId: string, openWaUrl: string): Promise { + const raw = await fetchChatMessages(chatId, openWaUrl); + const chatMeta = raw[0]?.chat; + + const convo: Conversation = { + chatId, + title: chatMeta?.formattedTitle ?? chatMeta?.name ?? chatId, + isGroup: Boolean(chatMeta?.isGroup), + unreadCount: chatMeta?.unreadCount ?? 0, + participants: buildParticipants(raw), + messages: raw.slice(-100).map(toMsg).sort((a, b) => a.ts - b.ts), + createdAt: Date.now(), + }; + + conversations.set(chatId, convo); + return convo; +} + +/** + * Collects unique participants from every message (and group metadata if any). + */ +function buildParticipants(messages: WhatsAppMessage[]): Participant[] { + const map = new Map(); + + for (const m of messages) { + const s: any = m.sender ?? m.chat?.contact ?? null; + if (!s) continue; + + if (!map.has(s.id)) { + map.set(s.id, { + id: s.id, + name: s.pushname || s.name || '', + isMe: Boolean(s.isMe), + isAdmin: Boolean(s.isAdmin || s.isSuperAdmin), + }); + } + } + + return [...map.values()]; +} + +function ensureParticipant(list: Participant[], sender: any): void { + if (!sender) return; + if (list.some((p) => p.id === sender.id)) return; + + list.push({ + id: sender.id, + name: sender.pushname || sender.name || '', + isMe: Boolean(sender.isMe), + }); +} + +/** + * Normalises a raw WhatsApp message to the lightweight {@link Msg} format. + */ +function toMsg(m: WhatsAppMessage): Msg { + const anyMsg = m as any; return { id: m.id, from: m.from, to: m.to, - ts: (m as any).timestamp || (m as any).t, - type: ((m as any).type as any) || 'chat', - text: (m as any).text || (m as any).caption || (m as any).body, - mediaUrl: (m as any).cloudUrl || (m as any).clientUrl, - mentions: ((m as any).mentionedJidList as any) || [], + ts: anyMsg.timestamp ?? anyMsg.t ?? Date.now(), + type: anyMsg.type ?? 'chat', + text: anyMsg.text ?? anyMsg.caption ?? anyMsg.body ?? '', + mediaUrl: anyMsg.cloudUrl ?? anyMsg.clientUrl ?? undefined, + mentions: anyMsg.mentionedJidList ?? [], meta: { - ack: (m as any).ack || 0, - hasReaction: (m as any).hasReaction || false, - isQuoted: !!(m as any).quotedMsg, + ack: anyMsg.ack ?? 0, + hasReaction: Boolean(anyMsg.hasReaction), + isQuoted: Boolean(anyMsg.quotedMsg), }, }; } -export async function getConversation( - chatId: string, - openWaUrl: string -): Promise { - console.log(`[conversationStore] Retrieving conversation for ${chatId}`); +// ─────────────────────────────────────────────────────────── public API ────── + +export async function getConversation(chatId: string, openWaUrl: string): Promise { let conv = conversations.get(chatId); - if (!conv) { - conv = await buildConversation(chatId, openWaUrl); - } + if (!conv) conv = await buildConversation(chatId, openWaUrl); return conv; } export function listConversations(): Conversation[] { - console.log('[conversationStore] Listing conversations'); - return Array.from(conversations.values()); -} - -export async function buildConversation( - chatId: string, - openWaUrl: string -): Promise { - console.log(`[conversationStore] Building conversation for ${chatId}`); - const rawMessages = await loadMessages(chatId, openWaUrl); - const now = Date.now(); - - const first = rawMessages[0]; - const chat = first?.chat; - const title = chat?.formattedTitle || chat?.name || chatId; - const isGroup = chat?.isGroup || false; - const unreadCount = chat?.unreadCount || 0; - - const participantsMap = new Map(); - if (chat?.contact) { - const c = chat.contact; - participantsMap.set(c.id, { - id: c.id, - name: c.pushname || c.name || '', - isMe: c.isMe, - }); - } - if (isGroup && chat?.groupMetadata?.participants) { - for (const p of chat.groupMetadata.participants as any[]) { - const c = p.contact || {}; - const id = c.id || p.id; - participantsMap.set(id, { - id, - name: c.pushname || c.name || '', - isMe: c.isMe || false, - isAdmin: p.isAdmin || p.isSuperAdmin, - }); - } - } - - for (const m of rawMessages) { - const s = m.sender; - if (s && !participantsMap.has(s.id)) { - participantsMap.set(s.id, { - id: s.id, - name: s.pushname || s.name || '', - isMe: s.isMe, - }); - } - } - - const messages: Msg[] = rawMessages.slice(-20).map(mapMessage); - messages.sort((a, b) => a.ts - b.ts); - - const conv: Conversation = { - chatId, - title, - isGroup, - unreadCount, - participants: Array.from(participantsMap.values()), - messages, - createdAt: conversations.get(chatId)?.createdAt || now, - }; - - conversations.set(chatId, conv); - return conv; + return [...conversations.values()]; } export function deleteConversation(chatId: string): boolean { - console.log(`[conversationStore] Deleting conversation ${chatId}`); return conversations.delete(chatId); } export async function addMessageToConversation( chatId: string, - msg: WhatsAppMessage, - openWaUrl: string + waMsg: WhatsAppMessage, + openWaUrl: string, ): Promise { - console.log(`[conversationStore] Adding message to ${chatId}`); - const conv = await getConversation(chatId, openWaUrl); - const mapped = mapMessage(msg); - conv.messages.push(mapped); - if (conv.messages.length > 20) conv.messages.shift(); - const s = msg.sender; - if (s && !conv.participants.some((p) => p.id === s.id)) { - conv.participants.push({ - id: s.id, - name: s.pushname || s.name || '', - isMe: s.isMe, - }); - } - return conv; -} + const convo = await getConversation(chatId, openWaUrl); + const msg = toMsg(waMsg); + convo.messages.push(msg); + + // keep last 100 msgs only + if (convo.messages.length > 100) convo.messages.shift(); + + ensureParticipant(convo.participants, waMsg.sender); + return convo; +} \ No newline at end of file