alexa-ai 2.1.1 → 2.2.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.
- package/CHANGELOG.md +74 -0
- package/README.md +124 -31
- package/examples/text2img-standalone.js +159 -0
- package/index.js +2 -0
- package/package.json +2 -1
- package/src/AlexaAI.js +198 -50
- package/src/core/Config.js +23 -2
- package/src/core/DeepAIClient.js +119 -11
- package/src/services/WebAnswer.js +153 -22
- package/src/services/WebSearch.js +428 -0
- package/test/fakes.js +171 -0
- package/test/run-tests.js +1113 -0
- package/test/wrapper-methods.js +986 -0
package/test/fakes.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test doubles
|
|
5
|
+
* ------------
|
|
6
|
+
* A tiny in-memory stand-in for PostgreSQL and for the DeepAI HTTP API, so the
|
|
7
|
+
* full `AlexaAI.chat()` pipeline — identity resolution, memory injection,
|
|
8
|
+
* guards, formatting, persistence — can be exercised with no network and no
|
|
9
|
+
* database. The fake DB understands only the handful of statements the engine
|
|
10
|
+
* actually issues; anything else returns an empty result.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function createFakeDb() {
|
|
14
|
+
const state = { users: [], identities: [], convos: [], messages: [], memories: [] };
|
|
15
|
+
let seq = 1;
|
|
16
|
+
|
|
17
|
+
const one = (sql, p = []) => {
|
|
18
|
+
const q = String(sql).replace(/\s+/g, ' ').trim();
|
|
19
|
+
|
|
20
|
+
if (/JOIN wa_user_identities i ON i.user_id = u.id WHERE i.jid/.test(q)) {
|
|
21
|
+
const alias = state.identities.find((i) => i.jid === p[0]);
|
|
22
|
+
return alias ? state.users.find((u) => u.id === alias.user_id) || null : null;
|
|
23
|
+
}
|
|
24
|
+
if (/^SELECT u\.\* FROM wa_users u JOIN wa_user_identities i ON i.user_id = u.id WHERE i.phone/.test(q)) {
|
|
25
|
+
const alias = state.identities.find((i) => i.phone === p[0]);
|
|
26
|
+
return alias ? state.users.find((u) => u.id === alias.user_id) || null : null;
|
|
27
|
+
}
|
|
28
|
+
if (/^INSERT INTO wa_users/.test(q)) {
|
|
29
|
+
let user = state.users.find((u) => u.jid === p[0]);
|
|
30
|
+
if (!user) {
|
|
31
|
+
user = {
|
|
32
|
+
id: seq++,
|
|
33
|
+
jid: p[0],
|
|
34
|
+
jid_local: p[1],
|
|
35
|
+
jid_server: p[2],
|
|
36
|
+
jid_type: p[3],
|
|
37
|
+
phone: p[4],
|
|
38
|
+
push_name: p[5],
|
|
39
|
+
display_name: null,
|
|
40
|
+
is_blocked: false,
|
|
41
|
+
is_admin: false,
|
|
42
|
+
message_count: 0,
|
|
43
|
+
token_estimate: 0,
|
|
44
|
+
metadata: {},
|
|
45
|
+
first_seen_at: new Date(),
|
|
46
|
+
last_seen_at: new Date(),
|
|
47
|
+
};
|
|
48
|
+
state.users.push(user);
|
|
49
|
+
} else if (p[5]) {
|
|
50
|
+
user.push_name = user.push_name || p[5];
|
|
51
|
+
}
|
|
52
|
+
return user;
|
|
53
|
+
}
|
|
54
|
+
if (/^UPDATE wa_users SET last_seen_at/.test(q)) return state.users.find((u) => u.id === p[0]) || null;
|
|
55
|
+
if (/^SELECT \* FROM wa_users WHERE id/.test(q)) return state.users.find((u) => u.id === p[0]) || null;
|
|
56
|
+
if (/^SELECT \* FROM wa_user_identities WHERE jid/.test(q)) {
|
|
57
|
+
return state.identities.find((i) => i.jid === p[0]) || null;
|
|
58
|
+
}
|
|
59
|
+
if (/^SELECT jid FROM wa_user_identities/.test(q)) {
|
|
60
|
+
const rows = state.identities.filter((i) => i.user_id === p[0]);
|
|
61
|
+
const primary = rows.find((r) => r.is_primary) || rows[0];
|
|
62
|
+
return primary ? { jid: primary.jid } : null;
|
|
63
|
+
}
|
|
64
|
+
if (/^INSERT INTO wa_groups/.test(q)) {
|
|
65
|
+
return { id: seq++, jid: p[0], subject: p[1], is_enabled: true };
|
|
66
|
+
}
|
|
67
|
+
if (/^INSERT INTO wa_group_members/.test(q)) return { id: seq++ };
|
|
68
|
+
if (/^INSERT INTO wa_conversations/.test(q)) {
|
|
69
|
+
let convo = state.convos.find((c) => c.context_key === p[0]);
|
|
70
|
+
if (!convo) {
|
|
71
|
+
convo = { id: seq++, context_key: p[0], user_id: p[1], group_id: p[2], kind: p[3] };
|
|
72
|
+
state.convos.push(convo);
|
|
73
|
+
}
|
|
74
|
+
return convo;
|
|
75
|
+
}
|
|
76
|
+
if (/SELECT COALESCE\(\s*u.display_name/.test(q)) {
|
|
77
|
+
const user = state.users.find((u) => u.id === p[0]);
|
|
78
|
+
const named = state.memories.find((m) => m.user_id === p[0] && m.key === 'name');
|
|
79
|
+
return { name: user?.display_name || named?.value || user?.push_name || null };
|
|
80
|
+
}
|
|
81
|
+
if (/^INSERT INTO wa_messages/.test(q)) {
|
|
82
|
+
const row = { id: seq++, conversation_id: p[0], user_id: p[1], role: p[2], content: p[3] };
|
|
83
|
+
state.messages.push(row);
|
|
84
|
+
return row;
|
|
85
|
+
}
|
|
86
|
+
if (/^INSERT INTO wa_memories/.test(q)) {
|
|
87
|
+
const row = { user_id: p[0], key: p[1], value: p[2] };
|
|
88
|
+
state.memories = state.memories.filter((m) => !(m.user_id === row.user_id && m.key === row.key));
|
|
89
|
+
state.memories.push(row);
|
|
90
|
+
return row;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const many = (sql, p = []) => {
|
|
96
|
+
const q = String(sql).replace(/\s+/g, ' ').trim();
|
|
97
|
+
if (/FROM wa_messages/.test(q)) {
|
|
98
|
+
return state.messages
|
|
99
|
+
.filter((m) => m.conversation_id === p[0])
|
|
100
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
101
|
+
}
|
|
102
|
+
if (/FROM wa_memories/.test(q)) {
|
|
103
|
+
return state.memories.filter((m) => m.user_id === p[0]).map((m) => ({ key: m.key, value: m.value }));
|
|
104
|
+
}
|
|
105
|
+
if (/FROM wa_user_identities/.test(q)) {
|
|
106
|
+
return state.identities.filter((i) => i.user_id === p[0]);
|
|
107
|
+
}
|
|
108
|
+
return [];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const db = {
|
|
112
|
+
state,
|
|
113
|
+
connect: async () => {},
|
|
114
|
+
migrate: async () => {},
|
|
115
|
+
close: async () => {},
|
|
116
|
+
one: async (sql, p) => one(sql, p),
|
|
117
|
+
many: async (sql, p) => many(sql, p),
|
|
118
|
+
query: async (sql, p = []) => {
|
|
119
|
+
const q = String(sql).replace(/\s+/g, ' ').trim();
|
|
120
|
+
if (/^INSERT INTO wa_user_identities/.test(q)) {
|
|
121
|
+
if (!state.identities.find((i) => i.jid === p[1])) {
|
|
122
|
+
state.identities.push({
|
|
123
|
+
user_id: p[0],
|
|
124
|
+
jid: p[1],
|
|
125
|
+
jid_type: p[4],
|
|
126
|
+
phone: p[5],
|
|
127
|
+
is_primary: Boolean(p[6]),
|
|
128
|
+
first_seen_at: new Date(),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return { rows: [], rowCount: 1 };
|
|
132
|
+
}
|
|
133
|
+
const row = one(sql, p);
|
|
134
|
+
return { rows: row ? [row] : [], rowCount: row ? 1 : 0 };
|
|
135
|
+
},
|
|
136
|
+
transaction: async (fn) => fn({ query: async (sql, p) => db.query(sql, p) }),
|
|
137
|
+
};
|
|
138
|
+
return db;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Replace global.fetch with a queue-driven DeepAI stub.
|
|
143
|
+
* @param {string[]} replies bodies returned by successive chat calls
|
|
144
|
+
* @returns {{ restore: () => void, calls: object[], push: (body:string)=>void }}
|
|
145
|
+
*/
|
|
146
|
+
function installFakeDeepAI(replies = []) {
|
|
147
|
+
const queue = [...replies];
|
|
148
|
+
const calls = [];
|
|
149
|
+
const realFetch = global.fetch;
|
|
150
|
+
|
|
151
|
+
global.fetch = async (url, init = {}) => {
|
|
152
|
+
const fields = {};
|
|
153
|
+
if (init.body && typeof init.body.forEach === 'function') {
|
|
154
|
+
init.body.forEach((value, key) => {
|
|
155
|
+
fields[key] = typeof value === 'string' ? value : '[file]';
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
calls.push({ url: String(url), fields });
|
|
159
|
+
const body = queue.length ? queue.shift() : 'Hello!';
|
|
160
|
+
return {
|
|
161
|
+
status: 200,
|
|
162
|
+
headers: { get: () => 'text/plain' },
|
|
163
|
+
text: async () => body,
|
|
164
|
+
body: null,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return { calls, push: (body) => queue.push(body), restore: () => { global.fetch = realFetch; } };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { createFakeDb, installFakeDeepAI };
|