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.
- package/CHANGELOG.md +136 -0
- package/LICENSE +15 -0
- package/README.md +862 -0
- package/examples/bot-ai.js +531 -0
- package/examples/demo.js +147 -0
- package/index.js +75 -0
- package/package.json +50 -0
- package/src/AlexaAI.js +1099 -0
- package/src/core/Config.js +249 -0
- package/src/core/DeepAIClient.js +789 -0
- package/src/core/Endpoints.js +74 -0
- package/src/core/Persona.js +102 -0
- package/src/core/StreamParser.js +157 -0
- package/src/core/SystemPrompt.js +7 -0
- package/src/core/errors.js +51 -0
- package/src/db/Database.js +161 -0
- package/src/db/schema.sql +214 -0
- package/src/repositories/ConversationRepository.js +206 -0
- package/src/repositories/IdentityRepository.js +244 -0
- package/src/repositories/MemoryRepository.js +215 -0
- package/src/repositories/UserRepository.js +275 -0
- package/src/services/AmnesiaGuard.js +176 -0
- package/src/services/FactMiner.js +151 -0
- package/src/services/IdentityGuard.js +203 -0
- package/src/services/IdentityResolver.js +179 -0
- package/src/services/ImageDescriber.js +335 -0
- package/src/services/MathDetector.js +64 -0
- package/src/services/MemoryExtractor.js +142 -0
- package/src/services/PromptBuilder.js +216 -0
- package/src/services/ResponseFormatter.js +121 -0
- package/src/services/TriggerDetector.js +182 -0
- package/src/services/WebAnswer.js +573 -0
- package/src/utils/JidParser.js +148 -0
- package/src/utils/Media.js +235 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Persona = require('./Persona');
|
|
4
|
+
const { ENDPOINTS } = require('./Endpoints');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Config
|
|
8
|
+
* ------
|
|
9
|
+
* Central, validated configuration object. Every subsystem receives an
|
|
10
|
+
* instance of this class instead of reaching into `process.env`, which keeps
|
|
11
|
+
* the engine testable and lets the host bot pass values inline:
|
|
12
|
+
*
|
|
13
|
+
* new AlexaAI({ key: 'deepaikey', postgresUrl: 'postgres://...' })
|
|
14
|
+
*/
|
|
15
|
+
class Config {
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* @param {string} options.key DeepAI api-key (tryit-... or account key)
|
|
19
|
+
* @param {string[]} [options.keys] Extra keys to rotate through on quota errors
|
|
20
|
+
* @param {string} options.postgresUrl PostgreSQL connection string
|
|
21
|
+
* @param {string} [options.model] DeepAI model id
|
|
22
|
+
* @param {string[]} [options.fallbackModels] Tried in order when the main model is refused
|
|
23
|
+
* @param {string} [options.visionModel] Model used when images are attached
|
|
24
|
+
* @param {string[]} [options.visionModels] Vision fallback chain
|
|
25
|
+
* @param {string} [options.imageModel] Model used by generateImage()
|
|
26
|
+
* @param {string} [options.assistantName] Persona name (default 'Alexa')
|
|
27
|
+
* @param {string} [options.creator] Persona creator (default 'Hansaka')
|
|
28
|
+
* @param {string} [options.systemPrompt] Override the whole persona text
|
|
29
|
+
* @param {boolean} [options.systemRole] Also send a role:'system' turn (default true)
|
|
30
|
+
* @param {object} [options.endpoints] Override any DeepAI route
|
|
31
|
+
* @param {number} [options.historyLimit] Messages replayed to the model
|
|
32
|
+
* @param {number} [options.maxMemories] Memory rows injected per request
|
|
33
|
+
* @param {number} [options.timeout] Per-request timeout (ms)
|
|
34
|
+
* @param {number} [options.maxRetries] Network retry attempts
|
|
35
|
+
* @param {boolean} [options.sharedGroupThread] One shared thread per group
|
|
36
|
+
* @param {boolean} [options.autoMigrate] Create tables on first connect
|
|
37
|
+
* @param {boolean} [options.debug] Verbose logging
|
|
38
|
+
* @param {object} [options.pool] Extra node-postgres pool options
|
|
39
|
+
* @param {boolean|object} [options.ssl] SSL config passed to pg
|
|
40
|
+
*/
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
const opts = options || {};
|
|
43
|
+
|
|
44
|
+
// ---- Accept several aliases so the host bot can stay terse ----------
|
|
45
|
+
const key = opts.key || opts.apiKey || opts.deepaiKey || process.env.DEEPAI_API_KEY;
|
|
46
|
+
const postgresUrl =
|
|
47
|
+
opts.postgresUrl ||
|
|
48
|
+
opts.postgresURL ||
|
|
49
|
+
opts.postgueurl ||
|
|
50
|
+
opts.postgres ||
|
|
51
|
+
opts.databaseUrl ||
|
|
52
|
+
opts.connectionString ||
|
|
53
|
+
process.env.POSTGRES_URL ||
|
|
54
|
+
process.env.DATABASE_URL;
|
|
55
|
+
|
|
56
|
+
if (!key || typeof key !== 'string') {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
"AlexaAI: 'key' is required. Example: new AlexaAI({ key: 'tryit-...', postgresUrl: 'postgres://...' })"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!postgresUrl || typeof postgresUrl !== 'string') {
|
|
62
|
+
throw new TypeError(
|
|
63
|
+
"AlexaAI: 'postgresUrl' is required. Example: new AlexaAI({ key: '...', postgresUrl: 'postgres://user:pass@host:5432/db' })"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.key = key.trim();
|
|
68
|
+
this.postgresUrl = postgresUrl.trim();
|
|
69
|
+
|
|
70
|
+
// Extra keys are rotated to when DeepAI answers "try it exceeded".
|
|
71
|
+
this.keys = Array.from(
|
|
72
|
+
new Set(
|
|
73
|
+
[this.key]
|
|
74
|
+
.concat(Array.isArray(opts.keys) ? opts.keys : [])
|
|
75
|
+
.concat(String(process.env.DEEPAI_API_KEYS || '').split(','))
|
|
76
|
+
.map((k) => String(k || '').trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
// Generate a fresh anonymous "tryit" key when every configured key is
|
|
81
|
+
// exhausted. Off by default: it only works while DeepAI keeps issuing
|
|
82
|
+
// anonymous keys client-side.
|
|
83
|
+
this.autoKeyRotation = opts.autoKeyRotation === true;
|
|
84
|
+
|
|
85
|
+
// ---- DeepAI endpoint / model ---------------------------------------
|
|
86
|
+
this.baseUrl = (opts.baseUrl || 'https://api.deepai.org').replace(/\/+$/, '');
|
|
87
|
+
this.origin = opts.origin || 'https://deepai.org';
|
|
88
|
+
this.endpoints = { ...ENDPOINTS, ...(opts.endpoints || {}) };
|
|
89
|
+
// Back-compat: the old flat options still win if supplied.
|
|
90
|
+
if (opts.chatPath) this.endpoints.chat = opts.chatPath;
|
|
91
|
+
if (opts.uploadPath) this.endpoints.attachmentUpload = opts.uploadPath;
|
|
92
|
+
this.chatPath = this.endpoints.chat;
|
|
93
|
+
this.uploadPath = this.endpoints.attachmentUpload;
|
|
94
|
+
|
|
95
|
+
this.chatStyle = opts.chatStyle || 'chat';
|
|
96
|
+
this.model = opts.model || 'standard';
|
|
97
|
+
this.fallbackModels = Config._list(opts.fallbackModels, ['standard']).filter((m) => m !== this.model);
|
|
98
|
+
// Vision requests are routed to a vision-capable model. On anonymous
|
|
99
|
+
// "tryit" keys DeepAI downgrades this server-side; ImageDescriber
|
|
100
|
+
// detects that and degrades gracefully.
|
|
101
|
+
this.visionModel = opts.visionModel || 'gpt-4o-mini';
|
|
102
|
+
this.visionModels = Config._list(opts.visionModels, [
|
|
103
|
+
this.visionModel,
|
|
104
|
+
'gpt-4.1-mini',
|
|
105
|
+
'gpt-4o',
|
|
106
|
+
'standard',
|
|
107
|
+
]);
|
|
108
|
+
this.imageModel = opts.imageModel || 'text2img';
|
|
109
|
+
|
|
110
|
+
// ---- Chat request feature flags (mirrors the deepai.org client) -----
|
|
111
|
+
this.enabledTools = Config._list(opts.enabledTools, ['image_generator', 'image_editor']);
|
|
112
|
+
this.toolActivitySupport = opts.toolActivitySupport !== false;
|
|
113
|
+
this.thinkingImageToolSupport = opts.thinkingImageToolSupport !== false;
|
|
114
|
+
this.thinkingSupport = opts.thinkingSupport === true; // needs a reasoning model
|
|
115
|
+
this.serverMemory = opts.serverMemory === true; // DeepAI's own /chat_memory profile
|
|
116
|
+
this.webAccess = opts.webAccess === true; // DeepAI web search
|
|
117
|
+
this.sandbox = opts.sandbox === true; // "agent mode" (pro only)
|
|
118
|
+
this.concierge = opts.concierge === true; // background tasks (pro only)
|
|
119
|
+
this.sendSessionUuid = opts.sendSessionUuid !== false;
|
|
120
|
+
this.checkSensitivity = opts.checkSensitivity === true;
|
|
121
|
+
this.saveRemoteSessions = opts.saveRemoteSessions === true;
|
|
122
|
+
this.taskPollInterval = Config._int(opts.taskPollInterval, 1500, 250, 15000);
|
|
123
|
+
this.taskPollTimeout = Config._int(opts.taskPollTimeout, 120000, 5000, 600000);
|
|
124
|
+
|
|
125
|
+
// ---- Persona --------------------------------------------------------
|
|
126
|
+
this.assistantName = String(opts.assistantName || 'Alexa').trim() || 'Alexa';
|
|
127
|
+
this.creator = String(opts.creator || opts.creatorName || 'Hansaka').trim() || 'Hansaka';
|
|
128
|
+
this.systemPrompt =
|
|
129
|
+
opts.systemPrompt ||
|
|
130
|
+
Persona.build({ assistantName: this.assistantName, creator: this.creator });
|
|
131
|
+
// DeepAI historically ignored role:'system'; sending it anyway costs
|
|
132
|
+
// nothing and helps every backend that *does* honour it.
|
|
133
|
+
this.systemRole = opts.systemRole !== false;
|
|
134
|
+
this.identityLock = opts.identityLock !== false;
|
|
135
|
+
this.amnesiaGuard = opts.amnesiaGuard !== false;
|
|
136
|
+
|
|
137
|
+
// ---- Conversation / memory tuning -----------------------------------
|
|
138
|
+
this.historyLimit = Config._int(opts.historyLimit, 14, 2, 60);
|
|
139
|
+
this.maxMemories = Config._int(opts.maxMemories, 25, 0, 200);
|
|
140
|
+
this.maxMessageLength = Config._int(opts.maxMessageLength, 8000, 100, 100000);
|
|
141
|
+
this.sharedGroupThread = Boolean(opts.sharedGroupThread);
|
|
142
|
+
// Link @lid <-> phone jids so one human is one row (see IdentityResolver).
|
|
143
|
+
this.linkIdentities = opts.linkIdentities !== false;
|
|
144
|
+
this.mergeIdentities = opts.mergeIdentities !== false;
|
|
145
|
+
|
|
146
|
+
// ---- Vision / OCR ----------------------------------------------------
|
|
147
|
+
// DeepAI's own vision needs a PAID key (free 'tryit' keys are
|
|
148
|
+
// downgraded to a text-only model), so OCR is used as a fallback to
|
|
149
|
+
// read screenshots, documents and error messages.
|
|
150
|
+
this.ocrEnabled = opts.ocr !== false;
|
|
151
|
+
this.ocrUrl = opts.ocrUrl || 'https://api.ocr.space/parse/image';
|
|
152
|
+
this.ocrApiKey = opts.ocrApiKey || process.env.OCR_API_KEY || 'helloworld';
|
|
153
|
+
this.ocrLanguage = opts.ocrLanguage || 'eng';
|
|
154
|
+
this.ocrTimeout = Config._int(opts.ocrTimeout, 25000, 1000, 120000);
|
|
155
|
+
this.maxImageBytes = Config._int(opts.maxImageBytes, 12 * 1024 * 1024, 64 * 1024, 64 * 1024 * 1024);
|
|
156
|
+
|
|
157
|
+
// ---- Networking ------------------------------------------------------
|
|
158
|
+
this.timeout = Config._int(opts.timeout, 60000, 1000, 600000);
|
|
159
|
+
this.maxRetries = Config._int(opts.maxRetries, 2, 0, 10);
|
|
160
|
+
this.retryDelay = Config._int(opts.retryDelay, 800, 0, 30000);
|
|
161
|
+
this.userAgent =
|
|
162
|
+
opts.userAgent ||
|
|
163
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36';
|
|
164
|
+
|
|
165
|
+
// ---- Database --------------------------------------------------------
|
|
166
|
+
this.autoMigrate = opts.autoMigrate !== false; // default true
|
|
167
|
+
this.schema = opts.schema || 'public';
|
|
168
|
+
this.ssl = Config._resolveSsl(opts.ssl, this.postgresUrl);
|
|
169
|
+
this.pool = Object.assign(
|
|
170
|
+
{ max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 15000 },
|
|
171
|
+
opts.pool || {}
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
this.debug = Boolean(opts.debug);
|
|
175
|
+
this.logger = opts.logger || console;
|
|
176
|
+
|
|
177
|
+
Object.freeze(this.pool);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Managed Postgres (Supabase/Neon/Heroku/Railway) almost always needs SSL
|
|
182
|
+
* but ships self-signed chains, so default to relaxed verification unless
|
|
183
|
+
* the caller says otherwise or connects to localhost.
|
|
184
|
+
*/
|
|
185
|
+
static _resolveSsl(ssl, url) {
|
|
186
|
+
if (ssl !== undefined) return ssl;
|
|
187
|
+
const isLocal = /@(localhost|127\.0\.0\.1|\[::1\])[:/]/i.test(url) || /host=(localhost|127\.0\.0\.1)/i.test(url);
|
|
188
|
+
if (isLocal) return false;
|
|
189
|
+
if (/[?&]sslmode=disable/i.test(url)) return false;
|
|
190
|
+
return { rejectUnauthorized: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static _int(value, fallback, min, max) {
|
|
194
|
+
const n = Number.parseInt(value, 10);
|
|
195
|
+
if (Number.isNaN(n)) return fallback;
|
|
196
|
+
return Math.min(Math.max(n, min), max);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static _list(value, fallback) {
|
|
200
|
+
const source = Array.isArray(value) ? value : typeof value === 'string' ? value.split(',') : null;
|
|
201
|
+
if (!source) return [...fallback];
|
|
202
|
+
const cleaned = source.map((v) => String(v || '').trim()).filter(Boolean);
|
|
203
|
+
return cleaned.length ? Array.from(new Set(cleaned)) : [...fallback];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Absolute URL for a named endpoint (`url('chat')`). */
|
|
207
|
+
url(name, query = null) {
|
|
208
|
+
const path = this.endpoints[name] || name;
|
|
209
|
+
const base = `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
210
|
+
if (!query) return base;
|
|
211
|
+
const qs = new URLSearchParams(
|
|
212
|
+
Object.entries(query).filter(([, v]) => v !== undefined && v !== null)
|
|
213
|
+
).toString();
|
|
214
|
+
return qs ? `${base}?${qs}` : base;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get chatUrl() {
|
|
218
|
+
return this.url('chat');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
get uploadUrl() {
|
|
222
|
+
return this.url('attachmentUpload');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Redacted view, safe to log. */
|
|
226
|
+
toJSON() {
|
|
227
|
+
return {
|
|
228
|
+
baseUrl: this.baseUrl,
|
|
229
|
+
model: this.model,
|
|
230
|
+
fallbackModels: this.fallbackModels,
|
|
231
|
+
visionModels: this.visionModels,
|
|
232
|
+
assistantName: this.assistantName,
|
|
233
|
+
creator: this.creator,
|
|
234
|
+
historyLimit: this.historyLimit,
|
|
235
|
+
maxMemories: this.maxMemories,
|
|
236
|
+
sharedGroupThread: this.sharedGroupThread,
|
|
237
|
+
linkIdentities: this.linkIdentities,
|
|
238
|
+
timeout: this.timeout,
|
|
239
|
+
maxRetries: this.maxRetries,
|
|
240
|
+
autoMigrate: this.autoMigrate,
|
|
241
|
+
schema: this.schema,
|
|
242
|
+
keys: this.keys.length,
|
|
243
|
+
key: `${this.key.slice(0, 10)}…`,
|
|
244
|
+
postgresUrl: this.postgresUrl.replace(/\/\/([^:]+):([^@]+)@/, '//$1:****@'),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = Config;
|