adaptive-memory-multi-model-router 1.9.1 ā 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -24,6 +24,7 @@ const GmailIntegration = require('./gmail.js').GmailIntegration;
|
|
|
24
24
|
const DiscordIntegration = require('./discord.js').DiscordIntegration;
|
|
25
25
|
const AirtableIntegration = require('./airtable.js').AirtableIntegration;
|
|
26
26
|
const GoogleCalendarIntegration = require('./google-calendar.js').GoogleCalendarIntegration;
|
|
27
|
+
const WhatsAppIntegration = require('./whatsapp.js').WhatsAppIntegration;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Factory to create integrations
|
|
@@ -40,6 +41,7 @@ function createIntegration(type, config) {
|
|
|
40
41
|
case 'discord': return new DiscordIntegration(config.webhookUrl);
|
|
41
42
|
case 'airtable': return new AirtableIntegration(config.apiKey, config.baseId);
|
|
42
43
|
case 'google-calendar': return new GoogleCalendarIntegration(config.credentials);
|
|
44
|
+
case 'whatsapp': return new WhatsAppIntegration(config.phoneNumberId, config.accessToken);
|
|
43
45
|
default: throw new Error(`Unknown integration type: ${type}`);
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -56,6 +58,7 @@ module.exports = {
|
|
|
56
58
|
DiscordIntegration,
|
|
57
59
|
AirtableIntegration,
|
|
58
60
|
GoogleCalendarIntegration,
|
|
61
|
+
WhatsAppIntegration,
|
|
59
62
|
// Factory
|
|
60
63
|
createIntegration
|
|
61
64
|
};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WhatsApp ā Telegram Bridge Demo
|
|
4
|
+
*
|
|
5
|
+
* This demo shows how the bridge works without requiring real credentials.
|
|
6
|
+
* It uses mock implementations to demonstrate the flow.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Mock implementations for demo
|
|
10
|
+
class MockWhatsAppIntegration {
|
|
11
|
+
constructor(phoneNumberId, accessToken) {
|
|
12
|
+
this.phoneNumberId = phoneNumberId;
|
|
13
|
+
this.accessToken = accessToken;
|
|
14
|
+
console.log(`š± WhatsApp Integration initialized (Phone: ${phoneNumberId})`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async sendMessage(to, body) {
|
|
18
|
+
console.log(` š¤ WhatsApp ā ${to}: "${body.substring(0, 50)}..."`);
|
|
19
|
+
return {
|
|
20
|
+
messaging_product: 'whatsapp',
|
|
21
|
+
contacts: [{ input: to, wa_id: to }],
|
|
22
|
+
messages: [{ id: `wamid.${Date.now()}` }]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getMessages() {
|
|
27
|
+
return { messages: [] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class MockTelegramIntegration {
|
|
32
|
+
constructor(botToken) {
|
|
33
|
+
this.botToken = botToken;
|
|
34
|
+
this.messageId = 1000;
|
|
35
|
+
console.log(`š¬ Telegram Integration initialized (Bot: ${botToken.substring(0, 10)}...)`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async sendMessage(chatId, text) {
|
|
39
|
+
this.messageId++;
|
|
40
|
+
console.log(` š¤ Telegram ā Chat ${chatId}:`);
|
|
41
|
+
console.log(` ${text.substring(0, 100)}...`);
|
|
42
|
+
return {
|
|
43
|
+
message_id: this.messageId,
|
|
44
|
+
chat: { id: chatId },
|
|
45
|
+
text: text
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getUpdates() {
|
|
50
|
+
return { result: [] };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Import A3M Router
|
|
55
|
+
const { createA3MRouter } = require('../dist/index.js');
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* WhatsApp ā Telegram Bridge Demo
|
|
59
|
+
*/
|
|
60
|
+
class WhatsAppTelegramBridgeDemo {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.whatsapp = new MockWhatsAppIntegration('1234567890', 'mock_token');
|
|
63
|
+
this.telegram = new MockTelegramIntegration('mock_bot_token_12345');
|
|
64
|
+
this.telegramChatId = '987654321';
|
|
65
|
+
|
|
66
|
+
// A3M Router for intelligent routing
|
|
67
|
+
this.router = createA3MRouter();
|
|
68
|
+
|
|
69
|
+
// Message tracking
|
|
70
|
+
this.pendingReplies = new Map();
|
|
71
|
+
this.demoMode = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async handleWhatsAppMessage(whatsappMessage) {
|
|
75
|
+
console.log('\nš© STEP 1: Received WhatsApp Message');
|
|
76
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
77
|
+
|
|
78
|
+
const entry = whatsappMessage.entry?.[0];
|
|
79
|
+
const change = entry?.changes?.[0];
|
|
80
|
+
const value = change?.value;
|
|
81
|
+
const message = value?.messages?.[0];
|
|
82
|
+
|
|
83
|
+
if (!message) {
|
|
84
|
+
console.log('ā No message in payload');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const from = message.from;
|
|
89
|
+
const text = message.text?.body || '';
|
|
90
|
+
const messageId = message.id;
|
|
91
|
+
|
|
92
|
+
console.log(` From: ${from}`);
|
|
93
|
+
console.log(` Text: "${text}"`);
|
|
94
|
+
console.log(` Message ID: ${messageId}`);
|
|
95
|
+
|
|
96
|
+
// Route the message
|
|
97
|
+
console.log('\nš STEP 2: Route Message via A3M Router');
|
|
98
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
99
|
+
|
|
100
|
+
const route = this.router.route(text);
|
|
101
|
+
console.log(` Primary Model: ${route.primary_model}`);
|
|
102
|
+
console.log(` Fallbacks: ${route.fallback_models.slice(0, 2).join(', ')}`);
|
|
103
|
+
console.log(` Estimated Cost: $${route.estimated_cost.toFixed(6)}`);
|
|
104
|
+
console.log(` Reasoning: ${route.reasoning}`);
|
|
105
|
+
|
|
106
|
+
// Forward to Telegram
|
|
107
|
+
console.log('\nš¤ STEP 3: Forward to Telegram Bot');
|
|
108
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
109
|
+
|
|
110
|
+
const telegramResult = await this.forwardToTelegram(from, text, route);
|
|
111
|
+
|
|
112
|
+
this.pendingReplies.set(telegramResult.messageId, {
|
|
113
|
+
whatsappUser: from,
|
|
114
|
+
originalText: text,
|
|
115
|
+
timestamp: Date.now(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log(` Telegram Message ID: ${telegramResult.messageId}`);
|
|
119
|
+
console.log(` Status: ā
Forwarded successfully`);
|
|
120
|
+
|
|
121
|
+
// Simulate Telegram bot reply
|
|
122
|
+
console.log('\nš© STEP 4: Telegram Bot Processes & Replies');
|
|
123
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
124
|
+
|
|
125
|
+
await this.simulateTelegramReply(telegramResult.messageId, text);
|
|
126
|
+
|
|
127
|
+
return telegramResult;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async forwardToTelegram(whatsappUser, text, route) {
|
|
131
|
+
const formattedMessage = `
|
|
132
|
+
š <b>WhatsApp Bridge</b>
|
|
133
|
+
š± <b>From:</b> ${whatsappUser}
|
|
134
|
+
š¤ <b>Route:</b> ${route.primary_model}
|
|
135
|
+
š° <b>Est. Cost:</b> $${route.estimated_cost.toFixed(6)}
|
|
136
|
+
|
|
137
|
+
š¬ <b>Message:</b>
|
|
138
|
+
${text}
|
|
139
|
+
|
|
140
|
+
<i>Reply to this message to respond back to WhatsApp user</i>
|
|
141
|
+
`.trim();
|
|
142
|
+
|
|
143
|
+
const result = await this.telegram.sendMessage(this.telegramChatId, formattedMessage);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
messageId: result.message_id,
|
|
147
|
+
chatId: this.telegramChatId,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async simulateTelegramReply(originalMessageId, originalText) {
|
|
152
|
+
// Simulate bot processing time
|
|
153
|
+
console.log(' š¤ Bot is processing...');
|
|
154
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
155
|
+
|
|
156
|
+
// Generate contextual reply
|
|
157
|
+
let reply;
|
|
158
|
+
if (originalText.toLowerCase().includes('order')) {
|
|
159
|
+
reply = `I've checked your order. It will be shipped tomorrow! š¦`;
|
|
160
|
+
} else if (originalText.toLowerCase().includes('help')) {
|
|
161
|
+
reply = `I'm here to help! What do you need assistance with? š¤`;
|
|
162
|
+
} else if (originalText.toLowerCase().includes('price')) {
|
|
163
|
+
reply = `Let me check the pricing for you. One moment please... š°`;
|
|
164
|
+
} else {
|
|
165
|
+
reply = `Thanks for your message! Our team will get back to you shortly. ā°`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(` Bot Reply: "${reply}"`);
|
|
169
|
+
|
|
170
|
+
// Handle the reply
|
|
171
|
+
await this.handleTelegramReply({
|
|
172
|
+
message: {
|
|
173
|
+
message_id: Date.now(),
|
|
174
|
+
reply_to_message: { message_id: originalMessageId },
|
|
175
|
+
text: reply,
|
|
176
|
+
chat: { id: this.telegramChatId }
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async handleTelegramReply(telegramUpdate) {
|
|
182
|
+
console.log('\nš¤ STEP 5: Send Reply Back to WhatsApp');
|
|
183
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
184
|
+
|
|
185
|
+
const message = telegramUpdate.message;
|
|
186
|
+
const replyToMessage = message.reply_to_message;
|
|
187
|
+
const originalMessageId = replyToMessage.message_id;
|
|
188
|
+
|
|
189
|
+
const pending = this.pendingReplies.get(originalMessageId);
|
|
190
|
+
if (!pending) {
|
|
191
|
+
console.log('ā No pending reply found');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const replyText = message.text;
|
|
196
|
+
console.log(` Original WhatsApp User: ${pending.whatsappUser}`);
|
|
197
|
+
console.log(` Reply Text: "${replyText}"`);
|
|
198
|
+
|
|
199
|
+
// Send back to WhatsApp
|
|
200
|
+
await this.whatsapp.sendMessage(pending.whatsappUser, replyText);
|
|
201
|
+
|
|
202
|
+
this.pendingReplies.delete(originalMessageId);
|
|
203
|
+
|
|
204
|
+
console.log(' Status: ā
Reply sent successfully');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Demo scenarios
|
|
209
|
+
const DEMO_MESSAGES = [
|
|
210
|
+
{
|
|
211
|
+
description: 'Customer asking about order',
|
|
212
|
+
payload: {
|
|
213
|
+
entry: [{
|
|
214
|
+
changes: [{
|
|
215
|
+
value: {
|
|
216
|
+
messages: [{
|
|
217
|
+
from: '+1234567890',
|
|
218
|
+
id: 'wamid.demo1',
|
|
219
|
+
text: { body: 'Hello, I need help with my order #12345' },
|
|
220
|
+
timestamp: Date.now().toString(),
|
|
221
|
+
}],
|
|
222
|
+
},
|
|
223
|
+
}],
|
|
224
|
+
}],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
description: 'Customer asking for pricing',
|
|
229
|
+
payload: {
|
|
230
|
+
entry: [{
|
|
231
|
+
changes: [{
|
|
232
|
+
value: {
|
|
233
|
+
messages: [{
|
|
234
|
+
from: '+9876543210',
|
|
235
|
+
id: 'wamid.demo2',
|
|
236
|
+
text: { body: 'What is the price for your premium plan?' },
|
|
237
|
+
timestamp: Date.now().toString(),
|
|
238
|
+
}],
|
|
239
|
+
},
|
|
240
|
+
}],
|
|
241
|
+
}],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
description: 'General inquiry',
|
|
246
|
+
payload: {
|
|
247
|
+
entry: [{
|
|
248
|
+
changes: [{
|
|
249
|
+
value: {
|
|
250
|
+
messages: [{
|
|
251
|
+
from: '+5555555555',
|
|
252
|
+
id: 'wamid.demo3',
|
|
253
|
+
text: { body: 'Hi there, I have a question about your services' },
|
|
254
|
+
timestamp: Date.now().toString(),
|
|
255
|
+
}],
|
|
256
|
+
},
|
|
257
|
+
}],
|
|
258
|
+
}],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
// Run demo
|
|
264
|
+
async function main() {
|
|
265
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
266
|
+
console.log('š± WhatsApp ā Telegram Bridge Demo');
|
|
267
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log('This demo shows how to use a Telegram bot to process');
|
|
270
|
+
console.log('WhatsApp messages and send replies back.');
|
|
271
|
+
console.log('');
|
|
272
|
+
|
|
273
|
+
const bridge = new WhatsAppTelegramBridgeDemo();
|
|
274
|
+
|
|
275
|
+
for (const scenario of DEMO_MESSAGES) {
|
|
276
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
277
|
+
console.log(`š Scenario: ${scenario.description}`);
|
|
278
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
279
|
+
|
|
280
|
+
await bridge.handleWhatsAppMessage(scenario.payload);
|
|
281
|
+
|
|
282
|
+
// Wait between scenarios
|
|
283
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
287
|
+
console.log('ā
Demo Complete!');
|
|
288
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
289
|
+
console.log('');
|
|
290
|
+
console.log('To use in production:');
|
|
291
|
+
console.log(' 1. Get WhatsApp Business API credentials');
|
|
292
|
+
console.log(' 2. Create Telegram bot via @BotFather');
|
|
293
|
+
console.log(' 3. Set up webhooks for both platforms');
|
|
294
|
+
console.log(' 4. Use whatsapp-telegram-bridge.js with real credentials');
|
|
295
|
+
console.log('');
|
|
296
|
+
console.log('Documentation:');
|
|
297
|
+
console.log(' - WhatsApp: https://business.whatsapp.com/products/business-platform');
|
|
298
|
+
console.log(' - Telegram: https://core.telegram.org/bots/api');
|
|
299
|
+
console.log('');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WhatsApp ā Telegram Bridge Example
|
|
4
|
+
*
|
|
5
|
+
* This example shows how to use a Telegram bot to process
|
|
6
|
+
* WhatsApp messages and send replies back.
|
|
7
|
+
*
|
|
8
|
+
* Prerequisites:
|
|
9
|
+
* 1. WhatsApp Business API credentials (phoneNumberId, accessToken)
|
|
10
|
+
* 2. Telegram Bot Token (from @BotFather)
|
|
11
|
+
* 3. Webhook endpoint to receive WhatsApp messages
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node whatsapp-telegram-bridge.js
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
createIntegration,
|
|
19
|
+
WhatsAppIntegration,
|
|
20
|
+
TelegramIntegration,
|
|
21
|
+
createA3MRouter
|
|
22
|
+
} = require('../dist/index.js');
|
|
23
|
+
|
|
24
|
+
// Configuration
|
|
25
|
+
const CONFIG = {
|
|
26
|
+
whatsapp: {
|
|
27
|
+
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID,
|
|
28
|
+
accessToken: process.env.WHATSAPP_ACCESS_TOKEN,
|
|
29
|
+
},
|
|
30
|
+
telegram: {
|
|
31
|
+
botToken: process.env.TELEGRAM_BOT_TOKEN,
|
|
32
|
+
// The chat ID where your bot will process messages
|
|
33
|
+
// This could be your personal chat with the bot
|
|
34
|
+
chatId: process.env.TELEGRAM_CHAT_ID,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Validate config
|
|
39
|
+
function validateConfig() {
|
|
40
|
+
const missing = [];
|
|
41
|
+
if (!CONFIG.whatsapp.phoneNumberId) missing.push('WHATSAPP_PHONE_NUMBER_ID');
|
|
42
|
+
if (!CONFIG.whatsapp.accessToken) missing.push('WHATSAPP_ACCESS_TOKEN');
|
|
43
|
+
if (!CONFIG.telegram.botToken) missing.push('TELEGRAM_BOT_TOKEN');
|
|
44
|
+
if (!CONFIG.telegram.chatId) missing.push('TELEGRAM_CHAT_ID');
|
|
45
|
+
|
|
46
|
+
if (missing.length > 0) {
|
|
47
|
+
console.error('ā Missing environment variables:');
|
|
48
|
+
missing.forEach(v => console.error(` - ${v}`));
|
|
49
|
+
console.error('\nSet these variables and try again.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* WhatsApp ā Telegram Bridge
|
|
56
|
+
*
|
|
57
|
+
* Flow:
|
|
58
|
+
* 1. Receive message from WhatsApp (via webhook)
|
|
59
|
+
* 2. Forward message to Telegram bot
|
|
60
|
+
* 3. Get reply from Telegram (bot processes it)
|
|
61
|
+
* 4. Send reply back to WhatsApp user
|
|
62
|
+
*/
|
|
63
|
+
class WhatsAppTelegramBridge {
|
|
64
|
+
constructor(config) {
|
|
65
|
+
this.whatsapp = new WhatsAppIntegration(
|
|
66
|
+
config.whatsapp.phoneNumberId,
|
|
67
|
+
config.whatsapp.accessToken
|
|
68
|
+
);
|
|
69
|
+
this.telegram = new TelegramIntegration(config.telegram.botToken);
|
|
70
|
+
this.telegramChatId = config.telegram.chatId;
|
|
71
|
+
|
|
72
|
+
// A3M Router for intelligent routing
|
|
73
|
+
this.router = createA3MRouter();
|
|
74
|
+
|
|
75
|
+
// Message tracking
|
|
76
|
+
this.pendingReplies = new Map(); // messageId -> { whatsappUser, timestamp }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle incoming WhatsApp message
|
|
81
|
+
* @param {Object} whatsappMessage - WhatsApp webhook payload
|
|
82
|
+
*/
|
|
83
|
+
async handleWhatsAppMessage(whatsappMessage) {
|
|
84
|
+
try {
|
|
85
|
+
// Extract message details
|
|
86
|
+
const entry = whatsappMessage.entry?.[0];
|
|
87
|
+
const change = entry?.changes?.[0];
|
|
88
|
+
const value = change?.value;
|
|
89
|
+
const message = value?.messages?.[0];
|
|
90
|
+
|
|
91
|
+
if (!message) {
|
|
92
|
+
console.log('No message in webhook payload');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const from = message.from; // WhatsApp user phone number
|
|
97
|
+
const text = message.text?.body || '';
|
|
98
|
+
const messageId = message.id;
|
|
99
|
+
|
|
100
|
+
console.log(`š© WhatsApp message from ${from}: "${text.substring(0, 50)}..."`);
|
|
101
|
+
|
|
102
|
+
// Route the message to get best processing strategy
|
|
103
|
+
const route = this.router.route(text);
|
|
104
|
+
console.log(`š Routed to: ${route.primary_model} (${route.reasoning})`);
|
|
105
|
+
|
|
106
|
+
// Forward to Telegram bot with context
|
|
107
|
+
const telegramMessage = await this.forwardToTelegram(from, text, route);
|
|
108
|
+
|
|
109
|
+
// Track pending reply
|
|
110
|
+
this.pendingReplies.set(telegramMessage.messageId, {
|
|
111
|
+
whatsappUser: from,
|
|
112
|
+
originalText: text,
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(`š¤ Forwarded to Telegram (message ID: ${telegramMessage.messageId})`);
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('ā Error handling WhatsApp message:', error.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Forward WhatsApp message to Telegram
|
|
125
|
+
*/
|
|
126
|
+
async forwardToTelegram(whatsappUser, text, route) {
|
|
127
|
+
// Format message for Telegram bot
|
|
128
|
+
const formattedMessage = `
|
|
129
|
+
š <b>WhatsApp Bridge</b>
|
|
130
|
+
š± <b>From:</b> ${whatsappUser}
|
|
131
|
+
š¤ <b>Route:</b> ${route.primary_model}
|
|
132
|
+
|
|
133
|
+
š¬ <b>Message:</b>
|
|
134
|
+
${text}
|
|
135
|
+
|
|
136
|
+
<i>Reply to this message to respond back to WhatsApp user</i>
|
|
137
|
+
`.trim();
|
|
138
|
+
|
|
139
|
+
// Send to Telegram
|
|
140
|
+
const result = await this.telegram.sendMessage(
|
|
141
|
+
this.telegramChatId,
|
|
142
|
+
formattedMessage
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
messageId: result.message_id || `msg_${Date.now()}`,
|
|
147
|
+
chatId: this.telegramChatId,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Handle Telegram reply
|
|
153
|
+
* @param {Object} telegramUpdate - Telegram webhook/update payload
|
|
154
|
+
*/
|
|
155
|
+
async handleTelegramReply(telegramUpdate) {
|
|
156
|
+
try {
|
|
157
|
+
const message = telegramUpdate.message || telegramUpdate.callback_query?.message;
|
|
158
|
+
if (!message) return;
|
|
159
|
+
|
|
160
|
+
const replyToMessage = message.reply_to_message;
|
|
161
|
+
if (!replyToMessage) {
|
|
162
|
+
console.log('Not a reply message, ignoring');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract original message ID from reply
|
|
167
|
+
const originalMessageId = replyToMessage.message_id;
|
|
168
|
+
const pending = this.pendingReplies.get(originalMessageId);
|
|
169
|
+
|
|
170
|
+
if (!pending) {
|
|
171
|
+
console.log('No pending WhatsApp reply found for this message');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const replyText = message.text || message.caption || '';
|
|
176
|
+
console.log(`š© Telegram reply: "${replyText.substring(0, 50)}..."`);
|
|
177
|
+
|
|
178
|
+
// Send reply back to WhatsApp
|
|
179
|
+
await this.sendWhatsAppReply(pending.whatsappUser, replyText);
|
|
180
|
+
|
|
181
|
+
// Clean up pending reply
|
|
182
|
+
this.pendingReplies.delete(originalMessageId);
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('ā Error handling Telegram reply:', error.message);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Send reply back to WhatsApp user
|
|
191
|
+
*/
|
|
192
|
+
async sendWhatsAppReply(to, text) {
|
|
193
|
+
const result = await this.whatsapp.sendMessage(to, text);
|
|
194
|
+
console.log(`š¤ Sent WhatsApp reply to ${to}: "${text.substring(0, 50)}..."`);
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Clean up old pending replies (older than 1 hour)
|
|
200
|
+
*/
|
|
201
|
+
cleanupPendingReplies() {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const oneHour = 60 * 60 * 1000;
|
|
204
|
+
|
|
205
|
+
for (const [messageId, pending] of this.pendingReplies) {
|
|
206
|
+
if (now - pending.timestamp > oneHour) {
|
|
207
|
+
this.pendingReplies.delete(messageId);
|
|
208
|
+
console.log(`š§¹ Cleaned up expired pending reply: ${messageId}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Start cleanup interval
|
|
215
|
+
*/
|
|
216
|
+
startCleanup() {
|
|
217
|
+
setInterval(() => this.cleanupPendingReplies(), 5 * 60 * 1000); // Every 5 minutes
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Example usage
|
|
222
|
+
async function main() {
|
|
223
|
+
validateConfig();
|
|
224
|
+
|
|
225
|
+
console.log('š Starting WhatsApp ā Telegram Bridge\n');
|
|
226
|
+
|
|
227
|
+
const bridge = new WhatsAppTelegramBridge(CONFIG);
|
|
228
|
+
bridge.startCleanup();
|
|
229
|
+
|
|
230
|
+
console.log('ā
Bridge initialized');
|
|
231
|
+
console.log(` WhatsApp Phone Number ID: ${CONFIG.whatsapp.phoneNumberId}`);
|
|
232
|
+
console.log(` Telegram Chat ID: ${CONFIG.telegram.chatId}`);
|
|
233
|
+
console.log('');
|
|
234
|
+
|
|
235
|
+
// Example: Simulate receiving a WhatsApp message
|
|
236
|
+
console.log('š Example: Simulating WhatsApp webhook payload\n');
|
|
237
|
+
|
|
238
|
+
const exampleWhatsAppMessage = {
|
|
239
|
+
entry: [{
|
|
240
|
+
changes: [{
|
|
241
|
+
value: {
|
|
242
|
+
messages: [{
|
|
243
|
+
from: '1234567890',
|
|
244
|
+
id: 'wamid.example123',
|
|
245
|
+
text: { body: 'Hello, I need help with my order #12345' },
|
|
246
|
+
timestamp: Date.now().toString(),
|
|
247
|
+
}],
|
|
248
|
+
},
|
|
249
|
+
}],
|
|
250
|
+
}],
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await bridge.handleWhatsAppMessage(exampleWhatsAppMessage);
|
|
254
|
+
|
|
255
|
+
console.log('\nš To use this in production:');
|
|
256
|
+
console.log(' 1. Set up WhatsApp Business API webhook');
|
|
257
|
+
console.log(' 2. Set up Telegram bot webhook');
|
|
258
|
+
console.log(' 3. Route webhooks to handleWhatsAppMessage() and handleTelegramReply()');
|
|
259
|
+
console.log('');
|
|
260
|
+
console.log('š See: https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks');
|
|
261
|
+
console.log('š See: https://core.telegram.org/bots/webhooks');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Run if executed directly
|
|
265
|
+
if (require.main === module) {
|
|
266
|
+
main().catch(console.error);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = { WhatsAppTelegramBridge };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"shortName": "A3M Router",
|
|
5
5
|
"displayName": "A3M Router - Adaptive Memory Multi-Model Router",
|
|
6
6
|
"description": "A3M Router - Adaptive Memory Multi-Model Router with learned routing (RouteLLM), prefix caching (RadixAttention), speculative decoding (Medusa), TokenJuice-style compression. 14 LLM providers, 10 integrations, Python bindings. 20x more adaptable for ML/AI developers.",
|
|
@@ -174,7 +174,10 @@
|
|
|
174
174
|
},
|
|
175
175
|
"homepage": "https://github.com/Das-rebel/adaptive-memory-multi-model-router#readme",
|
|
176
176
|
"scripts": {
|
|
177
|
-
"test": "node test.js"
|
|
177
|
+
"test": "node test.js && node test/provider-test.js",
|
|
178
|
+
"test:providers": "node test/provider-test.js",
|
|
179
|
+
"benchmark": "node test/benchmark.js",
|
|
180
|
+
"benchmark:verbose": "node test/benchmark.js --verbose"
|
|
178
181
|
},
|
|
179
182
|
"engines": {
|
|
180
183
|
"node": ">=16.0.0"
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router - Provider Benchmark
|
|
4
|
+
*
|
|
5
|
+
* Benchmarks all available providers across:
|
|
6
|
+
* - Latency (response time)
|
|
7
|
+
* - Cost (per 1K tokens)
|
|
8
|
+
* - Quality (simple factual questions)
|
|
9
|
+
* - Cost-effectiveness (quality per dollar)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const {
|
|
14
|
+
getAvailableProviders,
|
|
15
|
+
providerConfig,
|
|
16
|
+
countTokens,
|
|
17
|
+
estimateCost,
|
|
18
|
+
} = require('../dist/index.js');
|
|
19
|
+
|
|
20
|
+
// Benchmark configuration
|
|
21
|
+
const CONFIG = {
|
|
22
|
+
timeout: 60000,
|
|
23
|
+
maxTokens: 50,
|
|
24
|
+
verbose: process.argv.includes('--verbose') || process.argv.includes('-v'),
|
|
25
|
+
json: process.argv.includes('--json'),
|
|
26
|
+
provider: process.argv.find(arg => arg.startsWith('--provider='))?.split('=')[1],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Test queries for different scenarios
|
|
30
|
+
const QUERIES = {
|
|
31
|
+
simple: 'What is 2+2?',
|
|
32
|
+
code: 'Write a Python function to reverse a string.',
|
|
33
|
+
math: 'Calculate the square root of 144.',
|
|
34
|
+
creative: 'Write a haiku about programming.',
|
|
35
|
+
reasoning: 'Explain why the sky is blue.',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Results storage
|
|
39
|
+
const results = [];
|
|
40
|
+
|
|
41
|
+
// Helper: Call a provider
|
|
42
|
+
async function callProvider(id, provider, query) {
|
|
43
|
+
const model = provider.models[0];
|
|
44
|
+
const startTime = Date.now();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (provider.type === 'cli') {
|
|
48
|
+
// CLI provider
|
|
49
|
+
if (id === 'commandcode') {
|
|
50
|
+
const raw = execSync(`commandcode -p "${query.replace(/"/g, '\\"')}" --skip-onboarding 2>&1`, {
|
|
51
|
+
timeout: CONFIG.timeout,
|
|
52
|
+
encoding: 'utf-8'
|
|
53
|
+
});
|
|
54
|
+
const content = raw.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
55
|
+
const latency = Date.now() - startTime;
|
|
56
|
+
const tokens = Math.ceil(content.length / 4);
|
|
57
|
+
return {
|
|
58
|
+
content: content.substring(0, 200),
|
|
59
|
+
tokens,
|
|
60
|
+
cost: 0,
|
|
61
|
+
latency,
|
|
62
|
+
success: true,
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
// Generic CLI
|
|
66
|
+
const raw = execSync(`${provider.cliCommand} run "${query.replace(/"/g, '\\"')}" 2>&1`, {
|
|
67
|
+
timeout: CONFIG.timeout,
|
|
68
|
+
encoding: 'utf-8'
|
|
69
|
+
});
|
|
70
|
+
const lines = raw.replace(/\x1b\[[0-9;]*m/g, '').split('\n').filter(l => l.trim() && !l.startsWith('>'));
|
|
71
|
+
const content = lines.join(' ').trim();
|
|
72
|
+
const latency = Date.now() - startTime;
|
|
73
|
+
const tokens = Math.ceil(content.length / 4);
|
|
74
|
+
return {
|
|
75
|
+
content: content.substring(0, 200),
|
|
76
|
+
tokens,
|
|
77
|
+
cost: 0,
|
|
78
|
+
latency,
|
|
79
|
+
success: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// API provider
|
|
85
|
+
if (!provider.apiKey) {
|
|
86
|
+
return { error: 'No API key', success: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const resp = await fetch(provider.baseUrl, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
'Authorization': `Bearer ${provider.apiKey}`,
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
model,
|
|
97
|
+
messages: [{ role: 'user', content: query }],
|
|
98
|
+
max_tokens: CONFIG.maxTokens,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const latency = Date.now() - startTime;
|
|
103
|
+
const data = await resp.json();
|
|
104
|
+
|
|
105
|
+
if (data.error) {
|
|
106
|
+
return { error: data.error.message, success: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const content = data.choices?.[0]?.message?.content || '';
|
|
110
|
+
const promptTokens = data.usage?.prompt_tokens || countTokens(query);
|
|
111
|
+
const completionTokens = data.usage?.completion_tokens || countTokens(content);
|
|
112
|
+
const cost = (promptTokens / 1000 * provider.costPerK.input) +
|
|
113
|
+
(completionTokens / 1000 * provider.costPerK.output);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: content.substring(0, 200),
|
|
117
|
+
tokens: promptTokens + completionTokens,
|
|
118
|
+
cost,
|
|
119
|
+
latency,
|
|
120
|
+
success: true,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return { error: e.message, success: false };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Helper: Check answer quality (simple heuristic)
|
|
129
|
+
function checkQuality(query, response) {
|
|
130
|
+
const lower = response.toLowerCase();
|
|
131
|
+
|
|
132
|
+
if (query.includes('2+2')) {
|
|
133
|
+
return lower.includes('4') ? 1 : 0;
|
|
134
|
+
}
|
|
135
|
+
if (query.includes('square root of 144')) {
|
|
136
|
+
return lower.includes('12') ? 1 : 0;
|
|
137
|
+
}
|
|
138
|
+
if (query.includes('reverse a string')) {
|
|
139
|
+
return lower.includes('def') || lower.includes('function') ? 1 : 0;
|
|
140
|
+
}
|
|
141
|
+
if (query.includes('haiku')) {
|
|
142
|
+
// Check for 3 lines (rough haiku check)
|
|
143
|
+
const lines = response.split('\n').filter(l => l.trim());
|
|
144
|
+
return lines.length >= 2 ? 1 : 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Default: check for reasonable length
|
|
148
|
+
return response.length > 20 ? 0.8 : 0.5;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Main benchmark
|
|
152
|
+
async function runBenchmark() {
|
|
153
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
154
|
+
console.log('š A3M Router - Provider Benchmark');
|
|
155
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
156
|
+
|
|
157
|
+
const providers = getAvailableProviders();
|
|
158
|
+
const providerList = CONFIG.provider
|
|
159
|
+
? [[CONFIG.provider, providers[CONFIG.provider]]].filter(([_, p]) => p)
|
|
160
|
+
: Object.entries(providers);
|
|
161
|
+
|
|
162
|
+
if (providerList.length === 0) {
|
|
163
|
+
console.log('ā No providers available. Configure API keys in ~/.config/a3m-router/providers.json');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`Testing ${providerList.length} provider(s)...\n`);
|
|
168
|
+
|
|
169
|
+
for (const [id, provider] of providerList) {
|
|
170
|
+
if (CONFIG.verbose) {
|
|
171
|
+
console.log(`Testing ${provider.name}...`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const providerResults = {
|
|
175
|
+
id,
|
|
176
|
+
name: provider.name,
|
|
177
|
+
type: provider.type,
|
|
178
|
+
model: provider.models[0],
|
|
179
|
+
queries: {},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
for (const [queryType, query] of Object.entries(QUERIES)) {
|
|
183
|
+
if (CONFIG.verbose) {
|
|
184
|
+
console.log(` ${queryType}: "${query.substring(0, 40)}..."`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const result = await callProvider(id, provider, query);
|
|
188
|
+
|
|
189
|
+
if (result.success) {
|
|
190
|
+
result.quality = checkQuality(query, result.content);
|
|
191
|
+
result.costEffectiveness = result.cost > 0
|
|
192
|
+
? result.quality / result.cost
|
|
193
|
+
: result.quality * 1000; // Free providers get high score
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
providerResults.queries[queryType] = result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Calculate averages
|
|
200
|
+
const successful = Object.values(providerResults.queries).filter(r => r.success);
|
|
201
|
+
if (successful.length > 0) {
|
|
202
|
+
providerResults.avgLatency = successful.reduce((a, r) => a + r.latency, 0) / successful.length;
|
|
203
|
+
providerResults.avgCost = successful.reduce((a, r) => a + r.cost, 0) / successful.length;
|
|
204
|
+
providerResults.avgQuality = successful.reduce((a, r) => a + r.quality, 0) / successful.length;
|
|
205
|
+
providerResults.avgCostEffectiveness = successful.reduce((a, r) => a + r.costEffectiveness, 0) / successful.length;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
results.push(providerResults);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Output results
|
|
212
|
+
if (CONFIG.json) {
|
|
213
|
+
console.log(JSON.stringify(results, null, 2));
|
|
214
|
+
} else {
|
|
215
|
+
printResults(results);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function printResults(results) {
|
|
220
|
+
// Summary table
|
|
221
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
222
|
+
console.log('ā Provider Type Model Latency Cost Quality ā');
|
|
223
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤');
|
|
224
|
+
|
|
225
|
+
// Sort by cost-effectiveness
|
|
226
|
+
const sorted = [...results].sort((a, b) => (b.avgCostEffectiveness || 0) - (a.avgCostEffectiveness || 0));
|
|
227
|
+
|
|
228
|
+
for (const r of sorted) {
|
|
229
|
+
const name = r.name.substring(0, 17).padEnd(17);
|
|
230
|
+
const type = r.type.padEnd(7);
|
|
231
|
+
const model = (r.model || 'N/A').substring(0, 22).padEnd(22);
|
|
232
|
+
const latency = r.avgLatency ? `${Math.round(r.avgLatency)}ms`.padEnd(8) : 'N/A ';
|
|
233
|
+
const cost = r.avgCost !== undefined ? `$${r.avgCost.toFixed(4)}`.padEnd(7) : 'N/A ';
|
|
234
|
+
const quality = r.avgQuality ? `${(r.avgQuality * 100).toFixed(0)}%`.padEnd(7) : 'N/A ';
|
|
235
|
+
|
|
236
|
+
console.log(`ā ${name} ${type} ${model} ${latency} ${cost} ${quality} ā`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
240
|
+
console.log('');
|
|
241
|
+
|
|
242
|
+
// Rankings
|
|
243
|
+
console.log('š Rankings:');
|
|
244
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
245
|
+
|
|
246
|
+
// Fastest
|
|
247
|
+
const fastest = [...results].filter(r => r.avgLatency).sort((a, b) => a.avgLatency - b.avgLatency)[0];
|
|
248
|
+
if (fastest) {
|
|
249
|
+
console.log(` ā” Fastest: ${fastest.name} (${Math.round(fastest.avgLatency)}ms avg)`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Cheapest
|
|
253
|
+
const cheapest = [...results].filter(r => r.avgCost !== undefined).sort((a, b) => a.avgCost - b.avgCost)[0];
|
|
254
|
+
if (cheapest) {
|
|
255
|
+
console.log(` š° Cheapest: ${cheapest.name} ($${cheapest.avgCost.toFixed(6)} avg)`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Best quality
|
|
259
|
+
const bestQuality = [...results].filter(r => r.avgQuality).sort((a, b) => b.avgQuality - a.avgQuality)[0];
|
|
260
|
+
if (bestQuality) {
|
|
261
|
+
console.log(` šÆ Best Quality: ${bestQuality.name} (${(bestQuality.avgQuality * 100).toFixed(0)}% correct)`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Best cost-effectiveness
|
|
265
|
+
const bestValue = sorted[0];
|
|
266
|
+
if (bestValue) {
|
|
267
|
+
console.log(` ā Best Value: ${bestValue.name} (quality/$)`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log('');
|
|
271
|
+
|
|
272
|
+
// Detailed results
|
|
273
|
+
if (CONFIG.verbose) {
|
|
274
|
+
console.log('š Detailed Results:');
|
|
275
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
276
|
+
|
|
277
|
+
for (const r of results) {
|
|
278
|
+
console.log(`\n${r.name} (${r.type}):`);
|
|
279
|
+
|
|
280
|
+
for (const [queryType, result] of Object.entries(r.queries)) {
|
|
281
|
+
if (result.success) {
|
|
282
|
+
console.log(` ${queryType.padEnd(10)} ${result.latency}ms $${result.cost.toFixed(6)} Q:${(result.quality * 100).toFixed(0)}% "${result.content.substring(0, 50)}..."`);
|
|
283
|
+
} else {
|
|
284
|
+
console.log(` ${queryType.padEnd(10)} ā ${result.error || 'Failed'}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log('');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Run benchmark
|
|
294
|
+
runBenchmark().catch(e => {
|
|
295
|
+
console.error('Benchmark failed:', e.message);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
});
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router - Provider Test Framework
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive tests for the generic provider system.
|
|
6
|
+
* Tests work with whatever providers the user has configured.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
createA3MRouter,
|
|
11
|
+
getAvailableProviders,
|
|
12
|
+
registerProvider,
|
|
13
|
+
deregisterProvider,
|
|
14
|
+
DEFAULT_PROVIDERS,
|
|
15
|
+
providerConfig,
|
|
16
|
+
routeQuery,
|
|
17
|
+
routeBatch,
|
|
18
|
+
recommendForTask,
|
|
19
|
+
extractQueryFeatures,
|
|
20
|
+
MODEL_PROFILES,
|
|
21
|
+
countTokens,
|
|
22
|
+
estimateCost,
|
|
23
|
+
MemoryTree,
|
|
24
|
+
CostTracker,
|
|
25
|
+
ResponseCache,
|
|
26
|
+
ProviderRegistry,
|
|
27
|
+
compressText,
|
|
28
|
+
isonEncode,
|
|
29
|
+
isonDecode,
|
|
30
|
+
} = require('../dist/index.js');
|
|
31
|
+
|
|
32
|
+
// Test configuration
|
|
33
|
+
const TEST_CONFIG = {
|
|
34
|
+
verbose: process.argv.includes('--verbose') || process.argv.includes('-v'),
|
|
35
|
+
skipLive: process.argv.includes('--skip-live'),
|
|
36
|
+
timeout: 30000,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Test state
|
|
40
|
+
let passed = 0;
|
|
41
|
+
let failed = 0;
|
|
42
|
+
let skipped = 0;
|
|
43
|
+
|
|
44
|
+
// Test utilities
|
|
45
|
+
function log(message, level = 'info') {
|
|
46
|
+
if (level === 'error') console.error(message);
|
|
47
|
+
else if (TEST_CONFIG.verbose || level !== 'debug') console.log(message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function test(name, fn) {
|
|
51
|
+
try {
|
|
52
|
+
fn();
|
|
53
|
+
log(` ā
${name}`, 'success');
|
|
54
|
+
passed++;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
log(` ā ${name}: ${e.message}`, 'error');
|
|
57
|
+
failed++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function asyncTest(name, fn) {
|
|
62
|
+
try {
|
|
63
|
+
await fn();
|
|
64
|
+
log(` ā
${name}`, 'success');
|
|
65
|
+
passed++;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
log(` ā ${name}: ${e.message}`, 'error');
|
|
68
|
+
failed++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function skip(name, reason) {
|
|
73
|
+
log(` āļø ${name} (skipped: ${reason})`, 'warn');
|
|
74
|
+
skipped++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// TEST SUITE
|
|
79
|
+
// ============================================================
|
|
80
|
+
|
|
81
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
82
|
+
console.log('š§Ŗ A3M Router - Provider Test Framework');
|
|
83
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
84
|
+
|
|
85
|
+
// 1. Provider Configuration Tests
|
|
86
|
+
console.log('š¦ 1. Provider Configuration');
|
|
87
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
88
|
+
|
|
89
|
+
test('providerConfig module loads', () => {
|
|
90
|
+
if (!providerConfig) throw new Error('providerConfig not exported');
|
|
91
|
+
if (typeof providerConfig.loadConfig !== 'function') throw new Error('loadConfig not a function');
|
|
92
|
+
if (typeof providerConfig.getAvailableProviders !== 'function') throw new Error('getAvailableProviders not a function');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('DEFAULT_PROVIDERS has expected structure', () => {
|
|
96
|
+
if (!DEFAULT_PROVIDERS) throw new Error('DEFAULT_PROVIDERS not defined');
|
|
97
|
+
|
|
98
|
+
// Check at least some providers exist
|
|
99
|
+
const providerCount = Object.keys(DEFAULT_PROVIDERS).length;
|
|
100
|
+
if (providerCount < 5) throw new Error(`Expected at least 5 providers, got ${providerCount}`);
|
|
101
|
+
|
|
102
|
+
// Check provider structure
|
|
103
|
+
for (const [id, provider] of Object.entries(DEFAULT_PROVIDERS)) {
|
|
104
|
+
if (!provider.id) throw new Error(`${id}: missing id`);
|
|
105
|
+
if (!provider.name) throw new Error(`${id}: missing name`);
|
|
106
|
+
if (!provider.type) throw new Error(`${id}: missing type`);
|
|
107
|
+
if (!['api', 'cli', 'local'].includes(provider.type)) throw new Error(`${id}: invalid type ${provider.type}`);
|
|
108
|
+
if (typeof provider.priority !== 'number') throw new Error(`${id}: missing priority`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('getAvailableProviders returns configured providers', () => {
|
|
113
|
+
const available = getAvailableProviders();
|
|
114
|
+
if (!available) throw new Error('getAvailableProviders returned null');
|
|
115
|
+
|
|
116
|
+
// Should return object with providers that have API keys
|
|
117
|
+
for (const [id, provider] of Object.entries(available)) {
|
|
118
|
+
if (!provider.id) throw new Error(`${id}: missing id`);
|
|
119
|
+
if (!provider.name) throw new Error(`${id}: missing name`);
|
|
120
|
+
if (!provider.models) throw new Error(`${id}: missing models`);
|
|
121
|
+
if (!Array.isArray(provider.models)) throw new Error(`${id}: models not an array`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('Provider types are correct', () => {
|
|
126
|
+
const available = getAvailableProviders();
|
|
127
|
+
|
|
128
|
+
for (const [id, provider] of Object.entries(available)) {
|
|
129
|
+
if (!['api', 'cli', 'local'].includes(provider.type)) {
|
|
130
|
+
throw new Error(`${id}: invalid type ${provider.type}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// API providers should have baseUrl and apiKeyEnv
|
|
134
|
+
if (provider.type === 'api') {
|
|
135
|
+
if (!provider.baseUrl) throw new Error(`${id}: API provider missing baseUrl`);
|
|
136
|
+
if (!provider.apiKeyEnv) throw new Error(`${id}: API provider missing apiKeyEnv`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// CLI providers should have cliCommand
|
|
140
|
+
if (provider.type === 'cli') {
|
|
141
|
+
if (!provider.cliCommand) throw new Error(`${id}: CLI provider missing cliCommand`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 2. Routing Tests
|
|
147
|
+
console.log('\nš 2. Routing');
|
|
148
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
149
|
+
|
|
150
|
+
test('routeQuery returns valid result', () => {
|
|
151
|
+
const result = routeQuery('What is 2+2?');
|
|
152
|
+
if (!result) throw new Error('routeQuery returned null');
|
|
153
|
+
if (!result.primary_model) throw new Error('missing primary_model');
|
|
154
|
+
if (!Array.isArray(result.fallback_models)) throw new Error('fallback_models not an array');
|
|
155
|
+
if (typeof result.estimated_cost !== 'number') throw new Error('estimated_cost not a number');
|
|
156
|
+
if (typeof result.confidence !== 'number') throw new Error('confidence not a number');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('routeQuery selects appropriate provider for code', () => {
|
|
160
|
+
const result = routeQuery('Write a Python function to sort an array');
|
|
161
|
+
if (!result.primary_model) throw new Error('missing primary_model');
|
|
162
|
+
if (!result.reasoning) throw new Error('missing reasoning');
|
|
163
|
+
|
|
164
|
+
// Should detect code
|
|
165
|
+
const features = extractQueryFeatures('Write a Python function to sort an array');
|
|
166
|
+
if (!features.has_code) throw new Error('should detect code');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('routeQuery selects appropriate provider for math', () => {
|
|
170
|
+
const result = routeQuery('Calculate the integral of x^2');
|
|
171
|
+
if (!result.primary_model) throw new Error('missing primary_model');
|
|
172
|
+
|
|
173
|
+
const features = extractQueryFeatures('Calculate the integral of x^2');
|
|
174
|
+
if (!features.has_math) throw new Error('should detect math');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('routeQuery selects appropriate provider for translation', () => {
|
|
178
|
+
const result = routeQuery('Translate hello to French');
|
|
179
|
+
if (!result.primary_model) throw new Error('missing primary_model');
|
|
180
|
+
|
|
181
|
+
const features = extractQueryFeatures('Translate hello to French');
|
|
182
|
+
if (!features.is_translation) throw new Error('should detect translation');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('routeBatch returns array of results', () => {
|
|
186
|
+
const queries = ['Hello', 'What is 2+2?', 'Write Python code'];
|
|
187
|
+
const results = routeBatch(queries);
|
|
188
|
+
|
|
189
|
+
if (!Array.isArray(results)) throw new Error('routeBatch should return array');
|
|
190
|
+
if (results.length !== queries.length) throw new Error(`Expected ${queries.length} results, got ${results.length}`);
|
|
191
|
+
|
|
192
|
+
results.forEach((r, i) => {
|
|
193
|
+
if (!r.primary_model) throw new Error(`result ${i}: missing primary_model`);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('recommendForTask returns recommendation', () => {
|
|
198
|
+
const rec = recommendForTask('coding');
|
|
199
|
+
if (!rec) throw new Error('recommendForTask returned null');
|
|
200
|
+
if (!rec.primary) throw new Error('missing primary');
|
|
201
|
+
if (!Array.isArray(rec.fallbacks)) throw new Error('fallbacks not an array');
|
|
202
|
+
if (!rec.reason) throw new Error('missing reason');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// 3. Model Profile Tests
|
|
206
|
+
console.log('\nš 3. Model Profiles');
|
|
207
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
208
|
+
|
|
209
|
+
test('MODEL_PROFILES is populated', () => {
|
|
210
|
+
if (!MODEL_PROFILES) throw new Error('MODEL_PROFILES not defined');
|
|
211
|
+
if (Object.keys(MODEL_PROFILES).length === 0) throw new Error('MODEL_PROFILES is empty');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('Model profiles have required fields', () => {
|
|
215
|
+
for (const [name, profile] of Object.entries(MODEL_PROFILES)) {
|
|
216
|
+
if (!profile.name) throw new Error(`${name}: missing name`);
|
|
217
|
+
if (!profile.provider) throw new Error(`${name}: missing provider`);
|
|
218
|
+
if (typeof profile.cost_per_1k_input !== 'number') throw new Error(`${name}: missing cost_per_1k_input`);
|
|
219
|
+
if (typeof profile.cost_per_1k_output !== 'number') throw new Error(`${name}: missing cost_per_1k_output`);
|
|
220
|
+
if (typeof profile.quality_score !== 'number') throw new Error(`${name}: missing quality_score`);
|
|
221
|
+
if (!Array.isArray(profile.strengths)) throw new Error(`${name}: strengths not an array`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// 4. Token Utility Tests
|
|
226
|
+
console.log('\nš¢ 4. Token Utilities');
|
|
227
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
228
|
+
|
|
229
|
+
test('countTokens returns number', () => {
|
|
230
|
+
const tokens = countTokens('Hello world');
|
|
231
|
+
if (typeof tokens !== 'number') throw new Error('should return number');
|
|
232
|
+
if (tokens <= 0) throw new Error('should return positive number');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('countTokens counts correctly', () => {
|
|
236
|
+
const tokens = countTokens('Hello world');
|
|
237
|
+
if (tokens < 2) throw new Error('should count at least 2 tokens for 2 words');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('estimateCost returns number', () => {
|
|
241
|
+
const cost = estimateCost(100, 50, 'gpt-4o');
|
|
242
|
+
if (typeof cost !== 'number') throw new Error('should return number');
|
|
243
|
+
if (cost < 0) throw new Error('should return non-negative');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// 5. A3M Router Factory Tests
|
|
247
|
+
console.log('\nš 5. A3M Router Factory');
|
|
248
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
249
|
+
|
|
250
|
+
test('createA3MRouter returns router object', () => {
|
|
251
|
+
const router = createA3MRouter({});
|
|
252
|
+
if (!router) throw new Error('createA3MRouter returned null');
|
|
253
|
+
if (typeof router.route !== 'function') throw new Error('missing route function');
|
|
254
|
+
if (typeof router.routeBatch !== 'function') throw new Error('missing routeBatch function');
|
|
255
|
+
if (typeof router.recommend !== 'function') throw new Error('missing recommend function');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('createA3MRouter has memory', () => {
|
|
259
|
+
const router = createA3MRouter({});
|
|
260
|
+
if (!router.memory) throw new Error('missing memory');
|
|
261
|
+
if (typeof router.memory.add !== 'function') throw new Error('memory missing add');
|
|
262
|
+
if (typeof router.memory.search !== 'function') throw new Error('memory missing search');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('createA3MRouter has cache', () => {
|
|
266
|
+
const router = createA3MRouter({});
|
|
267
|
+
if (!router.cache) throw new Error('missing cache');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('createA3MRouter has costTracker', () => {
|
|
271
|
+
const router = createA3MRouter({});
|
|
272
|
+
if (!router.costTracker) throw new Error('missing costTracker');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('createA3MRouter has providers registry', () => {
|
|
276
|
+
const router = createA3MRouter({});
|
|
277
|
+
if (!router.providers) throw new Error('missing providers');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('createA3MRouter has compression', () => {
|
|
281
|
+
const router = createA3MRouter({});
|
|
282
|
+
if (!router.compression) throw new Error('missing compression');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('createA3MRouter has vault', () => {
|
|
286
|
+
const router = createA3MRouter({});
|
|
287
|
+
if (!router.vault) throw new Error('missing vault');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('createA3MRouter has autoFetch', () => {
|
|
291
|
+
const router = createA3MRouter({});
|
|
292
|
+
if (!router.autoFetch) throw new Error('missing autoFetch');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('createA3MRouter has oauth', () => {
|
|
296
|
+
const router = createA3MRouter({});
|
|
297
|
+
if (!router.oauth) throw new Error('missing oauth');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// 6. Memory Tree Tests
|
|
301
|
+
console.log('\nš§ 6. Memory Tree');
|
|
302
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
303
|
+
|
|
304
|
+
test('MemoryTree can add and search', () => {
|
|
305
|
+
const memory = new MemoryTree({ maxSize: 100 });
|
|
306
|
+
memory.add('Python is great for data science', { tags: ['python', 'data'] });
|
|
307
|
+
memory.add('JavaScript is great for web', { tags: ['js', 'web'] });
|
|
308
|
+
|
|
309
|
+
const results = memory.search('python data');
|
|
310
|
+
if (!Array.isArray(results)) throw new Error('search should return array');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('MemoryTree getStats returns stats', () => {
|
|
314
|
+
const memory = new MemoryTree({ maxSize: 100 });
|
|
315
|
+
memory.add('Test entry', { tags: ['test'] });
|
|
316
|
+
|
|
317
|
+
const stats = memory.getStats();
|
|
318
|
+
if (!stats) throw new Error('getStats returned null');
|
|
319
|
+
if (typeof stats.totalChunks !== 'number') throw new Error('missing totalChunks');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// 7. Provider Registry Tests
|
|
323
|
+
console.log('\nš 7. Provider Registry');
|
|
324
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
325
|
+
|
|
326
|
+
test('ProviderRegistry can be instantiated', () => {
|
|
327
|
+
const registry = new ProviderRegistry();
|
|
328
|
+
if (!registry) throw new Error('failed to create registry');
|
|
329
|
+
if (typeof registry.getReadyProviders !== 'function') throw new Error('missing getReadyProviders');
|
|
330
|
+
if (typeof registry.selectModel !== 'function') throw new Error('missing selectModel');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('ProviderRegistry getStatus returns status', () => {
|
|
334
|
+
const registry = new ProviderRegistry();
|
|
335
|
+
const status = registry.getStatus();
|
|
336
|
+
if (!status) throw new Error('getStatus returned null');
|
|
337
|
+
if (!Array.isArray(status.providers)) throw new Error('providers not an array');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 8. Dynamic Provider Registration Tests
|
|
341
|
+
console.log('\nš§ 8. Dynamic Provider Registration');
|
|
342
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
343
|
+
|
|
344
|
+
test('registerProvider adds new provider', () => {
|
|
345
|
+
const testProvider = {
|
|
346
|
+
name: 'TestProvider',
|
|
347
|
+
type: 'api',
|
|
348
|
+
baseUrl: 'https://test.example.com',
|
|
349
|
+
models: ['test-model'],
|
|
350
|
+
priority: 99,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
registerProvider('test-provider', testProvider);
|
|
354
|
+
|
|
355
|
+
// Check it was added
|
|
356
|
+
if (!providerConfig._providers['test-provider']) throw new Error('provider not added');
|
|
357
|
+
if (providerConfig._providers['test-provider'].name !== 'TestProvider') {
|
|
358
|
+
throw new Error('provider name mismatch');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Clean up
|
|
362
|
+
deregisterProvider('test-provider');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('deregisterProvider removes provider', () => {
|
|
366
|
+
// First add
|
|
367
|
+
registerProvider('temp-provider', { name: 'Temp', type: 'api', models: [] });
|
|
368
|
+
if (!providerConfig._providers['temp-provider']) throw new Error('provider not added');
|
|
369
|
+
|
|
370
|
+
// Then remove
|
|
371
|
+
deregisterProvider('temp-provider');
|
|
372
|
+
if (providerConfig._providers['temp-provider']) throw new Error('provider not removed');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// 9. Compression Tests
|
|
376
|
+
console.log('\nšļø 9. Compression');
|
|
377
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
378
|
+
|
|
379
|
+
test('compressText reduces size', () => {
|
|
380
|
+
const text = 'This is a test message that should be compressed to save tokens.';
|
|
381
|
+
const compressed = compressText(text, 0.5);
|
|
382
|
+
if (!compressed) throw new Error('compressText returned null');
|
|
383
|
+
if (compressed.length >= text.length) throw new Error('compression did not reduce size');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('isonEncode/Decode roundtrip', () => {
|
|
387
|
+
const text = 'function test() { return "hello world"; }';
|
|
388
|
+
const encoded = isonEncode(text);
|
|
389
|
+
if (!encoded) throw new Error('isonEncode returned null');
|
|
390
|
+
if (typeof encoded !== 'string') throw new Error('isonEncode should return string');
|
|
391
|
+
|
|
392
|
+
const decoded = isonDecode(encoded);
|
|
393
|
+
if (!decoded) throw new Error('isonDecode returned null');
|
|
394
|
+
if (typeof decoded !== 'string') throw new Error('isonDecode should return string');
|
|
395
|
+
// Decoded might not be identical due to compression, but should be similar
|
|
396
|
+
if (decoded.length < 5) throw new Error('decoded text too short');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// 10. End-to-End Pipeline Test
|
|
400
|
+
console.log('\nš 10. End-to-End Pipeline');
|
|
401
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
402
|
+
|
|
403
|
+
test('Full pipeline: route ā track ā remember', () => {
|
|
404
|
+
const router = createA3MRouter({ memory: { maxSize: 100 } });
|
|
405
|
+
|
|
406
|
+
// Route
|
|
407
|
+
const route = router.route('Test query');
|
|
408
|
+
if (!route.primary_model) throw new Error('routing failed');
|
|
409
|
+
|
|
410
|
+
// Track (via costTracker)
|
|
411
|
+
if (!router.costTracker) throw new Error('costTracker not available');
|
|
412
|
+
|
|
413
|
+
// Remember
|
|
414
|
+
router.memory.add('Test query result', { route: route.primary_model });
|
|
415
|
+
const search = router.memory.search('test');
|
|
416
|
+
if (!Array.isArray(search)) throw new Error('memory search failed');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ============================================================
|
|
420
|
+
// LIVE PROVIDER TESTS (if not skipped)
|
|
421
|
+
// ============================================================
|
|
422
|
+
|
|
423
|
+
if (!TEST_CONFIG.skipLive) {
|
|
424
|
+
console.log('\nš Live Provider Tests');
|
|
425
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
426
|
+
|
|
427
|
+
const available = getAvailableProviders();
|
|
428
|
+
|
|
429
|
+
if (Object.keys(available).length === 0) {
|
|
430
|
+
skip('No providers configured', 'No API keys found in environment');
|
|
431
|
+
} else {
|
|
432
|
+
for (const [id, provider] of Object.entries(available)) {
|
|
433
|
+
asyncTest(`Health check: ${provider.name}`, async () => {
|
|
434
|
+
const health = await providerConfig.healthCheck(id);
|
|
435
|
+
if (!health) throw new Error('healthCheck returned null');
|
|
436
|
+
|
|
437
|
+
// CLI providers may not have traditional health checks
|
|
438
|
+
if (provider.type === 'cli') {
|
|
439
|
+
log(` ${id}: CLI provider (type: ${health.type || 'unknown'})`, 'debug');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!health.healthy) {
|
|
444
|
+
throw new Error(`unhealthy: ${health.error || 'unknown error'}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
log(` ${id}: healthy (${health.latency}ms)`, 'debug');
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================================================
|
|
454
|
+
// SUMMARY
|
|
455
|
+
// ============================================================
|
|
456
|
+
|
|
457
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
458
|
+
console.log('š Test Summary');
|
|
459
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
460
|
+
console.log(` Total: ${passed + failed + skipped}`);
|
|
461
|
+
console.log(` Passed: ${passed} ā
`);
|
|
462
|
+
console.log(` Failed: ${failed}${failed > 0 ? ' ā' : ''}`);
|
|
463
|
+
console.log(` Skipped: ${skipped}${skipped > 0 ? ' āļø' : ''}`);
|
|
464
|
+
console.log('');
|
|
465
|
+
|
|
466
|
+
if (failed > 0) {
|
|
467
|
+
console.log('ā Some tests failed');
|
|
468
|
+
process.exit(1);
|
|
469
|
+
} else {
|
|
470
|
+
console.log('ā
All tests passed!');
|
|
471
|
+
process.exit(0);
|
|
472
|
+
}
|