alexa-ai 2.1.1

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.
@@ -0,0 +1,531 @@
1
+ /**
2
+ * src/modules/Aii.js (or callai.js — whichever your bot requires)
3
+ * ---------------------------------------------------------------------------
4
+ * Alexa's AI layer, powered by the `alexa-ai` package (DeepAI + PostgreSQL)
5
+ * instead of the Hugging Face Gradio Space.
6
+ *
7
+ * DROP-IN COMPATIBLE: the exported function keeps the exact same signature as
8
+ * the old Gradio version, so no call site in the bot has to change:
9
+ *
10
+ * ai(message, userId, groupId, userName, callback)
11
+ *
12
+ * • `message` may be a string OR { text: "...", files: [...] }
13
+ * • `userId` is the sender jid ('78151912841263@lid', '947...@s.whatsapp.net')
14
+ * …or, better, everything you know: see IDENTITY below
15
+ * • `groupId` is the group jid ('120363413125431525@g.us') or "" for a DM
16
+ * • `userName` is the WhatsApp push name
17
+ * • `callback(err, reply)` is optional; the function also returns the reply
18
+ *
19
+ * THE PERSONA
20
+ * This module deliberately does NOT pass `systemPrompt`, `assistantName` or
21
+ * `creator`. The engine's DEFAULT system prompt is used — the full Alexa
22
+ * persona (identity rules, WhatsApp formatting, the 4 strict triggers, math
23
+ * rules, vision rules, @MEMORY tracking). Overriding it here would silently
24
+ * disable those guarantees, so don't, unless you really mean to.
25
+ *
26
+ * IDENTITY — read this once, it is the important bit
27
+ * WhatsApp calls the same human by two different addresses:
28
+ *
29
+ * DM -> 94771234567@s.whatsapp.net (phone jid)
30
+ * GROUP -> 78151912841263@lid (privacy / LID jid)
31
+ *
32
+ * If the bot only ever passes one of them, the engine sees two people and
33
+ * Alexa "forgets" the user the moment they speak in a group. Baileys hands
34
+ * you both on every group message, so pass both:
35
+ *
36
+ * const sender = msg.key.participant || msg.key.remoteJid;
37
+ * const senderAlt = msg.key.participantAlt || msg.key.participantPn;
38
+ *
39
+ * await ai(text, { id: sender, phone: senderAlt }, groupId, pushName);
40
+ * // or simply: await ai.fromMessage(msg, sock);
41
+ *
42
+ * Plain strings still work exactly as before — you just don't get the
43
+ * cross-chat recognition until you supply the second address.
44
+ *
45
+ * REQUIRED alexa-ai VERSION
46
+ * The extras below (generateImage, searchWeb, upscaleImage, …) exist from
47
+ * alexa-ai 2.0.0; the media/alias fixes from 2.1.0 and the long-form
48
+ * searchWeb() from 2.1.1. `getEngine()` checks
49
+ * this at startup and throws a clear message instead of the confusing
50
+ * "getEngine(...).generateImage is not a function" you get from an old copy
51
+ * in node_modules. If you see that error:
52
+ *
53
+ * npm install github:AlexaInc/deepai # or your fork / tarball
54
+ * node -e "console.log(require('alexa-ai').version)" # must print >= 2.1.1
55
+ *
56
+ * REQUIRED .env
57
+ * DEEPAI_API_KEY=tryit-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (your DeepAI key)
58
+ * POSTGRES_URL=postgres://user:pass@host:5432/dbname
59
+ *
60
+ * OPTIONAL .env
61
+ * DEEPAI_API_KEYS=key1,key2 extra keys, rotated when one hits its quota
62
+ * CHAT_MODEL=standard DeepAI model ('standard' on free keys)
63
+ * OCR_API_KEY=... your own ocr.space key (reads text in images)
64
+ * AI_DEBUG=1 verbose engine logging
65
+ * ---------------------------------------------------------------------------
66
+ */
67
+
68
+ const fs = require("fs");
69
+ const path = require("path");
70
+
71
+ const config = require("../config");
72
+ const AlexaAI = require("alexa-ai");
73
+
74
+ /** Oldest alexa-ai build this wrapper is known to work with. */
75
+ const MIN_ENGINE_VERSION = "2.1.1";
76
+
77
+ /** Every engine method this file calls. Checked once at startup. */
78
+ const REQUIRED_METHODS = [
79
+ "chat", "init", "close", "health", "deepaiHealth", "stats",
80
+ "forgetAll", "getMemories", "remember", "forget", "clearHistory",
81
+ "blockUser", "unblockUser", "isBlocked", "setGroupEnabled", "isGroupEnabled", "getProfile",
82
+ "linkIdentity", "getAliases", "whoIs",
83
+ "generateImage", "editImage", "upscaleImage", "colorizeImage", "detectNsfw",
84
+ "describeImage", "summarizeText", "searchWeb",
85
+ ];
86
+
87
+ /** Singleton engine — one PostgreSQL pool for the whole bot. */
88
+ let engine = null;
89
+
90
+ /** Throw a clear error when node_modules holds an older alexa-ai. */
91
+ function assertEngineVersion(instance) {
92
+ const installed = String(AlexaAI.version || instance.version || "0.0.0");
93
+ const missing = REQUIRED_METHODS.filter((m) => typeof instance[m] !== "function");
94
+ if (missing.length || compareVersions(installed, MIN_ENGINE_VERSION) < 0) {
95
+ throw new Error(
96
+ `alexa-ai ${installed} is too old for src/modules/Aii.js (needs >= ${MIN_ENGINE_VERSION}). ` +
97
+ (missing.length ? `Missing methods: ${missing.join(", ")}. ` : "") +
98
+ `Reinstall it: npm install github:AlexaInc/deepai (then delete node_modules/alexa-ai if npm kept the old copy).`,
99
+ );
100
+ }
101
+ }
102
+
103
+ function compareVersions(a, b) {
104
+ const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
105
+ const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
106
+ for (let i = 0; i < 3; i++) {
107
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
108
+ }
109
+ return 0;
110
+ }
111
+
112
+ /** Create (once) and return the AI engine. */
113
+ function getEngine() {
114
+ if (engine) return engine;
115
+
116
+ const key = config.DEEPAI_API_KEY || process.env.DEEPAI_API_KEY;
117
+ const postgresUrl =
118
+ config.POSTGRES_URL || process.env.POSTGRES_URL || process.env.DATABASE_URL;
119
+
120
+ if (!key) throw new Error("DEEPAI_API_KEY is missing from .env");
121
+ if (!postgresUrl) throw new Error("POSTGRES_URL is missing from .env");
122
+
123
+ // Extra keys are optional: "key1,key2" in .env, or an array in config.
124
+ const extraKeys = []
125
+ .concat(config.DEEPAI_API_KEYS || [])
126
+ .concat(String(process.env.DEEPAI_API_KEYS || "").split(","))
127
+ .map((k) => String(k || "").trim())
128
+ .filter(Boolean);
129
+
130
+ const instance = new AlexaAI({
131
+ key,
132
+ keys: extraKeys, // rotated automatically on "try it exceeded"
133
+ postgresUrl,
134
+
135
+ // --- model -------------------------------------------------------------
136
+ // 'standard' is the safe free-tier default; anything else is tried first
137
+ // and falls back automatically if DeepAI refuses it.
138
+ model: config.CHAT_MODEL || process.env.CHAT_MODEL || "standard",
139
+ fallbackModels: ["gpt-4o-mini", "standard"],
140
+ visionModel: "gpt-4o-mini",
141
+
142
+ // --- persona -----------------------------------------------------------
143
+ // NOTHING here on purpose: the engine's default Alexa system prompt is
144
+ // used, together with the identity lock and the memory guard.
145
+
146
+ // --- conversation tuning ------------------------------------------------
147
+ historyLimit: 14, // past messages replayed to the model
148
+ maxMemories: 25, // facts injected per request
149
+ sharedGroupThread: false, // false = each member has their own thread
150
+
151
+ // --- identity -----------------------------------------------------------
152
+ linkIdentities: true, // @lid <-> phone jid are the same human
153
+ mergeIdentities: true, // fold duplicate rows together when proven
154
+
155
+ // --- images ---------------------------------------------------------------
156
+ ocr: true, // read text inside screenshots on free keys
157
+ ocrApiKey: config.OCR_API_KEY || process.env.OCR_API_KEY, // optional
158
+
159
+ // --- infrastructure -------------------------------------------------------
160
+ timeout: 60000,
161
+ maxRetries: 2,
162
+ autoMigrate: true, // create tables on first run
163
+ debug: Boolean(config.AI_DEBUG || process.env.AI_DEBUG),
164
+ });
165
+
166
+ assertEngineVersion(instance);
167
+ engine = instance;
168
+
169
+ console.log(`✅ Alexa AI engine ready (alexa-ai ${AlexaAI.version}, DeepAI + PostgreSQL)`);
170
+ return engine;
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Input normalisation
175
+ // ---------------------------------------------------------------------------
176
+
177
+ const MIME_BY_EXT = {
178
+ ".png": "image/png",
179
+ ".jpg": "image/jpeg",
180
+ ".jpeg": "image/jpeg",
181
+ ".webp": "image/webp",
182
+ ".gif": "image/gif",
183
+ ".bmp": "image/bmp",
184
+ ".pdf": "application/pdf",
185
+ ".txt": "text/plain",
186
+ ".md": "text/markdown",
187
+ ".csv": "text/csv",
188
+ ".json": "application/json",
189
+ ".log": "text/plain",
190
+ ".doc": "application/msword",
191
+ ".docx":
192
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
193
+ };
194
+
195
+ function guessMime(filePath) {
196
+ return (
197
+ MIME_BY_EXT[path.extname(String(filePath)).toLowerCase()] || "image/jpeg"
198
+ );
199
+ }
200
+
201
+ function isReadableFile(p) {
202
+ try {
203
+ return typeof p === "string" && p.length < 4096 && fs.existsSync(p) && fs.statSync(p).isFile();
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Turn anything the bot might hand us into something the engine accepts.
211
+ *
212
+ * The engine itself (>= 2.1.0) already understands Buffer · Uint8Array ·
213
+ * data URI · raw base64 · http(s) URL · { buffer } · { url } · { base64 } ·
214
+ * { data }. The only thing it cannot do is read the bot's DISK, so local
215
+ * paths and `{ path }` objects are loaded here; everything else is passed
216
+ * through untouched.
217
+ *
218
+ * @returns {object|Buffer|string|null}
219
+ */
220
+ function toMedia(file) {
221
+ if (!file) return null;
222
+
223
+ if (typeof file === "string" && !/^data:|^https?:\/\//i.test(file) && isReadableFile(file)) {
224
+ return {
225
+ buffer: fs.readFileSync(file),
226
+ mimetype: guessMime(file),
227
+ filename: path.basename(file),
228
+ };
229
+ }
230
+
231
+ if (file && typeof file === "object" && !Buffer.isBuffer(file) && !(file instanceof Uint8Array)) {
232
+ if (!file.buffer && !file.url && !file.base64 && !file.data && file.path && isReadableFile(file.path)) {
233
+ return {
234
+ buffer: fs.readFileSync(file.path),
235
+ mimetype: file.mimetype || guessMime(file.path),
236
+ filename: file.filename || path.basename(file.path),
237
+ };
238
+ }
239
+ }
240
+
241
+ // Buffers, base64, data URIs, URLs and { buffer | url | base64 } objects:
242
+ // the engine normalises these itself.
243
+ return file;
244
+ }
245
+
246
+ /**
247
+ * Turn whatever the bot knows about the sender into the address list the
248
+ * engine needs. See the IDENTITY note at the top of the file.
249
+ *
250
+ * Accepts:
251
+ * 'x@lid' (classic — still works)
252
+ * ['x@lid', '947...@s.whatsapp.net']
253
+ * { id, lid, phone, aliases: [] }
254
+ */
255
+ function toIdentity(userId) {
256
+ if (Array.isArray(userId)) {
257
+ const list = userId.map(String).filter(Boolean);
258
+ return { userId: list[0] || "default_user", aliases: list.slice(1) };
259
+ }
260
+ if (userId && typeof userId === "object") {
261
+ const primary = userId.id || userId.jid || userId.lid || userId.phone;
262
+ return {
263
+ userId: String(primary || "default_user"),
264
+ userLid: userId.lid ? String(userId.lid) : undefined,
265
+ userPhone: userId.phone ? String(userId.phone) : undefined,
266
+ aliases: (userId.aliases || []).map(String).filter(Boolean),
267
+ };
268
+ }
269
+ return { userId: String(userId || "default_user") };
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Main entry point
274
+ // ---------------------------------------------------------------------------
275
+
276
+ /**
277
+ * Main AI function — same signature as the old Gradio implementation.
278
+ *
279
+ * @param {string|{text:string, files:Array}} message
280
+ * @param {string|string[]|{id:string,lid?:string,phone?:string}} userId
281
+ * @param {string} [groupId] e.g. '120363413125431525@g.us' ("" for DM)
282
+ * @param {string} [userName]
283
+ * @param {function} [callback] (err, reply)
284
+ * @param {object} [options] extra per-call options: { groupName, messageId,
285
+ * isAdmin, model, webAccess, thinking, onToken, signal, full }
286
+ * @returns {Promise<string>} the reply text ('' on failure)
287
+ */
288
+ async function ai(
289
+ message,
290
+ userId,
291
+ groupId = "",
292
+ userName = "User",
293
+ callback,
294
+ options = {},
295
+ ) {
296
+ try {
297
+ const client = getEngine();
298
+
299
+ // --- normalise the message shape (string OR { text, files }) -----------
300
+ let text = "";
301
+ let media = null;
302
+
303
+ if (typeof message === "string") {
304
+ text = message;
305
+ } else if (typeof message === "object" && message !== null) {
306
+ text = message.text || message.body || message.caption || "";
307
+
308
+ // `files` may hold a Buffer, a URL, a local path, a raw base64 string, a
309
+ // data URI, or an object. `image` / `base64` / `file` are also accepted.
310
+ const file =
311
+ (Array.isArray(message.files) ? message.files.find(Boolean) : null) ||
312
+ message.image ||
313
+ message.file ||
314
+ message.base64 ||
315
+ null;
316
+
317
+ media = toMedia(file);
318
+ }
319
+
320
+ if (!text && !media) {
321
+ if (typeof callback === "function") callback(null, "");
322
+ return "";
323
+ }
324
+
325
+ // --- ask Alexa ----------------------------------------------------------
326
+ const result = await client.chat({
327
+ ...toIdentity(userId),
328
+ message: text,
329
+ groupId: groupId ? String(groupId) : null,
330
+ groupName: options.groupName || null,
331
+ userName: String(userName || "User"),
332
+ image: media,
333
+ messageId: options.messageId || null,
334
+ isAdmin: Boolean(options.isAdmin),
335
+ model: options.model,
336
+ webAccess: options.webAccess,
337
+ thinking: options.thinking,
338
+ onToken: options.onToken,
339
+ signal: options.signal,
340
+ });
341
+
342
+ // Blocked users / disabled groups come back as an empty reply on purpose:
343
+ // the bot should simply stay silent.
344
+ const reply = result.text || "";
345
+
346
+ if (typeof callback === "function") callback(null, reply);
347
+ // `full: true` gives you chunks, generated image urls, memories, timings…
348
+ return options.full ? result : reply;
349
+ } catch (err) {
350
+ console.error("❌ Error in Alexa AI call:", err.message);
351
+ if (typeof callback === "function") callback(err.message, null);
352
+ return options.full ? { text: "", error: err.message, chunks: [] } : "";
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Convenience wrapper for Baileys: extracts the sender, the LID/phone pair,
358
+ * the group, the push name and any attached image from a raw message object.
359
+ *
360
+ * const reply = await ai.fromMessage(msg, sock);
361
+ *
362
+ * @param {object} msg a Baileys `messages.upsert` message
363
+ * @param {object} [sock] the Baileys socket (used to download media, optional)
364
+ * @param {object} [options] forwarded to ai()
365
+ */
366
+ ai.fromMessage = async (msg, sock = null, options = {}) => {
367
+ const info = msg?.message || {};
368
+ const remoteJid = msg?.key?.remoteJid || "";
369
+ const isGroup = remoteJid.endsWith("@g.us");
370
+
371
+ const sender = isGroup ? msg?.key?.participant || remoteJid : remoteJid;
372
+ const senderAlt =
373
+ msg?.key?.participantAlt ||
374
+ msg?.key?.participantPn ||
375
+ msg?.key?.senderPn ||
376
+ null;
377
+
378
+ const text =
379
+ info.conversation ||
380
+ info.extendedTextMessage?.text ||
381
+ info.imageMessage?.caption ||
382
+ info.videoMessage?.caption ||
383
+ info.documentMessage?.caption ||
384
+ "";
385
+
386
+ // Download an attached image/document when the socket is available.
387
+ let files = [];
388
+ const mediaNode = info.imageMessage || info.documentMessage || null;
389
+ if (mediaNode && sock?.downloadMediaMessage) {
390
+ try {
391
+ const buffer = await sock.downloadMediaMessage(msg);
392
+ if (buffer) {
393
+ files = [
394
+ {
395
+ buffer,
396
+ mimetype: mediaNode.mimetype || "image/jpeg",
397
+ filename: mediaNode.fileName || "image.jpg",
398
+ },
399
+ ];
400
+ }
401
+ } catch (err) {
402
+ console.warn("⚠️ Could not download media:", err.message);
403
+ }
404
+ }
405
+
406
+ return ai(
407
+ { text, files },
408
+ { id: sender, phone: senderAlt || undefined },
409
+ isGroup ? remoteJid : "",
410
+ msg?.pushName || "User",
411
+ undefined,
412
+ { messageId: msg?.key?.id || null, ...options },
413
+ );
414
+ };
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Extras — handy for bot commands. Ignore them if you don't need them.
418
+ // None of these throw: every one returns `{ ok, ... }` (or a row / value),
419
+ // so a command handler can do `if (!r.ok) return reply(r.message)`.
420
+ // ---------------------------------------------------------------------------
421
+
422
+ /** `.forget` command — wipe everything Alexa remembers about a user. */
423
+ ai.forgetUser = async (userId) => getEngine().forgetAll(userId);
424
+
425
+ /** `.memory` command — show what Alexa remembers. */
426
+ ai.getMemories = async (userId) => getEngine().getMemories(userId);
427
+
428
+ /** Teach Alexa one fact by hand. */
429
+ ai.remember = async (userId, key, value) =>
430
+ getEngine().remember(userId, key, value);
431
+
432
+ /** Forget one fact. */
433
+ ai.forget = async (userId, key) => getEngine().forget(userId, key);
434
+
435
+ /** `.reset` command — clear the chat transcript (memories are kept). */
436
+ ai.clearHistory = async (userId, groupId = null) =>
437
+ getEngine().clearHistory(userId, groupId || null);
438
+
439
+ /** Block / unblock a user from using the AI (any of their addresses works). */
440
+ ai.blockUser = async (userId) => getEngine().blockUser(userId);
441
+ ai.unblockUser = async (userId) => getEngine().unblockUser(userId);
442
+ ai.isBlocked = async (userId) => getEngine().isBlocked(userId);
443
+
444
+ /** Turn Alexa on/off inside one group (works before she has spoken there). */
445
+ ai.setGroupEnabled = async (groupId, enabled) =>
446
+ getEngine().setGroupEnabled(groupId, enabled);
447
+ ai.isGroupEnabled = async (groupId) => getEngine().isGroupEnabled(groupId);
448
+
449
+ /** Full profile: user row + memories + threads. */
450
+ ai.getProfile = async (userId) => getEngine().getProfile(userId);
451
+
452
+ // --- identity ---------------------------------------------------------------
453
+
454
+ /**
455
+ * Tell Alexa two WhatsApp addresses are the same human. Call it whenever
456
+ * Baileys reveals a mapping:
457
+ *
458
+ * const pn = await sock.signalRepository.lidMapping.getPNForLID(lid);
459
+ * if (pn) await ai.linkIdentity(lid, pn);
460
+ */
461
+ ai.linkIdentity = async (jidA, jidB) => getEngine().linkIdentity(jidA, jidB);
462
+
463
+ ai.getAliases = async (userId) => getEngine().getAliases(userId);
464
+
465
+ ai.whoIs = async (userId) => getEngine().whoIs(userId);
466
+
467
+ // --- media & extras ----------------------------------------------------------
468
+
469
+ /**
470
+ * `.image <prompt>` — text-to-image. `{ ok, url, via, error, message }`.
471
+ * On free keys `/api/text2img` is refused ("Out of API credits"); the engine
472
+ * then drives DeepAI's in-chat image tool automatically, so this works on
473
+ * the same `tryit-…` key as chat.
474
+ *
475
+ * const r = await ai.generateImage(prompt);
476
+ * if (r.ok) await sock.sendMessage(jid, { image: { url: r.url }, caption: prompt });
477
+ * else await sock.sendMessage(jid, { text: `Couldn't draw that: ${r.message}` });
478
+ */
479
+ ai.generateImage = async (prompt, opts) =>
480
+ getEngine().generateImage(prompt, opts);
481
+
482
+ ai.editImage = async (file, prompt) =>
483
+ getEngine().editImage(toMedia(file), prompt);
484
+
485
+ ai.upscaleImage = async (file) => getEngine().upscaleImage(toMedia(file));
486
+
487
+ ai.colorizeImage = async (file) => getEngine().colorizeImage(toMedia(file));
488
+
489
+ ai.detectNsfw = async (file) => getEngine().detectNsfw(toMedia(file));
490
+
491
+ /** `{ ok, text, description, source }` — `text` is ready to send as-is. */
492
+ ai.describeImage = async (file, caption = "") =>
493
+ getEngine().describeImage(toMedia(file), caption);
494
+
495
+ ai.summarizeText = async (text) => getEngine().summarizeText(text);
496
+
497
+ /**
498
+ * `.search <query>` — one-off web research, no memory writes.
499
+ * `{ ok, text, answer, sources: [{ title, url }], error, message }`
500
+ *
501
+ * `text` is the full WhatsApp message: a long, sectioned answer followed by
502
+ * one *Sources:* block. A short first reply is retried once automatically
503
+ * (`attempts` tells you). Pass `{ detail: "short" }` for a 2–4 sentence
504
+ * reply, `{ includeSources: false }` to keep the block out of `text`, or
505
+ * `{ language: "Sinhala" }` to answer in another language. Long answers can
506
+ * be split with `AlexaAI.ResponseFormatter.chunk(text)` if they exceed a
507
+ * single message.
508
+ */
509
+ ai.searchWeb = async (query, opts) => getEngine().searchWeb(query, opts);
510
+
511
+ // --- ops ----------------------------------------------------------------------
512
+
513
+ ai.stats = async () => getEngine().stats();
514
+
515
+ ai.health = async () => getEngine().health();
516
+
517
+ ai.deepaiHealth = async () => getEngine().deepaiHealth();
518
+
519
+ ai.engine = () => getEngine();
520
+
521
+ ai.version = () => AlexaAI.version;
522
+
523
+ ai.init = async () => getEngine().init();
524
+ ai.close = async () => {
525
+ if (engine) {
526
+ await engine.close();
527
+ engine = null;
528
+ }
529
+ };
530
+
531
+ module.exports = ai;
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Live demo of the AI engine.
5
+ *
6
+ * POSTGRES_URL=postgres://user:pass@host:5432/db \
7
+ * DEEPAI_KEY=tryit-xxxxx \
8
+ * node examples/demo.js
9
+ *
10
+ * Walks through the core scenario the engine is built for:
11
+ * 1. user introduces themselves in a DM
12
+ * 2. the same user is recognised in group A
13
+ * 3. and in group B
14
+ * 4. a different user in the same group stays isolated
15
+ * 5. the four strict trigger commands return byte-exact output
16
+ */
17
+
18
+ const AlexaAI = require('../index');
19
+
20
+ const KEY = process.env.DEEPAI_KEY || process.env.DEEPAI_API_KEY;
21
+ const PG = process.env.POSTGRES_URL;
22
+
23
+ if (!KEY || !PG) {
24
+ console.error(
25
+ 'Set DEEPAI_KEY and POSTGRES_URL first, e.g.\n' +
26
+ ' DEEPAI_KEY=tryit-... POSTGRES_URL=postgres://postgres:pass@localhost:5432/alexa node examples/demo.js'
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ const NIMAL = '78151912841263@lid';
32
+ const KASUN = '94770000000@s.whatsapp.net';
33
+ const GROUP_A = '120363413125431525@g.us';
34
+ const GROUP_B = '120363999888777666@g.us';
35
+
36
+ const line = (c = '─') => console.log(c.repeat(70));
37
+
38
+ async function main() {
39
+ const ai = new AlexaAI({ key: KEY, postgresUrl: PG });
40
+ await ai.init();
41
+
42
+ console.log('\n🤖 Alexa AI — live demo');
43
+ line('═');
44
+ console.log('config:', ai.config.toJSON());
45
+
46
+ // Fresh start for a repeatable demo.
47
+ if (process.env.RESET !== 'false') {
48
+ await ai.db.query('TRUNCATE wa_users, wa_groups RESTART IDENTITY CASCADE');
49
+ }
50
+
51
+ const say = async (label, params) => {
52
+ const where = params.groupId ? `GROUP ${params.groupName}` : 'DM';
53
+ const result = await ai.chat(params);
54
+ line();
55
+ console.log(`${label} [${where}]`);
56
+ console.log(` 👤 ${params.userName}: ${params.message}`);
57
+ console.log(` 🤖 Alexa: ${result.text}`);
58
+ if (result.trigger) console.log(` ⚡ trigger: ${result.trigger} (exact output)`);
59
+ if (Object.keys(result.memories).length) {
60
+ console.log(` 🧠 learned: ${JSON.stringify(result.memories)}`);
61
+ }
62
+ console.log(` ⏱ ${result.latencyMs}ms thread: ${result.contextKey}`);
63
+ return result;
64
+ };
65
+
66
+ // 1 — introduction in a DM
67
+ await say('1️⃣ Introduction', {
68
+ message: "Hi! I'm Nimal and I love playing cricket. I live in Galle.",
69
+ userId: NIMAL,
70
+ userName: 'Nimal',
71
+ });
72
+ console.log(`\n 📇 stored memories: ${JSON.stringify(await ai.getMemories(NIMAL))}`);
73
+
74
+ // 2 — recognised in group A
75
+ await say('2️⃣ Same person, group A', {
76
+ message: 'Do you remember my name and where I live?',
77
+ userId: NIMAL,
78
+ groupId: GROUP_A,
79
+ groupName: 'Cricket Fans',
80
+ userName: 'Nimal',
81
+ });
82
+
83
+ // 3 — recognised in a different group
84
+ await say('3️⃣ Same person, group B', {
85
+ message: 'What is my hobby?',
86
+ userId: NIMAL,
87
+ groupId: GROUP_B,
88
+ groupName: 'Office Chat',
89
+ userName: 'Nimal',
90
+ });
91
+
92
+ // 4 — a different user is isolated
93
+ await say('4️⃣ Different user, same group', {
94
+ message: 'Do you know my name?',
95
+ userId: KASUN,
96
+ groupId: GROUP_A,
97
+ groupName: 'Cricket Fans',
98
+ userName: 'Kasun',
99
+ });
100
+ console.log(`\n 📇 Kasun memories: ${JSON.stringify(await ai.getMemories(KASUN))} ← correctly empty`);
101
+
102
+ // 5 — strict trigger commands
103
+ line('═');
104
+ console.log('5️⃣ Strict trigger commands (must be byte-exact)\n');
105
+ for (const msg of [
106
+ 'What is the weather in Colombo today?',
107
+ 'Is it raining in Kandy right now?',
108
+ 'show menu',
109
+ 'ping',
110
+ 'send me the docs',
111
+ ]) {
112
+ const r = await ai.chat({ message: msg, userId: NIMAL, userName: 'Nimal' });
113
+ console.log(` "${msg}"\n -> ${JSON.stringify(r.text)} ${r.trigger ? '✅' : '❌ (went to AI)'}`);
114
+ }
115
+
116
+ // 6 — formatting + math
117
+ line('═');
118
+ console.log('6️⃣ Math + WhatsApp formatting\n');
119
+ const math = await ai.chat({
120
+ message: 'Calculate the area of a circle with radius 7',
121
+ userId: NIMAL,
122
+ userName: 'Nimal',
123
+ });
124
+ console.log(` 🤖 ${math.text}`);
125
+ console.log(` contains forbidden "**": ${math.text.includes('**') ? '❌ yes' : '✅ no'}`);
126
+
127
+ // 7 — stats
128
+ line('═');
129
+ console.log('7️⃣ Engine stats\n');
130
+ console.log(' ', await ai.stats());
131
+
132
+ const profile = await ai.getProfile(NIMAL);
133
+ console.log('\n Nimal threads:');
134
+ profile.conversations.forEach((c) => console.log(` • ${c.context_key} (${c.message_count} msgs)`));
135
+ console.log('\n Nimal memories:');
136
+ profile.memories.forEach((m) => console.log(` • ${m.key} = ${m.value} [learned in ${m.learned_in}]`));
137
+
138
+ line('═');
139
+ console.log('✅ demo complete\n');
140
+ await ai.close();
141
+ }
142
+
143
+ main().catch((err) => {
144
+ console.error('\n❌ demo failed:', err.message);
145
+ if (process.env.DEBUG) console.error(err);
146
+ process.exit(1);
147
+ });