se incluye al propio agente usuario de la cuenta de whatsapp dentro del convo, se actualiza system prompt para el agent
All checks were successful
Deploy conversation layer / deploy (push) Successful in 22s
All checks were successful
Deploy conversation layer / deploy (push) Successful in 22s
This commit is contained in:
@@ -57,11 +57,70 @@ 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.
|
/**
|
||||||
- whatsapp-router: Forwards WhatsApp messages to configured agents.
|
* Descripción de alto nivel para que cualquier agente (humano o LLM) entienda y
|
||||||
- chat-ui: Minimal web interface that also talks to an agent.
|
* trabaje con el repositorio sin perder tiempo buscando contexto.
|
||||||
- conversation-layer-agent: Answers questions about the repository.
|
*/
|
||||||
Run all services with docker-compose. Configure handler mappings in whatsapp-router/src/chatHandlers.ts.`;
|
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();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|||||||
@@ -1,145 +1,131 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { WhatsAppMessage, Conversation, Msg, Participant } from '../types';
|
import { WhatsAppMessage, Conversation, Msg, Participant } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In‑memory cache of conversations indexed by chatId.
|
||||||
|
*/
|
||||||
const conversations = new Map<string, Conversation>();
|
const conversations = new Map<string, Conversation>();
|
||||||
|
|
||||||
async function loadMessages(
|
|
||||||
chatId: string,
|
|
||||||
openWaUrl: string
|
// ─────────────────────────────────────────────────────── internal helpers ────
|
||||||
): Promise<WhatsAppMessage[]> {
|
|
||||||
console.log(`[conversationStore] Loading messages for ${chatId}`);
|
/**
|
||||||
|
* Fetches *all* messages for a chat using OpenWA and returns them as‑is.
|
||||||
|
*/
|
||||||
|
async function fetchChatMessages(chatId: string, openWaUrl: string): Promise<WhatsAppMessage[]> {
|
||||||
const { data } = await axios.post(`${openWaUrl}/loadAndGetAllMessagesInChat`, {
|
const { data } = await axios.post(`${openWaUrl}/loadAndGetAllMessagesInChat`, {
|
||||||
args: {
|
args: { chatId, includeMe: true, includeNotifications: true },
|
||||||
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<Conversation> {
|
||||||
|
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<string, Participant>();
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
id: m.id,
|
id: m.id,
|
||||||
from: m.from,
|
from: m.from,
|
||||||
to: m.to,
|
to: m.to,
|
||||||
ts: (m as any).timestamp || (m as any).t,
|
ts: anyMsg.timestamp ?? anyMsg.t ?? Date.now(),
|
||||||
type: ((m as any).type as any) || 'chat',
|
type: anyMsg.type ?? 'chat',
|
||||||
text: (m as any).text || (m as any).caption || (m as any).body,
|
text: anyMsg.text ?? anyMsg.caption ?? anyMsg.body ?? '',
|
||||||
mediaUrl: (m as any).cloudUrl || (m as any).clientUrl,
|
mediaUrl: anyMsg.cloudUrl ?? anyMsg.clientUrl ?? undefined,
|
||||||
mentions: ((m as any).mentionedJidList as any) || [],
|
mentions: anyMsg.mentionedJidList ?? [],
|
||||||
meta: {
|
meta: {
|
||||||
ack: (m as any).ack || 0,
|
ack: anyMsg.ack ?? 0,
|
||||||
hasReaction: (m as any).hasReaction || false,
|
hasReaction: Boolean(anyMsg.hasReaction),
|
||||||
isQuoted: !!(m as any).quotedMsg,
|
isQuoted: Boolean(anyMsg.quotedMsg),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getConversation(
|
// ─────────────────────────────────────────────────────────── public API ──────
|
||||||
chatId: string,
|
|
||||||
openWaUrl: string
|
export async function getConversation(chatId: string, openWaUrl: string): Promise<Conversation> {
|
||||||
): Promise<Conversation> {
|
|
||||||
console.log(`[conversationStore] Retrieving conversation for ${chatId}`);
|
|
||||||
let conv = conversations.get(chatId);
|
let conv = conversations.get(chatId);
|
||||||
if (!conv) {
|
if (!conv) conv = await buildConversation(chatId, openWaUrl);
|
||||||
conv = await buildConversation(chatId, openWaUrl);
|
|
||||||
}
|
|
||||||
return conv;
|
return conv;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listConversations(): Conversation[] {
|
export function listConversations(): Conversation[] {
|
||||||
console.log('[conversationStore] Listing conversations');
|
return [...conversations.values()];
|
||||||
return Array.from(conversations.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buildConversation(
|
|
||||||
chatId: string,
|
|
||||||
openWaUrl: string
|
|
||||||
): Promise<Conversation> {
|
|
||||||
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<string, Participant>();
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteConversation(chatId: string): boolean {
|
export function deleteConversation(chatId: string): boolean {
|
||||||
console.log(`[conversationStore] Deleting conversation ${chatId}`);
|
|
||||||
return conversations.delete(chatId);
|
return conversations.delete(chatId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addMessageToConversation(
|
export async function addMessageToConversation(
|
||||||
chatId: string,
|
chatId: string,
|
||||||
msg: WhatsAppMessage,
|
waMsg: WhatsAppMessage,
|
||||||
openWaUrl: string
|
openWaUrl: string,
|
||||||
): Promise<Conversation> {
|
): Promise<Conversation> {
|
||||||
console.log(`[conversationStore] Adding message to ${chatId}`);
|
const convo = await getConversation(chatId, openWaUrl);
|
||||||
const conv = await getConversation(chatId, openWaUrl);
|
const msg = toMsg(waMsg);
|
||||||
const mapped = mapMessage(msg);
|
convo.messages.push(msg);
|
||||||
conv.messages.push(mapped);
|
|
||||||
if (conv.messages.length > 20) conv.messages.shift();
|
// keep last 100 msgs only
|
||||||
const s = msg.sender;
|
if (convo.messages.length > 100) convo.messages.shift();
|
||||||
if (s && !conv.participants.some((p) => p.id === s.id)) {
|
|
||||||
conv.participants.push({
|
ensureParticipant(convo.participants, waMsg.sender);
|
||||||
id: s.id,
|
return convo;
|
||||||
name: s.pushname || s.name || '',
|
}
|
||||||
isMe: s.isMe,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return conv;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user