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,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;