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
package/README.md
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
# alexa-ai
|
|
2
|
+
|
|
3
|
+
> The AI engine behind the Alexa WhatsApp bot — DeepAI-powered conversation with
|
|
4
|
+
> PostgreSQL-backed long-term memory and cross-chat identity.
|
|
5
|
+
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://www.postgresql.org)
|
|
8
|
+
[](#testing)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](CHANGELOG.md)
|
|
11
|
+
|
|
12
|
+
`alexa-ai` is a standalone Node.js library with **no WhatsApp code in it**. Your
|
|
13
|
+
bot — Baileys, whatsapp-web.js or anything else — hands it a message and a
|
|
14
|
+
sender, and receives a WhatsApp-ready reply. Everything in between — persona,
|
|
15
|
+
memory, identity resolution, media understanding, output formatting and the
|
|
16
|
+
DeepAI transport — is handled by the engine.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const AlexaAI = require('alexa-ai');
|
|
20
|
+
|
|
21
|
+
const ai = new AlexaAI({
|
|
22
|
+
key: process.env.DEEPAI_API_KEY,
|
|
23
|
+
postgresUrl: process.env.POSTGRES_URL,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const { text } = await ai.chat({
|
|
27
|
+
message: "Hi, I'm Nimal and I love cricket",
|
|
28
|
+
userId: '78151912841263@lid', // sender (DM or group)
|
|
29
|
+
groupId: '120363413125431525@g.us', // omit for a DM
|
|
30
|
+
userName: 'Nimal',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await sock.sendMessage(jid, { text }); // your WhatsApp layer
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Highlights
|
|
39
|
+
|
|
40
|
+
- **Long-term memory in PostgreSQL.** Facts learned about a person survive
|
|
41
|
+
restarts and are available in their private chat *and* in every group.
|
|
42
|
+
- **One person, many addresses.** WhatsApp uses `…@lid` in groups and
|
|
43
|
+
`…@s.whatsapp.net` in DMs. The engine links every address to a single
|
|
44
|
+
identity and merges duplicate rows automatically.
|
|
45
|
+
- **The whole DeepAI surface.** Chat, attachments, reasoning tasks, sessions,
|
|
46
|
+
account settings and the classic `/api/*` family — text-to-image, upscaling,
|
|
47
|
+
editing, colourising, NSFW detection, summarisation — from one client.
|
|
48
|
+
- **Works on free keys.** Image generation, summarisation and image reading
|
|
49
|
+
fall back to routes that work on anonymous `tryit-…` keys.
|
|
50
|
+
- **WhatsApp-native output.** Markdown is converted to WhatsApp formatting,
|
|
51
|
+
long replies are chunked, and DeepAI's wire packets never reach the user.
|
|
52
|
+
- **A persona you can rely on.** Deterministic command triggers, an identity
|
|
53
|
+
lock and a memory guard keep Alexa in character whatever the backend returns.
|
|
54
|
+
- **Object-oriented and testable.** Every responsibility is its own class, and
|
|
55
|
+
300+ assertions run with no network and no database.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Contents
|
|
60
|
+
|
|
61
|
+
- [Requirements](#requirements)
|
|
62
|
+
- [Installation](#installation)
|
|
63
|
+
- [Quick start](#quick-start)
|
|
64
|
+
- [Usage](#usage)
|
|
65
|
+
- [`ai.chat(params)`](#aichatparams)
|
|
66
|
+
- [Integrating with an existing bot](#integrating-with-an-existing-bot)
|
|
67
|
+
- [Identity: one person, many addresses](#identity-one-person-many-addresses)
|
|
68
|
+
- [Memory](#memory)
|
|
69
|
+
- [Images and documents](#images-and-documents)
|
|
70
|
+
- [Image generation, web search and other tools](#image-generation-web-search-and-other-tools)
|
|
71
|
+
- [Moderation and administration](#moderation-and-administration)
|
|
72
|
+
- [Configuration](#configuration)
|
|
73
|
+
- [Architecture](#architecture)
|
|
74
|
+
- [Database schema](#database-schema)
|
|
75
|
+
- [DeepAI API reference](#deepai-api-reference)
|
|
76
|
+
- [Troubleshooting](#troubleshooting)
|
|
77
|
+
- [Testing](#testing)
|
|
78
|
+
- [Examples](#examples)
|
|
79
|
+
- [Changelog](#changelog)
|
|
80
|
+
- [License](#license)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Requirements
|
|
85
|
+
|
|
86
|
+
| | |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| **Node.js** | 18 or newer — the engine uses the built-in `fetch`, `FormData` and `Blob` |
|
|
89
|
+
| **PostgreSQL** | 12 or newer, local or managed (Supabase, Neon, Railway, Heroku…) |
|
|
90
|
+
| **DeepAI key** | an anonymous `tryit-…` key is enough for chat, memory, OCR and image generation; a paid key additionally unlocks native vision and the `/api/*` endpoints |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Installation
|
|
95
|
+
|
|
96
|
+
The package is distributed from this repository rather than the npm registry:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install github:AlexaInc/deepai
|
|
100
|
+
# or, from a local checkout
|
|
101
|
+
npm install /path/to/deepai
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`pg` is the only runtime dependency and is installed automatically.
|
|
105
|
+
|
|
106
|
+
Provide the two required settings through the environment (or pass them to the
|
|
107
|
+
constructor — see [Configuration](#configuration)):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
DEEPAI_API_KEY=tryit-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
111
|
+
POSTGRES_URL=postgres://user:password@host:5432/alexa
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Tables are created automatically on first connect. To run the migration
|
|
115
|
+
explicitly (for example in a deploy step):
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
POSTGRES_URL=... npm run migrate
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
To confirm the installed build:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
node -e "console.log(require('alexa-ai').version)" # 2.1.0
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Quick start
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const AlexaAI = require('alexa-ai');
|
|
133
|
+
|
|
134
|
+
const ai = new AlexaAI({
|
|
135
|
+
key: process.env.DEEPAI_API_KEY,
|
|
136
|
+
postgresUrl: process.env.POSTGRES_URL,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// A Baileys message from a group. Baileys exposes both addresses of the sender:
|
|
140
|
+
// the privacy jid used in the group and the phone jid used in DMs.
|
|
141
|
+
const { text } = await ai.chat({
|
|
142
|
+
message: text,
|
|
143
|
+
userId: msg.key.participant, // 78151912841263@lid
|
|
144
|
+
aliases: [msg.key.participantAlt], // 94771234567@s.whatsapp.net
|
|
145
|
+
groupId: msg.key.remoteJid, // 120363413125431525@g.us
|
|
146
|
+
userName: msg.pushName,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await sock.sendMessage(msg.key.remoteJid, { text });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
That is the whole integration. The engine opens its connection pool lazily on
|
|
153
|
+
the first call; call `await ai.close()` on shutdown.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Usage
|
|
158
|
+
|
|
159
|
+
### `ai.chat(params)`
|
|
160
|
+
|
|
161
|
+
Handles one incoming message: resolves the sender to a person, loads their
|
|
162
|
+
memory and the thread history, builds the prompt, calls DeepAI, post-processes
|
|
163
|
+
the reply and persists everything.
|
|
164
|
+
|
|
165
|
+
| param | type | required | notes |
|
|
166
|
+
| ----------- | ----------------- | -------- | -------------------------------------------------- |
|
|
167
|
+
| `message` | `string` | ✔\* | the user's text |
|
|
168
|
+
| `userId` | `string` | ✔ | `78151912841263@lid` or `947…@s.whatsapp.net` |
|
|
169
|
+
| `userLid` | `string` | – | the sender's `@lid` address, if known |
|
|
170
|
+
| `userPhone` | `string` | – | phone jid **or** bare number behind the `@lid` |
|
|
171
|
+
| `aliases` | `string[]` | – | any other address for the same person |
|
|
172
|
+
| `groupId` | `string` | – | `120363413125431525@g.us`; omit or `''` for a DM |
|
|
173
|
+
| `userName` | `string` | – | WhatsApp push name |
|
|
174
|
+
| `groupName` | `string` | – | group subject |
|
|
175
|
+
| `image` | see below | – | an attached image or document |
|
|
176
|
+
| `messageId` | `string` | – | WhatsApp message id, used to de-duplicate redeliveries |
|
|
177
|
+
| `isAdmin` | `boolean` | – | sender is a group admin |
|
|
178
|
+
| `model` | `string` | – | override the DeepAI model for this turn |
|
|
179
|
+
| `webAccess` | `boolean` | – | let DeepAI search the web for this turn |
|
|
180
|
+
| `thinking` | `boolean` | – | use the asynchronous reasoning path |
|
|
181
|
+
| `onToken` | `function` | – | `(delta, full)` streaming callback |
|
|
182
|
+
| `signal` | `AbortSignal` | – | cancel an in-flight request |
|
|
183
|
+
|
|
184
|
+
\* required unless an `image` is supplied.
|
|
185
|
+
|
|
186
|
+
`image` accepts a `Buffer`, `Uint8Array`, data URI, raw base64 string, `http(s)`
|
|
187
|
+
URL, or a `{ buffer | base64 | data | url, mimetype?, filename? }` object —
|
|
188
|
+
Baileys and whatsapp-web.js media objects work as-is. The content type is
|
|
189
|
+
detected from the bytes when it is missing or wrong.
|
|
190
|
+
|
|
191
|
+
> **Pass every address you have.** Supplying both the `@lid` and the phone jid
|
|
192
|
+
> (Baileys: `key.participant` and `key.participantAlt`) is what lets Alexa
|
|
193
|
+
> recognise a DM user inside a group. See [Identity](#identity-one-person-many-addresses).
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
{
|
|
199
|
+
text : 'Nice to meet you, Nimal! 🏏', // clean, WhatsApp-ready
|
|
200
|
+
raw : '...@MEMORY: {"name":"Nimal"}', // unmodified model output
|
|
201
|
+
memories : { name: 'Nimal', hobby: 'cricket' }, // facts learned this turn
|
|
202
|
+
trigger : null, // 'weather' | 'menu' | 'ping' | 'doc' when matched
|
|
203
|
+
isGroup : false,
|
|
204
|
+
contextKey: 'dm:78151912841263@lid',
|
|
205
|
+
userName : 'Nimal',
|
|
206
|
+
userId : 42, // the canonical person behind every alias
|
|
207
|
+
aliases : ['78151912841263@lid', '94771234567@s.whatsapp.net'],
|
|
208
|
+
mergedIdentities: false, // true when two rows were folded into one
|
|
209
|
+
repairedMemory : false, // true when an "I can't remember" denial was corrected
|
|
210
|
+
images : [], // urls when the model used its image tool
|
|
211
|
+
model : 'standard', // the model that actually answered
|
|
212
|
+
latencyMs : 1420,
|
|
213
|
+
chunks : ['...'], // pre-split for WhatsApp's length cap
|
|
214
|
+
error : null // 'user_blocked' | 'group_disabled' | 'DEEPAI_QUOTA_EXCEEDED' | ...
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`chat()` never throws on a network or model failure — it returns a friendly
|
|
219
|
+
`text` and sets `error`, so the bot always has something to send. It *does*
|
|
220
|
+
throw `ValidationError` for a malformed `userId`.
|
|
221
|
+
|
|
222
|
+
### Integrating with an existing bot
|
|
223
|
+
|
|
224
|
+
[`examples/bot-ai.js`](examples/bot-ai.js) is a complete drop-in module for a
|
|
225
|
+
bot whose AI layer has the classic callback signature. Copy it to
|
|
226
|
+
`src/modules/Aii.js` (or wherever your bot requires it) and nothing else
|
|
227
|
+
changes:
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
const ai = require('./modules/Aii');
|
|
231
|
+
|
|
232
|
+
// string message
|
|
233
|
+
const reply = await ai(text, senderJid, groupJid, pushName);
|
|
234
|
+
|
|
235
|
+
// message with an attachment, plus both sender addresses
|
|
236
|
+
const reply = await ai({ text, files: [buffer] }, { id: lid, phone: phoneJid }, groupJid, pushName);
|
|
237
|
+
|
|
238
|
+
// straight from a Baileys message object
|
|
239
|
+
const reply = await ai.fromMessage(msg, sock);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The module also exposes the administration and media helpers described below
|
|
243
|
+
(`ai.generateImage`, `ai.searchWeb`, `ai.blockUser`, …) and checks the
|
|
244
|
+
installed engine version at startup.
|
|
245
|
+
|
|
246
|
+
If you prefer to keep your own module, `engine.ask()` has the same
|
|
247
|
+
`(message, userId, groupId, userName, callback)` signature, including the
|
|
248
|
+
`{ text, files: [] }` message shape.
|
|
249
|
+
|
|
250
|
+
### Identity: one person, many addresses
|
|
251
|
+
|
|
252
|
+
WhatsApp addresses the same human differently depending on where they write:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
DM -> key.remoteJid = 94771234567@s.whatsapp.net (phone jid)
|
|
256
|
+
GROUP -> key.participant = 78151912841263@lid (privacy / LID jid)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
A naive implementation keys users on the jid and ends up with two rows — one
|
|
260
|
+
that knows everything about the person and one that knows nothing. Since LID
|
|
261
|
+
addressing is now the default in groups, this affects every user.
|
|
262
|
+
|
|
263
|
+
`alexa-ai` models identity as an **alias graph**:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
94771234567@s.whatsapp.net ─┐
|
|
267
|
+
78151912841263@lid ─────────┤ wa_user_identities ┌────────────────────┐
|
|
268
|
+
94771234567:12@s.whatsapp.net┤ (jid -> user_id) ─────►│ wa_users id = 42 │
|
|
269
|
+
…any future address ────────┘ └─────────┬──────────┘
|
|
270
|
+
│ user_id
|
|
271
|
+
┌────────────▼─────────────┐
|
|
272
|
+
│ wa_memories │
|
|
273
|
+
│ (user_id, key) UNIQUE │
|
|
274
|
+
└──────────────────────────┘
|
|
275
|
+
|
|
276
|
+
conversations stay separate so chat context never bleeds between rooms
|
|
277
|
+
(keyed on the person's canonical address, so they survive an address change):
|
|
278
|
+
dm:94771234567@s.whatsapp.net
|
|
279
|
+
group:120363413125431525@g.us:94771234567@s.whatsapp.net
|
|
280
|
+
group:120363999888777666@g.us:94771234567@s.whatsapp.net
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Pass every address you have and the engine links them. If two addresses turn
|
|
284
|
+
out to belong to rows that already exist, the rows are **merged** inside a
|
|
285
|
+
transaction — memories, transcripts, group membership and counters move to the
|
|
286
|
+
surviving row.
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
// Baileys provides both addresses on a group message
|
|
290
|
+
const sender = msg.key.participant || msg.key.remoteJid; // 781…@lid
|
|
291
|
+
const senderAlt = msg.key.participantAlt || msg.key.participantPn; // 947…@s.whatsapp.net
|
|
292
|
+
|
|
293
|
+
await ai.chat({ message, userId: sender, aliases: [senderAlt], groupId, userName });
|
|
294
|
+
|
|
295
|
+
// or record a mapping whenever you learn one
|
|
296
|
+
await ai.linkIdentity('78151912841263@lid', '94771234567@s.whatsapp.net');
|
|
297
|
+
|
|
298
|
+
await ai.getAliases('78151912841263@lid'); // every address for this person
|
|
299
|
+
await ai.whoIs('94771234567@s.whatsapp.net'); // { user, aliases, memories }
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Rules the engine follows:
|
|
303
|
+
|
|
304
|
+
- **Memories are global per person.** A fact learned in a DM is available in
|
|
305
|
+
every group, and vice versa.
|
|
306
|
+
- **Transcripts are per thread.** A DM and each group keep independent history,
|
|
307
|
+
so group chatter never contaminates a private conversation.
|
|
308
|
+
- **A jid belongs to exactly one person.** When an address appears under a
|
|
309
|
+
second user, the rows are merged: the older row survives, and the newest
|
|
310
|
+
value wins per memory key.
|
|
311
|
+
- **`@lid` is a privacy id, never a phone number.** `phone` stays `NULL` for
|
|
312
|
+
those identities until a phone jid is linked.
|
|
313
|
+
- **Device suffixes are ignored.** `94771234567:12@s.whatsapp.net` and
|
|
314
|
+
`94771234567@s.whatsapp.net` are the same person.
|
|
315
|
+
- **Nothing breaks without aliases.** If you only ever pass one address, the
|
|
316
|
+
engine behaves as a plain one-row-per-jid system.
|
|
317
|
+
|
|
318
|
+
Example session against a live database and the live API:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
[DM] "Hi, I'm Nimal and I love cricket. I live in Galle."
|
|
322
|
+
-> learned {name: Nimal, location: Galle, hobby: cricket}
|
|
323
|
+
[GROUP A] "Do you remember my name and where I live?"
|
|
324
|
+
-> "I do! Your name is Nimal, and I believe you're from Galle."
|
|
325
|
+
[GROUP B] "What is my hobby?"
|
|
326
|
+
-> "Your hobby is cricket."
|
|
327
|
+
[GROUP A] a different user asks "Do you know my name?"
|
|
328
|
+
-> "I don't have that information yet." ← correctly isolated
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Memory
|
|
332
|
+
|
|
333
|
+
Facts are learned in two ways and stored under `UNIQUE (user_id, key)`, so
|
|
334
|
+
re-learning `name` overwrites it instead of creating duplicates:
|
|
335
|
+
|
|
336
|
+
1. **From the model.** The persona asks the model to append
|
|
337
|
+
`@MEMORY: {"key": "value"}` to a reply whenever the user reveals something
|
|
338
|
+
personal. `MemoryExtractor` parses the tag (including malformed variants)
|
|
339
|
+
and strips it from the visible text.
|
|
340
|
+
2. **From the user's own words.** Small models frequently ignore the tag
|
|
341
|
+
rule, so `FactMiner` extracts high-confidence first-person facts locally
|
|
342
|
+
(`my name is…`, `I live in…`, `I support…`) and skips third-party
|
|
343
|
+
statements (`my friend lives in Kandy`). Model-emitted tags win on conflict.
|
|
344
|
+
|
|
345
|
+
Stored facts are injected into every prompt for that person — as a block in
|
|
346
|
+
the persona and again as a compact note directly above the live message, where
|
|
347
|
+
they are far less likely to be diluted by a long system prompt. A recall
|
|
348
|
+
question (*"do you remember me?"*) additionally receives an explicit
|
|
349
|
+
`[MEMORY CHECK]` directive, and `AmnesiaGuard` rewrites any residual
|
|
350
|
+
*"as a bot I can't remember"* from the database so the reply is never false.
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
await ai.getMemories(jid); // { name: 'Nimal', ... }
|
|
354
|
+
await ai.remember(jid, 'favourite_team', 'Sri Lanka');
|
|
355
|
+
await ai.forget(jid, 'favourite_team');
|
|
356
|
+
await ai.forgetAll(jid);
|
|
357
|
+
await ai.clearHistory(jid); // wipe the DM transcript (memories stay)
|
|
358
|
+
await ai.clearHistory(jid, groupJid); // wipe one group thread
|
|
359
|
+
await ai.getProfile(jid); // user + memories + threads
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Every `jid` argument follows the alias graph, so any address the person is
|
|
363
|
+
known under works.
|
|
364
|
+
|
|
365
|
+
### Images and documents
|
|
366
|
+
|
|
367
|
+
An attachment on `chat()` runs through a provider chain; the first provider
|
|
368
|
+
that produces text wins:
|
|
369
|
+
|
|
370
|
+
| # | Provider | Handles | Free key |
|
|
371
|
+
|---|----------|---------|----------|
|
|
372
|
+
| 0 | DeepAI document extraction | `.txt` `.pdf` `.docx` `.csv` `.md` … | ✅ |
|
|
373
|
+
| 1 | DeepAI native vision | full understanding of any photo | paid plans |
|
|
374
|
+
| 2 | OCR (`ocr.space`) | text inside images and screenshots | ✅ |
|
|
375
|
+
| 3 | Honest fallback | photos with no readable text | ✅ |
|
|
376
|
+
|
|
377
|
+
- The file is uploaded **once** and reused by every provider.
|
|
378
|
+
- Vision walks the full `visionModels` chain per model. A plan refusal puts
|
|
379
|
+
native vision on a 30-minute cooldown rather than disabling it for the life
|
|
380
|
+
of the process, so upgrading the key simply starts working.
|
|
381
|
+
- When the upload succeeded but nothing could be pre-read, the attachment
|
|
382
|
+
uuid is forwarded with the real conversation; an account with vision then
|
|
383
|
+
sees the picture in full context.
|
|
384
|
+
- A photo with no readable text gets an honest reply asking what it shows —
|
|
385
|
+
the model is never allowed to invent a description.
|
|
386
|
+
- Oversized files are rejected (`maxImageBytes`, default 12 MB) rather than
|
|
387
|
+
truncated.
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
// a screenshot -> OCR
|
|
391
|
+
await ai.chat({ message: 'what does this say?', userId, image: buffer });
|
|
392
|
+
// -> "It says: SECRETCODE ZQ7412 …"
|
|
393
|
+
|
|
394
|
+
// a document -> server-side extraction
|
|
395
|
+
await ai.chat({ message: 'what is the total?', userId,
|
|
396
|
+
image: { buffer, mimetype: 'text/plain', filename: 'invoice.txt' } });
|
|
397
|
+
// -> "`Total: 4500 LKR, due 2026-10-01`"
|
|
398
|
+
|
|
399
|
+
// read something without touching the conversation
|
|
400
|
+
await ai.describeImage(buffer, 'is this a receipt?'); // { ok, text, description, source }
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
> **Tip:** the default OCR key is `ocr.space`'s shared demo key and is
|
|
404
|
+
> rate-limited. Get a free key at [ocr.space/ocrapi](https://ocr.space/ocrapi)
|
|
405
|
+
> and set `OCR_API_KEY` (or `ocrApiKey`).
|
|
406
|
+
|
|
407
|
+
### Image generation, web search and other tools
|
|
408
|
+
|
|
409
|
+
These helpers live on the engine and **never throw** — each resolves to
|
|
410
|
+
`{ ok, …, error?, message? }` so a command handler can check `ok` and forward
|
|
411
|
+
`message` when it is false.
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
await ai.generateImage('a red tuk-tuk in Galle at sunset'); // { ok, url, id, via }
|
|
415
|
+
await ai.searchWeb('coffee'); // { ok, text, answer, sources: [{ title, url }] }
|
|
416
|
+
await ai.summarizeText(longText); // { ok, text }
|
|
417
|
+
await ai.describeImage(buffer, caption); // { ok, text, description, source }
|
|
418
|
+
|
|
419
|
+
await ai.upscaleImage(buffer); // { ok, url } 4x super-resolution
|
|
420
|
+
await ai.editImage(buffer, 'make the sky purple'); // { ok, url }
|
|
421
|
+
await ai.colorizeImage(buffer); // { ok, url }
|
|
422
|
+
await ai.detectNsfw(buffer); // { ok, score, nsfw }
|
|
423
|
+
|
|
424
|
+
await ai.deepaiHealth(); // { ok, latencyMs, reply }
|
|
425
|
+
await ai.deepai.runApi('waifu2x', { image: buffer }); // any /api/<name> endpoint
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
All media arguments accept the same shapes as `chat({ image })`.
|
|
429
|
+
|
|
430
|
+
**`generateImage()` on a free key.** `POST /api/text2img` is a paid endpoint:
|
|
431
|
+
anonymous keys receive `{"status": "Out of API credits"}`. The engine tries it
|
|
432
|
+
first because it is fast and returns a plain URL; when it is refused, it
|
|
433
|
+
drives the same in-chat `generate_image` tool the deepai.org web client uses,
|
|
434
|
+
which works on free chat keys. The result reports the route that answered
|
|
435
|
+
(`via: 'api' | 'chat'`). Options: `{ apiOnly }`, `{ chatToolOnly }`,
|
|
436
|
+
`{ aspectRatio: '16:9' }` for the chat tool, and `width` / `height` /
|
|
437
|
+
`image_generator_version` for the API.
|
|
438
|
+
|
|
439
|
+
**`summarizeText()`** follows the same pattern — `/api/summarization` first, a
|
|
440
|
+
stateless chat request as the fallback.
|
|
441
|
+
|
|
442
|
+
**`searchWeb()`** is a one-off research request with DeepAI's web access
|
|
443
|
+
enabled. It applies the persona and formatting rules but touches no user's
|
|
444
|
+
memory or history, so it can be called with no jid at all. The default answer
|
|
445
|
+
is long-form and ready to send to WhatsApp — an intro, three to five
|
|
446
|
+
`*Heading:*` sections with numbered `*Title*: detail` points, and one
|
|
447
|
+
`*Sources:*` block at the end:
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
Coffee remains one of the most traded commodities in the world, and 2026 has
|
|
451
|
+
brought record prices.
|
|
452
|
+
|
|
453
|
+
*Recent Coffee News:*
|
|
454
|
+
1. *Arabica futures hit a high*: Prices rose 12% in August after frost damaged
|
|
455
|
+
crops in Brazil.
|
|
456
|
+
2. *Starbucks menu shake-up*: The chain removed 30% of its drinks.
|
|
457
|
+
…
|
|
458
|
+
|
|
459
|
+
*Coffee Trends:*
|
|
460
|
+
1. *Cold brew keeps growing*: Ready-to-drink sales are up 20%.
|
|
461
|
+
…
|
|
462
|
+
|
|
463
|
+
*Sources:*
|
|
464
|
+
1. Reuters – Coffee prices — https://www.reuters.com/…
|
|
465
|
+
2. National Coffee Association — https://www.ncausa.org/
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
The model is given the layout as a fill-in template rather than a description
|
|
469
|
+
of one; small models follow a visible layout far more reliably. If a long-form
|
|
470
|
+
answer still comes back under `minWords` (default 150), one follow-up turn asks
|
|
471
|
+
the model to rewrite it in full, and the longer reply wins — `attempts` and
|
|
472
|
+
`words` in the result show what happened.
|
|
473
|
+
|
|
474
|
+
DeepAI reports the pages it used in two different ways depending on the model
|
|
475
|
+
that answered: some send a structured web-results packet, others (observed
|
|
476
|
+
with `gpt-4o-mini`) just write a `Sources:` list in prose. Both end up in the
|
|
477
|
+
`sources` array as `{ title, url, description }`; the list is rendered exactly
|
|
478
|
+
once in `text`, and `answer` holds the prose without it. Sentences in which
|
|
479
|
+
the model talks about itself — *"I'm a language model"*, *"I can't browse the
|
|
480
|
+
web"*, *"based on my training data"* — are removed, and leftover template
|
|
481
|
+
placeholders are dropped. Third-party names in the research itself (news
|
|
482
|
+
about OpenAI or Google) are kept verbatim.
|
|
483
|
+
|
|
484
|
+
```js
|
|
485
|
+
await ai.searchWeb('coffee', {
|
|
486
|
+
detail: 'short', // 2–4 sentences instead of the sectioned long form
|
|
487
|
+
minWords: 0, // never retry a short long-form answer (default 150)
|
|
488
|
+
includeSources: false, // keep the *Sources:* block out of `text`
|
|
489
|
+
maxSources: 3, // how many to list in `text` (the array is not capped)
|
|
490
|
+
language: 'Sinhala', // answer language (default: the language of the query)
|
|
491
|
+
instructions: 'focus on Sri Lanka',
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Moderation and administration
|
|
496
|
+
|
|
497
|
+
```js
|
|
498
|
+
await ai.blockUser(jid); // any of the person's addresses
|
|
499
|
+
await ai.unblockUser(jid);
|
|
500
|
+
await ai.isBlocked(jid);
|
|
501
|
+
|
|
502
|
+
await ai.setGroupEnabled(groupJid, false); // mute Alexa in one group
|
|
503
|
+
await ai.isGroupEnabled(groupJid); // unknown groups are enabled
|
|
504
|
+
|
|
505
|
+
await ai.stats(); // { users, groups, conversations, messages, memories, active_24h }
|
|
506
|
+
await ai.health(); // { ok, now, database }
|
|
507
|
+
await ai.close(); // close the pool on shutdown
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
`blockUser()` and `setGroupEnabled()` create the row when the person or group
|
|
511
|
+
has never been seen, so a pre-emptive block or mute is already in force on
|
|
512
|
+
the first message. Blocked users and disabled groups get
|
|
513
|
+
`{ text: '', error: 'user_blocked' | 'group_disabled' }` from `chat()`, so the
|
|
514
|
+
bot can simply skip sending.
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Configuration
|
|
519
|
+
|
|
520
|
+
```js
|
|
521
|
+
new AlexaAI({
|
|
522
|
+
key: 'tryit-...', // required (alias: apiKey); falls back to DEEPAI_API_KEY
|
|
523
|
+
postgresUrl: 'postgres://...', // required (alias: databaseUrl); falls back to POSTGRES_URL / DATABASE_URL
|
|
524
|
+
|
|
525
|
+
// DeepAI
|
|
526
|
+
model: 'standard', // chat model
|
|
527
|
+
fallbackModels: ['standard'], // tried in order when the main model is refused
|
|
528
|
+
visionModel: 'gpt-4o-mini', // first model tried for attachments
|
|
529
|
+
visionModels: [...], // full vision fallback chain
|
|
530
|
+
keys: ['tryit-a', 'tryit-b'], // rotated automatically on "try it exceeded"
|
|
531
|
+
autoKeyRotation: false, // mint a fresh anonymous key when every key is spent
|
|
532
|
+
webAccess: false, // DeepAI web search on every turn
|
|
533
|
+
thinkingSupport: false, // asynchronous reasoning path
|
|
534
|
+
serverMemory: false, // DeepAI's own /chat_memory profile
|
|
535
|
+
enabledTools: ['image_generator', 'image_editor'],
|
|
536
|
+
endpoints: { chat: '/hacking_is_a_serious_crime', ... }, // override any route
|
|
537
|
+
|
|
538
|
+
// Persona
|
|
539
|
+
assistantName: 'Alexa', // also drives IdentityGuard / AmnesiaGuard
|
|
540
|
+
creator: 'Hansaka',
|
|
541
|
+
systemPrompt: '...', // replace the whole persona
|
|
542
|
+
systemRole: true, // also send a role:'system' digest
|
|
543
|
+
identityLock: true, // inject the identity lock on identity questions
|
|
544
|
+
amnesiaGuard: true, // repair "I can't remember" denials
|
|
545
|
+
|
|
546
|
+
// Identity & memory
|
|
547
|
+
linkIdentities: true, // @lid <-> phone alias graph
|
|
548
|
+
mergeIdentities: true, // merge rows that prove to be one person
|
|
549
|
+
historyLimit: 14, // past messages replayed to the model
|
|
550
|
+
maxMemories: 25, // facts injected per request
|
|
551
|
+
sharedGroupThread: false, // true = one thread per group instead of per member
|
|
552
|
+
triggers: true, // deterministic weather/menu/ping/doc matching
|
|
553
|
+
memory: true, // long-term memory
|
|
554
|
+
factMining: true, // local fact extraction
|
|
555
|
+
|
|
556
|
+
// Media
|
|
557
|
+
ocr: true,
|
|
558
|
+
ocrApiKey: process.env.OCR_API_KEY,
|
|
559
|
+
maxImageBytes: 12 * 1024 * 1024,
|
|
560
|
+
|
|
561
|
+
// Infrastructure
|
|
562
|
+
timeout: 60000,
|
|
563
|
+
maxRetries: 2,
|
|
564
|
+
autoMigrate: true,
|
|
565
|
+
ssl: undefined, // auto: off for localhost, relaxed for managed PostgreSQL
|
|
566
|
+
pool: { max: 10 }, // extra node-postgres pool options
|
|
567
|
+
logger: console,
|
|
568
|
+
debug: false,
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## Architecture
|
|
575
|
+
|
|
576
|
+
The bot talks to one class, `AlexaAI`. Every other responsibility is its own
|
|
577
|
+
class with a single job:
|
|
578
|
+
|
|
579
|
+
```
|
|
580
|
+
AlexaAI orchestrator; the only class the bot touches
|
|
581
|
+
├── Config validated settings, env fallbacks, redacted logging
|
|
582
|
+
├── Endpoints every DeepAI route in one overridable map
|
|
583
|
+
├── DeepAIClient DeepAI transport: chat, tasks, attachments, sessions,
|
|
584
|
+
│ settings, /api/* — retries and key rotation
|
|
585
|
+
├── StreamParser splits DeepAI's stream: text | tool activity |
|
|
586
|
+
│ web results | generated images | chain-of-thought
|
|
587
|
+
├── Persona / SystemPrompt the Alexa prompt, renameable per deployment
|
|
588
|
+
├── Database pg pool, migrations, transactions
|
|
589
|
+
├── UserRepository users, groups, membership, blocking
|
|
590
|
+
├── IdentityRepository the alias graph (@lid <-> phone) and row merging
|
|
591
|
+
├── MemoryRepository long-term facts (global per person)
|
|
592
|
+
├── ConversationRepository threads, messages, history windows, usage log
|
|
593
|
+
├── IdentityResolver "which person is this?" across every address
|
|
594
|
+
├── PromptBuilder assembles chatHistory (system + persona + memory)
|
|
595
|
+
├── TriggerDetector deterministic weather/menu/ping/doc matching
|
|
596
|
+
├── MathDetector flags maths questions for terse answers
|
|
597
|
+
├── MemoryExtractor parses and strips the @MEMORY tag
|
|
598
|
+
├── FactMiner local first-person fact extraction
|
|
599
|
+
├── ResponseFormatter enforces WhatsApp formatting; chunks long replies
|
|
600
|
+
├── IdentityGuard keeps Alexa in character (no vendor names,
|
|
601
|
+
│ no "Alexa Mini", no self-denial)
|
|
602
|
+
├── AmnesiaGuard never lets her deny a memory she actually has
|
|
603
|
+
├── ImageDescriber vision chain: documents -> DeepAI -> OCR -> fallback
|
|
604
|
+
├── WebAnswer searchWeb prompt; lifts "Sources:" lists out of prose
|
|
605
|
+
├── Media normalises every media input shape
|
|
606
|
+
└── JidParser normalises @lid / @s.whatsapp.net / @g.us
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
All classes are exported from the package entry point for advanced use and
|
|
610
|
+
testing (`const { StreamParser, Media, JidParser } = require('alexa-ai')`).
|
|
611
|
+
|
|
612
|
+
### Design notes
|
|
613
|
+
|
|
614
|
+
**Command triggers are matched in code, not by the model.**
|
|
615
|
+
The bot's command router expects byte-exact outputs for four intents
|
|
616
|
+
(`weather <city>`, `menu`, `ping`, `doc`). Small models do not comply reliably
|
|
617
|
+
— `standard` will happily answer *"send me the docs"* with an essay.
|
|
618
|
+
`TriggerDetector` matches these intents deterministically and bypasses the
|
|
619
|
+
model entirely, so routing can never break. Disable with `triggers: false`.
|
|
620
|
+
|
|
621
|
+
**Memory is injected next to the question, not only at the top.**
|
|
622
|
+
Facts placed solely in the header of a long persona get diluted; in testing
|
|
623
|
+
the model insisted *"our conversation just started"* in 0/4 trials from the
|
|
624
|
+
header alone and recalled correctly in 4/4 when the same facts were repeated
|
|
625
|
+
as a compact note above the live message. Both placements are used.
|
|
626
|
+
|
|
627
|
+
**Facts are mined locally as a safety net.**
|
|
628
|
+
The model frequently ignores the `@MEMORY:` rule. `FactMiner` extracts
|
|
629
|
+
high-confidence facts from explicit first-person statements and defers to the
|
|
630
|
+
model's own tags on conflict. Disable with `factMining: false`.
|
|
631
|
+
|
|
632
|
+
**Identity is a graph, not a column.**
|
|
633
|
+
A person is not their jid. `wa_user_identities` maps every address to one
|
|
634
|
+
row, and rows that prove to be the same person are merged in a transaction.
|
|
635
|
+
This is what makes DM facts appear in groups.
|
|
636
|
+
|
|
637
|
+
**The engine has the last word, not the model.**
|
|
638
|
+
Two deterministic post-processors run on every reply: `IdentityGuard`
|
|
639
|
+
(vendor names, `Alexa Mini`-style renames, self-denials) and `AmnesiaGuard`
|
|
640
|
+
(*"I can't remember you"* while the database holds facts about you). Both
|
|
641
|
+
rewrite from data already in hand, add no latency and make no extra API call.
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Database schema
|
|
646
|
+
|
|
647
|
+
Eight tables, created automatically. Full DDL in
|
|
648
|
+
[`src/db/schema.sql`](src/db/schema.sql); the migration is idempotent.
|
|
649
|
+
|
|
650
|
+
| table | purpose |
|
|
651
|
+
| -------------------- | ------------------------------------------------------------------ |
|
|
652
|
+
| `wa_users` | one row per person; canonical `jid`, push name, counters, block flag |
|
|
653
|
+
| `wa_user_identities` | every address a person is seen under → one `user_id` (`@lid` ↔ phone) |
|
|
654
|
+
| `wa_groups` | one row per group; subject, per-group AI on/off switch |
|
|
655
|
+
| `wa_group_members` | which user was seen in which group (per-room stats, admin flag) |
|
|
656
|
+
| `wa_conversations` | one thread per DM / per (group, user); `context_key` unique |
|
|
657
|
+
| `wa_messages` | transcript; de-duplicated by `(conversation_id, wa_message_id)` |
|
|
658
|
+
| `wa_memories` | long-term facts, `UNIQUE (user_id, key)`, optional `expires_at` |
|
|
659
|
+
| `wa_ai_usage` | audit log: model, latency, ok/error |
|
|
660
|
+
|
|
661
|
+
Indexes cover the hot paths (newest-N messages per thread, memories per user,
|
|
662
|
+
recently active users). `updated_at` columns are maintained by triggers.
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## DeepAI API reference
|
|
667
|
+
|
|
668
|
+
`DeepAIClient` implements every route used by the deepai.org web client. All
|
|
669
|
+
of them are reachable as `ai.deepai.*`, and the paths live in one overridable
|
|
670
|
+
map (`endpoints`), so a rename on DeepAI's side is a configuration change.
|
|
671
|
+
|
|
672
|
+
| area | route | client method |
|
|
673
|
+
| ---- | ----- | ------------- |
|
|
674
|
+
| chat | `POST /hacking_is_a_serious_crime` | `chat()` / `chatDetailed()` |
|
|
675
|
+
| reasoning tasks | `GET /check_chat_task_status?type=&task_id=` | `taskStatus()` / `waitForTask()` |
|
|
676
|
+
| moderation score | `GET /check-sensitivity?request_id=` | `checkSensitivity()` |
|
|
677
|
+
| attachments | `POST /chat_attachments/upload` | `uploadAttachment()` |
|
|
678
|
+
| attachments | `GET /chat_attachments/get?uuid=` | `getAttachment()` |
|
|
679
|
+
| sessions | `POST /save_chat_session` | `saveSession()` |
|
|
680
|
+
| sessions | `GET /get_chat_session?uuid=` | `getSession()` |
|
|
681
|
+
| sessions | `POST /rename_chat_session` | `renameSession()` |
|
|
682
|
+
| sessions | `POST /delete_chat_session` | `deleteSession()` |
|
|
683
|
+
| sessions | `POST /delete_all_chat_history` | `deleteAllSessions()` |
|
|
684
|
+
| account memory | `GET/POST /chat_memory` | `chatMemory()` |
|
|
685
|
+
| agent mode | `GET/POST /chat_sandbox` | `chatSandbox()` |
|
|
686
|
+
| background tasks | `GET/POST /chat_concierge` | `chatConcierge()` |
|
|
687
|
+
| abuse report | `POST /report_character` | `reportCharacter()` |
|
|
688
|
+
| image generation | `POST /api/text2img` | `text2img()` |
|
|
689
|
+
| image editing | `POST /api/image-editor` | `editImage()` |
|
|
690
|
+
| upscaling | `POST /api/torch-srgan` | `upscaleImage()` |
|
|
691
|
+
| colourising | `POST /api/colorizer` | `colorizeImage()` |
|
|
692
|
+
| moderation | `POST /api/nsfw-detector` | `detectNsfw()` |
|
|
693
|
+
| summarising | `POST /api/summarization` | `summarize()` |
|
|
694
|
+
| anything else | `POST /api/<name>` | `runApi(name, fields)` |
|
|
695
|
+
|
|
696
|
+
### The chat request
|
|
697
|
+
|
|
698
|
+
```http
|
|
699
|
+
POST https://api.deepai.org/hacking_is_a_serious_crime
|
|
700
|
+
api-key: tryit-...
|
|
701
|
+
Origin: https://deepai.org
|
|
702
|
+
Content-Type: multipart/form-data
|
|
703
|
+
|
|
704
|
+
chat_style = chat
|
|
705
|
+
chatHistory = [{"role":"user","content":"..."}]
|
|
706
|
+
model = standard
|
|
707
|
+
session_uuid = <uuid v4>
|
|
708
|
+
tool_activity_support = 1
|
|
709
|
+
thinking_image_tool_support= 1
|
|
710
|
+
enabled_tools = ["image_generator","image_editor"]
|
|
711
|
+
attachment_uuids = ["..."] (top level — never inside a message)
|
|
712
|
+
memory_enabled = true|false
|
|
713
|
+
web_access_enabled = true|false
|
|
714
|
+
sandbox_enabled = true|false (+ sandbox_turn_id)
|
|
715
|
+
concierge_enabled = true|false
|
|
716
|
+
thinking_support = 1 (-> {"task_id"} + polling)
|
|
717
|
+
hacker_is_stinky = very_stinky
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### The chat response
|
|
721
|
+
|
|
722
|
+
The body is streamed UTF-8 text with out-of-band packets embedded in it.
|
|
723
|
+
`StreamParser` separates them so none of this reaches WhatsApp:
|
|
724
|
+
|
|
725
|
+
```
|
|
726
|
+
\u001C{"tool_activity":"Searching the web"}\u001C tool status pings
|
|
727
|
+
…answer…\u001C[{"title":…,"url":…}] web-search sources
|
|
728
|
+
…answer…\u001C{"type":"generated_image","share_url":…} generated image
|
|
729
|
+
\u001DTHINKING_START12s\u001E<chain of thought>\u001DTHINKING_END
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### Observed behaviour worth knowing
|
|
733
|
+
|
|
734
|
+
- The response is **streamed text**, not JSON and not SSE.
|
|
735
|
+
- Refusals arrive as a short JSON body — `{"status": "Only paid accounts can
|
|
736
|
+
use genius"}`, `{"status": "Out of API credits"}` — often with **HTTP 200**.
|
|
737
|
+
The client treats these as errors; quota refusals rotate to the next
|
|
738
|
+
configured key before the model chain is tried.
|
|
739
|
+
- **`role: "system"` is ignored** by the chat endpoint. The persona is
|
|
740
|
+
therefore delivered as a priming user/assistant pair, which the API does
|
|
741
|
+
honour; a short system digest is sent as well for backends that respect it.
|
|
742
|
+
- Anonymous `tryit-` keys are limited to `standard` and `gpt-4o-mini`.
|
|
743
|
+
Requesting `gpt-4.1`, `claude-*` or Genius returns a paid-account refusal.
|
|
744
|
+
- **Any request carrying an attachment is routed to a text-only model on free
|
|
745
|
+
keys**, whatever the `model` field says — we confirmed this by requesting
|
|
746
|
+
non-existent model names, matched browser headers, signed-in cookies and the
|
|
747
|
+
full browser field set; every variant resolved to
|
|
748
|
+
`llama-3.1-8b-instruct-turbo`, while the same key routes text-only requests
|
|
749
|
+
to `gpt-4o-mini` correctly. DeepAI states the reason in the reply itself:
|
|
750
|
+
*"neither native vision nor document text extraction added their contents to
|
|
751
|
+
this model request."* Document extraction (`.txt`, `.pdf`, …) does work on
|
|
752
|
+
free keys, which is why documents are provider #0 in the vision chain.
|
|
753
|
+
- `attachment_uuids` **must be a top-level form field**. Placing it inside a
|
|
754
|
+
message object forces the text-only downgrade even on paid keys.
|
|
755
|
+
- `/chat_attachments/upload` rejects the `api-key` header but succeeds
|
|
756
|
+
anonymously **with** an `Origin` header.
|
|
757
|
+
- Message `content` must be a **plain string**. OpenAI-style array content
|
|
758
|
+
(`[{type:'text'},{type:'image_url'}]`) is rejected with HTTP 500.
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## Troubleshooting
|
|
763
|
+
|
|
764
|
+
**`TypeError: getEngine(...).generateImage is not a function`** (or
|
|
765
|
+
`.searchWeb`, `.upscaleImage`, …)
|
|
766
|
+
The copy of `alexa-ai` in your bot's `node_modules` is older than the wrapper
|
|
767
|
+
expects. Because the package is installed from GitHub, `npm install` does not
|
|
768
|
+
refresh it automatically. Reinstall and verify:
|
|
769
|
+
|
|
770
|
+
```bash
|
|
771
|
+
npm uninstall alexa-ai
|
|
772
|
+
npm install github:AlexaInc/deepai
|
|
773
|
+
node -e "console.log(require('alexa-ai').version)" # must print 2.1.1 or newer
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
`AlexaAI.version` and `AlexaAI.methods()` let the bot assert this at startup;
|
|
777
|
+
`examples/bot-ai.js` does so in `assertEngineVersion()`.
|
|
778
|
+
|
|
779
|
+
**`searchWeb()` answers are short, or `sources` is empty while links appear in
|
|
780
|
+
`text`**
|
|
781
|
+
Fixed in 2.1.1. Earlier builds asked the model for *"a short, direct answer"*
|
|
782
|
+
and only read sources from DeepAI's structured packet, which `gpt-4o-mini`
|
|
783
|
+
does not send. Reinstall as above and confirm the version is 2.1.1 or newer.
|
|
784
|
+
If a particular model still answers briefly, check `attempts` / `words` in the
|
|
785
|
+
result: the engine retries once below `minWords`, and a persistently short
|
|
786
|
+
model is best swapped with the `model` option.
|
|
787
|
+
|
|
788
|
+
**`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
|
|
789
|
+
Both routes were refused: `/api/text2img` needs credits and the in-chat image
|
|
790
|
+
tool hit the key's chat quota. Add more keys (`keys: [...]`) or wait for the
|
|
791
|
+
quota to reset. `message` carries DeepAI's exact wording.
|
|
792
|
+
|
|
793
|
+
**Photos are answered with "I can't view images right now"**
|
|
794
|
+
Native vision needs a paid DeepAI key; on a free key only text inside the
|
|
795
|
+
image can be read (OCR). Set your own `OCR_API_KEY` if the shared demo key is
|
|
796
|
+
rate-limited. Documents (`.txt`, `.pdf`, `.docx`) are extracted server-side
|
|
797
|
+
and work on free keys.
|
|
798
|
+
|
|
799
|
+
**Alexa recognises someone in a DM but not in a group**
|
|
800
|
+
Only one of the person's addresses is being passed. Supply both on group
|
|
801
|
+
messages (`userId: key.participant`, `aliases: [key.participantAlt]`) or call
|
|
802
|
+
`linkIdentity()` once when you learn the mapping.
|
|
803
|
+
|
|
804
|
+
**`Only paid accounts can use …`**
|
|
805
|
+
The configured `model` is not available on the key. The engine falls through
|
|
806
|
+
`fallbackModels`; keep `'standard'` in that list.
|
|
807
|
+
|
|
808
|
+
**`Cannot connect to PostgreSQL`**
|
|
809
|
+
Managed providers usually require SSL. The engine enables relaxed SSL
|
|
810
|
+
automatically for non-local hosts; pass `ssl: { rejectUnauthorized: true }`
|
|
811
|
+
(or a CA bundle) to enforce verification, or `ssl: false` to disable it.
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Testing
|
|
816
|
+
|
|
817
|
+
```bash
|
|
818
|
+
npm test # no network, no database
|
|
819
|
+
POSTGRES_URL=postgres://... npm test # + live PostgreSQL
|
|
820
|
+
POSTGRES_URL=... DEEPAI_KEY=tryit-... node test/run-tests.js # + live DeepAI
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
- `test/run-tests.js` — 217 unit assertions covering jid parsing, alias
|
|
824
|
+
collection, memory extraction from malformed model output, trigger
|
|
825
|
+
matching, formatting enforcement, identity and amnesia repair, the DeepAI
|
|
826
|
+
stream format and the exact request shape of every endpoint (mocked
|
|
827
|
+
transport), plus an end-to-end run of the `chat()` pipeline on a fake
|
|
828
|
+
database and a fake DeepAI. With `POSTGRES_URL` it additionally proves the
|
|
829
|
+
identity model against a real database: a fact learned under a phone jid in
|
|
830
|
+
a DM is readable under the `@lid` in a group, two pre-existing rows merge
|
|
831
|
+
without losing a memory or a transcript, and unrelated users stay isolated.
|
|
832
|
+
- `test/wrapper-methods.js` — 175 assertions exercising every method exposed
|
|
833
|
+
to the bot (`generateImage`, `searchWeb`, `summarizeText`, the media
|
|
834
|
+
helpers, `describeImage`, `deepaiHealth`, and the memory, identity and
|
|
835
|
+
moderation API) against a mock that behaves like DeepAI's free tier —
|
|
836
|
+
including both ways DeepAI reports web-search sources — plus alias and
|
|
837
|
+
unseen-row cases on a real database when `POSTGRES_URL` is set.
|
|
838
|
+
|
|
839
|
+
392 assertions run offline; 458 with a database.
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
## Examples
|
|
844
|
+
|
|
845
|
+
| file | what it shows |
|
|
846
|
+
| ---- | ------------- |
|
|
847
|
+
| [`examples/bot-ai.js`](examples/bot-ai.js) | complete drop-in AI module for the bot: callback signature, Baileys helper, admin and media commands, startup version check |
|
|
848
|
+
| [`examples/demo.js`](examples/demo.js) | interactive walkthrough — a user introduces themselves in a DM, is recognised in two groups, a second user stays isolated, triggers return exact output |
|
|
849
|
+
|
|
850
|
+
```bash
|
|
851
|
+
POSTGRES_URL=... DEEPAI_KEY=tryit-... node examples/demo.js
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
## Changelog
|
|
857
|
+
|
|
858
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
859
|
+
|
|
860
|
+
## License
|
|
861
|
+
|
|
862
|
+
[ISC](LICENSE) © Hansaka
|