alexa-ai 2.1.1 → 2.1.2
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 +37 -0
- package/README.md +76 -20
- package/index.js +2 -0
- package/package.json +2 -1
- package/src/AlexaAI.js +115 -32
- package/src/core/Config.js +9 -0
- 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 +1035 -0
- package/test/wrapper-methods.js +986 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to `alexa-ai` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.1.2] — 2026-09-06
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **`searchWeb()` now searches the web itself** instead of trusting DeepAI's
|
|
11
|
+
server-side web access. Observed live with `gpt-4o-mini`: the same query
|
|
12
|
+
produced one report without any sources and one with four invented links,
|
|
13
|
+
because the model skipped the search and wrote from memory. The engine now
|
|
14
|
+
queries Bing, Bing News, Wikipedia, Google News and DuckDuckGo in parallel
|
|
15
|
+
(no API key needed; the four feeds were verified live on 2026-09-06 —
|
|
16
|
+
DuckDuckGo's html endpoint serves a bot challenge from data-centre IPs, so
|
|
17
|
+
its lite endpoint is tried first and a challenge page yields no results), hands the results to the model as numbered material, and asks
|
|
18
|
+
it to cite result numbers and never write URLs. The `*Sources:*` block is
|
|
19
|
+
built from the search results only — cited first — so a link the model made
|
|
20
|
+
up can never reach the chat. Citation markers are removed from the prose;
|
|
21
|
+
URLs the model still types are turned into citations when they match a
|
|
22
|
+
result and dropped otherwise.
|
|
23
|
+
- Result gained `grounded` (did the engine's search answer?), `providers`,
|
|
24
|
+
`via` (`'model'` | `'digest'`), and each source carries `date`, `provider`
|
|
25
|
+
and `cited`.
|
|
26
|
+
- New options: `results` (bring your own search API's results), `search:false`,
|
|
27
|
+
`providers`, `maxResults`; constructor options `webSearch`,
|
|
28
|
+
`webSearchProviders`, `webSearchTimeout`, `webSearchResults`,
|
|
29
|
+
`webSearchProvider(query, { maxResults, signal })`.
|
|
30
|
+
- When every provider fails the request falls back to DeepAI's web access as
|
|
31
|
+
before (`grounded:false`); when the search worked but the model call failed,
|
|
32
|
+
a plain digest of the results is returned (`via:'digest'`, still `ok:true`).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- `WebSearch` service (exported) with parsers for the DuckDuckGo html/lite
|
|
36
|
+
pages, RSS 2.0 news feeds and the MediaWiki search API, redirect unwrapping
|
|
37
|
+
(`duckduckgo.com/l/?uddg=`, `bing.com/news/apiclick.aspx?url=`), ad and
|
|
38
|
+
duplicate filtering, and provider round-robin.
|
|
39
|
+
- `WebAnswer.formatResults()`, `extractCitations()`, `stripUrls()`, `digest()`.
|
|
40
|
+
- The npm package now ships the test suite (`test/`), so the published
|
|
41
|
+
tarball is a complete snapshot of the repository at the tagged version:
|
|
42
|
+
`npm explore alexa-ai -- npm test`.
|
|
43
|
+
|
|
7
44
|
## [2.1.1] — 2026-09-06
|
|
8
45
|
|
|
9
46
|
### Fixed
|
package/README.md
CHANGED
|
@@ -439,11 +439,16 @@ which works on free chat keys. The result reports the route that answered
|
|
|
439
439
|
**`summarizeText()`** follows the same pattern — `/api/summarization` first, a
|
|
440
440
|
stateless chat request as the fallback.
|
|
441
441
|
|
|
442
|
-
**`searchWeb()`** is a one-off research request
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
442
|
+
**`searchWeb()`** is a one-off research request that touches no user's
|
|
443
|
+
memory or history, so it can be called with no jid at all. The engine does the
|
|
444
|
+
searching itself — Bing, Bing News, Wikipedia, Google News and DuckDuckGo, in
|
|
445
|
+
parallel, no API key needed — and hands the results to the model as numbered
|
|
446
|
+
material. The model writes the report from them and cites result numbers; it
|
|
447
|
+
is told never to write a URL. The `*Sources:*` block is then built from the
|
|
448
|
+
search results only (cited ones first), so every link the bot shows is a page
|
|
449
|
+
that actually came back from a search, never one the model made up. The
|
|
450
|
+
default answer is long-form and ready to send to WhatsApp — an intro, three to
|
|
451
|
+
five `*Heading:*` sections with numbered `*Title*: detail` points, and one
|
|
447
452
|
`*Sources:*` block at the end:
|
|
448
453
|
|
|
449
454
|
```
|
|
@@ -471,15 +476,25 @@ answer still comes back under `minWords` (default 150), one follow-up turn asks
|
|
|
471
476
|
the model to rewrite it in full, and the longer reply wins — `attempts` and
|
|
472
477
|
`words` in the result show what happened.
|
|
473
478
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
479
|
+
The result tells you which path answered:
|
|
480
|
+
|
|
481
|
+
```js
|
|
482
|
+
const r = await ai.searchWeb('coffee');
|
|
483
|
+
// r.grounded true → the engine searched; sources are real search results
|
|
484
|
+
// false → no search results (host offline / providers blocked);
|
|
485
|
+
// DeepAI's own web access answered, and its sources are
|
|
486
|
+
// whatever the model reported — treat them with care
|
|
487
|
+
// r.providers ['bing', 'bing-news', 'wikipedia', 'google-news', 'duckduckgo'] — who answered
|
|
488
|
+
// r.sources [{ title, url, description, date, provider, cited }]
|
|
489
|
+
// r.via 'model' | 'digest' (the model failed but the search worked:
|
|
490
|
+
// a plain list of the results is returned)
|
|
491
|
+
// r.words, r.attempts, r.model
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Sentences in which the model talks about itself — *"I'm a language model"*,
|
|
495
|
+
*"I can't browse the web"*, *"based on my training data"* — are removed, and
|
|
496
|
+
leftover template placeholders are dropped. Third-party names in the research
|
|
497
|
+
itself (news about OpenAI or Google) are kept verbatim.
|
|
483
498
|
|
|
484
499
|
```js
|
|
485
500
|
await ai.searchWeb('coffee', {
|
|
@@ -489,9 +504,37 @@ await ai.searchWeb('coffee', {
|
|
|
489
504
|
maxSources: 3, // how many to list in `text` (the array is not capped)
|
|
490
505
|
language: 'Sinhala', // answer language (default: the language of the query)
|
|
491
506
|
instructions: 'focus on Sri Lanka',
|
|
507
|
+
providers: ['bing-news', 'google-news'], // only news for this call
|
|
508
|
+
maxResults: 5, // results handed to the model (default 8)
|
|
509
|
+
search: false, // skip the built-in search, use DeepAI's web access
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Bring your own search API (Brave, Serper, Tavily, …): pass its results and
|
|
513
|
+
// the built-in search is skipped — the sources block is built from them.
|
|
514
|
+
await ai.searchWeb('coffee', {
|
|
515
|
+
results: [{ title, url, description, date }],
|
|
492
516
|
});
|
|
493
517
|
```
|
|
494
518
|
|
|
519
|
+
Engine-wide settings in the constructor:
|
|
520
|
+
|
|
521
|
+
```js
|
|
522
|
+
new AlexaAI({
|
|
523
|
+
webSearch: true, // false = never search; always use DeepAI's
|
|
524
|
+
webSearchProviders: ['bing', 'bing-news', 'wikipedia', 'google-news', 'duckduckgo'],
|
|
525
|
+
webSearchTimeout: 8000, // per provider, ms
|
|
526
|
+
webSearchResults: 8, // results handed to the model
|
|
527
|
+
webSearchProvider: async (query, { maxResults, signal }) => [...], // replace the built-ins
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
The built-in providers are public pages and feeds, so they can change or be
|
|
532
|
+
rate-limited; each one is best-effort and a failure just means fewer results.
|
|
533
|
+
When none return anything the request falls back to DeepAI's server-side web
|
|
534
|
+
access — which on the free models is exactly the behaviour that motivated this
|
|
535
|
+
design: it often skips the search and writes a plausible report with invented
|
|
536
|
+
links. Check `grounded` if that matters to you.
|
|
537
|
+
|
|
495
538
|
### Moderation and administration
|
|
496
539
|
|
|
497
540
|
```js
|
|
@@ -601,7 +644,8 @@ AlexaAI orchestrator; the only class the bot touches
|
|
|
601
644
|
│ no "Alexa Mini", no self-denial)
|
|
602
645
|
├── AmnesiaGuard never lets her deny a memory she actually has
|
|
603
646
|
├── ImageDescriber vision chain: documents -> DeepAI -> OCR -> fallback
|
|
604
|
-
├──
|
|
647
|
+
├── WebSearch the engine's own web search (Bing, Bing/Google News, Wikipedia, DuckDuckGo)
|
|
648
|
+
├── WebAnswer searchWeb prompt, citation handling, sources block
|
|
605
649
|
├── Media normalises every media input shape
|
|
606
650
|
└── JidParser normalises @lid / @s.whatsapp.net / @g.us
|
|
607
651
|
```
|
|
@@ -770,7 +814,7 @@ refresh it automatically. Reinstall and verify:
|
|
|
770
814
|
```bash
|
|
771
815
|
npm uninstall alexa-ai
|
|
772
816
|
npm install github:AlexaInc/deepai
|
|
773
|
-
node -e "console.log(require('alexa-ai').version)" # must print 2.1.
|
|
817
|
+
node -e "console.log(require('alexa-ai').version)" # must print 2.1.2 or newer
|
|
774
818
|
```
|
|
775
819
|
|
|
776
820
|
`AlexaAI.version` and `AlexaAI.methods()` let the bot assert this at startup;
|
|
@@ -780,10 +824,22 @@ node -e "console.log(require('alexa-ai').version)" # must print 2.1.1 or newer
|
|
|
780
824
|
`text`**
|
|
781
825
|
Fixed in 2.1.1. Earlier builds asked the model for *"a short, direct answer"*
|
|
782
826
|
and only read sources from DeepAI's structured packet, which `gpt-4o-mini`
|
|
783
|
-
does not send.
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
827
|
+
does not send. If a particular model still answers briefly, check `attempts` /
|
|
828
|
+
`words` in the result: the engine retries once below `minWords`, and a
|
|
829
|
+
persistently short model is best swapped with the `model` option.
|
|
830
|
+
|
|
831
|
+
**`searchWeb()` sometimes has no sources, or the sources are links that do
|
|
832
|
+
not exist**
|
|
833
|
+
Fixed in 2.1.2. Before, the model was both researcher and writer: DeepAI's
|
|
834
|
+
server-side search ran unreliably on the free models, so the model often
|
|
835
|
+
wrote from memory — one run had no sources, the next had four invented ones.
|
|
836
|
+
The engine now searches the web itself and the sources block is built only
|
|
837
|
+
from those results. If `sources` is still empty, look at `grounded` in the
|
|
838
|
+
result: `false` means none of the search providers answered from your host
|
|
839
|
+
(firewall, DNS, rate limit — enable `debug:true` to see each provider's
|
|
840
|
+
error), and the reply came from DeepAI's own web access. Point
|
|
841
|
+
`webSearchProvider` at a search API you control if the public endpoints are
|
|
842
|
+
blocked where the bot runs.
|
|
787
843
|
|
|
788
844
|
**`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
|
|
789
845
|
Both routes were refused: `/api/text2img` needs credits and the in-chat image
|
package/index.js
CHANGED
|
@@ -33,6 +33,7 @@ const ResponseFormatter = require('./src/services/ResponseFormatter');
|
|
|
33
33
|
const TriggerDetector = require('./src/services/TriggerDetector');
|
|
34
34
|
const ImageDescriber = require('./src/services/ImageDescriber');
|
|
35
35
|
const WebAnswer = require('./src/services/WebAnswer');
|
|
36
|
+
const WebSearch = require('./src/services/WebSearch');
|
|
36
37
|
const JidParser = require('./src/utils/JidParser');
|
|
37
38
|
const Media = require('./src/utils/Media');
|
|
38
39
|
const StreamParser = require('./src/core/StreamParser');
|
|
@@ -63,6 +64,7 @@ module.exports.ResponseFormatter = ResponseFormatter;
|
|
|
63
64
|
module.exports.TriggerDetector = TriggerDetector;
|
|
64
65
|
module.exports.ImageDescriber = ImageDescriber;
|
|
65
66
|
module.exports.WebAnswer = WebAnswer;
|
|
67
|
+
module.exports.WebSearch = WebSearch;
|
|
66
68
|
module.exports.JidParser = JidParser;
|
|
67
69
|
module.exports.Media = Media;
|
|
68
70
|
module.exports.StreamParser = StreamParser;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexa-ai",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "AI engine for the Alexa WhatsApp bot: DeepAI-powered chat with PostgreSQL-backed long-term memory, cross-chat identity (@lid <-> phone), vision/OCR, image generation and web search.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"index.js",
|
|
41
41
|
"src/",
|
|
42
42
|
"examples/",
|
|
43
|
+
"test/",
|
|
43
44
|
"README.md",
|
|
44
45
|
"CHANGELOG.md",
|
|
45
46
|
"LICENSE"
|
package/src/AlexaAI.js
CHANGED
|
@@ -17,6 +17,7 @@ const IdentityResolver = require('./services/IdentityResolver');
|
|
|
17
17
|
const TriggerDetector = require('./services/TriggerDetector');
|
|
18
18
|
const ImageDescriber = require('./services/ImageDescriber');
|
|
19
19
|
const WebAnswer = require('./services/WebAnswer');
|
|
20
|
+
const WebSearch = require('./services/WebSearch');
|
|
20
21
|
const StreamParser = require('./core/StreamParser');
|
|
21
22
|
const JidParser = require('./utils/JidParser');
|
|
22
23
|
const Media = require('./utils/Media');
|
|
@@ -69,6 +70,7 @@ class AlexaAI {
|
|
|
69
70
|
|
|
70
71
|
this.prompts = new PromptBuilder(this.config);
|
|
71
72
|
this.vision = new ImageDescriber(this.client, this.config);
|
|
73
|
+
this.webSearch = new WebSearch(this.config);
|
|
72
74
|
|
|
73
75
|
// Persona-aware guards (renaming the assistant renames these too).
|
|
74
76
|
this.identityGuard = new IdentityGuard({
|
|
@@ -785,25 +787,35 @@ class AlexaAI {
|
|
|
785
787
|
* One-off, stateless web research request — for "search the web for X"
|
|
786
788
|
* commands that must not touch anyone's memory.
|
|
787
789
|
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
790
|
+
* HOW IT WORKS
|
|
791
|
+
* ------------
|
|
792
|
+
* 1. The engine searches the web itself (`WebSearch`: DuckDuckGo, Bing
|
|
793
|
+
* News, Google News, Wikipedia — no API key needed) and collects real
|
|
794
|
+
* pages with titles, snippets and dates.
|
|
795
|
+
* 2. Those results go to the model as numbered material. The model writes
|
|
796
|
+
* the report from them and cites result numbers; it is told never to
|
|
797
|
+
* write a URL.
|
|
798
|
+
* 3. The `*Sources:*` block is built from the search results only —
|
|
799
|
+
* cited ones first. A URL the model wrote itself is never shown.
|
|
794
800
|
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
801
|
+
* When the search returns nothing (offline host, all providers blocked)
|
|
802
|
+
* the request falls back to DeepAI's server-side web access, which is
|
|
803
|
+
* unreliable on free models: it may skip the search and invent pages.
|
|
804
|
+
* `grounded` in the result tells you which path answered.
|
|
798
805
|
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
806
|
+
* Default output is long-form: an intro, three to five `*Heading:*`
|
|
807
|
+
* sections with numbered `*Headline*: detail` points (about 300–450
|
|
808
|
+
* words), then one `*Sources:*` block. A long-form reply under `minWords`
|
|
809
|
+
* is retried once; the longer reply wins.
|
|
803
810
|
*
|
|
804
811
|
* @param {string} query
|
|
805
812
|
* @param {object} [opts]
|
|
806
813
|
* @param {'long'|'short'} [opts.detail='long'] `short` = 2–4 sentences
|
|
814
|
+
* @param {Array<{title?:string,url:string,description?:string,date?:string}>} [opts.results]
|
|
815
|
+
* results from the host application's own search API (skips the built-in search)
|
|
816
|
+
* @param {boolean} [opts.search=true] `false` = skip the built-in search, use DeepAI's
|
|
817
|
+
* @param {string[]} [opts.providers] subset of WebSearch.PROVIDERS for this call
|
|
818
|
+
* @param {number} [opts.maxResults] results handed to the model (default config.webSearchResults)
|
|
807
819
|
* @param {number} [opts.minWords=150] long form only: retry once below this (0 disables)
|
|
808
820
|
* @param {boolean} [opts.includeSources=true] append the *Sources:* block to `text`
|
|
809
821
|
* @param {number} [opts.maxSources=5] sources listed in `text` (the array is not capped)
|
|
@@ -812,12 +824,12 @@ class AlexaAI {
|
|
|
812
824
|
* @param {string} [opts.model]
|
|
813
825
|
* @param {string} [opts.userName]
|
|
814
826
|
* @param {AbortSignal} [opts.signal]
|
|
815
|
-
* @returns {Promise<{ok:boolean, text:string, answer:string, sources:Array<{title:string|null,url:string|null,description:string|null}>, words:number, attempts:number, model?:string|null, error?:string, message?:string}>}
|
|
827
|
+
* @returns {Promise<{ok:boolean, text:string, answer:string, sources:Array<{title:string|null,url:string|null,description:string|null,date?:string|null,provider?:string,cited?:boolean}>, grounded:boolean, providers:string[], words:number, attempts:number, via:'model'|'digest', model?:string|null, error?:string, message?:string}>}
|
|
816
828
|
*/
|
|
817
829
|
async searchWeb(query, opts = {}) {
|
|
818
830
|
const question = String(query ?? '').trim();
|
|
819
831
|
const failure = (error, message, extra = {}) => ({
|
|
820
|
-
ok: false, text: '', answer: '', sources: [], words: 0, attempts: 0, error, message, ...extra,
|
|
832
|
+
ok: false, text: '', answer: '', sources: [], grounded: false, providers: [], words: 0, attempts: 0, via: 'model', error, message, ...extra,
|
|
821
833
|
});
|
|
822
834
|
if (!question) return failure('VALIDATION_ERROR', 'searchWeb(): query is required');
|
|
823
835
|
|
|
@@ -826,18 +838,48 @@ class AlexaAI {
|
|
|
826
838
|
const maxSources = Number.isFinite(opts.maxSources) ? opts.maxSources : 5;
|
|
827
839
|
const minWords = detail === 'long' && Number.isFinite(opts.minWords) ? Math.max(0, opts.minWords) : detail === 'long' ? 150 : 0;
|
|
828
840
|
|
|
829
|
-
|
|
841
|
+
// ---- 1. search ------------------------------------------------------
|
|
842
|
+
let results = WebSearch.normalise(opts.results, 'caller');
|
|
843
|
+
let providers = results.length ? ['caller'] : [];
|
|
844
|
+
if (!results.length && opts.search !== false) {
|
|
845
|
+
const found = await this.webSearch.search(question, {
|
|
846
|
+
providers: opts.providers,
|
|
847
|
+
maxResults: opts.maxResults,
|
|
848
|
+
signal: opts.signal,
|
|
849
|
+
});
|
|
850
|
+
results = found.results;
|
|
851
|
+
providers = found.providers;
|
|
852
|
+
if (!results.length && this.config.debug) {
|
|
853
|
+
this.log.debug?.(
|
|
854
|
+
`[AlexaAI] searchWeb: no web results for "${question}" ` +
|
|
855
|
+
(found.errors.length ? `(${found.errors.map((e) => `${e.provider}: ${e.message}`).join('; ')})` : '') +
|
|
856
|
+
' — falling back to DeepAI web access'
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const grounded = results.length > 0;
|
|
861
|
+
|
|
862
|
+
// ---- 2. ask the model ---------------------------------------------
|
|
863
|
+
const request = { search: !grounded, webAccess: !grounded, model: opts.model, signal: opts.signal };
|
|
830
864
|
const messages = this.prompts.build({
|
|
831
|
-
message: WebAnswer.prompt(question, {
|
|
865
|
+
message: WebAnswer.prompt(question, {
|
|
866
|
+
detail,
|
|
867
|
+
results: grounded ? results : null,
|
|
868
|
+
language: opts.language,
|
|
869
|
+
instructions: opts.instructions,
|
|
870
|
+
}),
|
|
832
871
|
memories: {},
|
|
833
872
|
history: [],
|
|
834
873
|
userName: opts.userName || null,
|
|
835
874
|
});
|
|
836
875
|
|
|
876
|
+
let reply;
|
|
877
|
+
let result;
|
|
878
|
+
let attempts = 0;
|
|
837
879
|
try {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
880
|
+
reply = await this.client.chatDetailed(messages, request);
|
|
881
|
+
result = this._webAnswerFrom(reply, results);
|
|
882
|
+
attempts = 1;
|
|
841
883
|
|
|
842
884
|
if (minWords && result.words < minWords && result.answer) {
|
|
843
885
|
// Second chance: keep the first reply in the transcript so the
|
|
@@ -845,12 +887,12 @@ class AlexaAI {
|
|
|
845
887
|
const retryMessages = [
|
|
846
888
|
...messages,
|
|
847
889
|
{ role: 'assistant', content: reply.text },
|
|
848
|
-
{ role: 'user', content: WebAnswer.expandPrompt(question, result.words) },
|
|
890
|
+
{ role: 'user', content: WebAnswer.expandPrompt(question, result.words, { grounded }) },
|
|
849
891
|
];
|
|
850
892
|
attempts = 2;
|
|
851
893
|
try {
|
|
852
894
|
const second = await this.client.chatDetailed(retryMessages, request);
|
|
853
|
-
const candidate = this._webAnswerFrom(second, result.sources);
|
|
895
|
+
const candidate = this._webAnswerFrom(second, results, result.sources);
|
|
854
896
|
if (candidate.words > result.words) {
|
|
855
897
|
reply = second;
|
|
856
898
|
result = candidate;
|
|
@@ -859,34 +901,75 @@ class AlexaAI {
|
|
|
859
901
|
this.log.debug?.(`[AlexaAI] searchWeb expansion failed, keeping first reply: ${err.message}`);
|
|
860
902
|
}
|
|
861
903
|
}
|
|
862
|
-
|
|
863
|
-
const { answer, sources, words } = result;
|
|
864
|
-
if (!answer && !sources.length) return failure('DEEPAI_EMPTY', 'DeepAI returned no answer', { attempts });
|
|
865
|
-
|
|
866
|
-
const text = WebAnswer.render(answer, sources, { includeSources: includeSources || !answer, maxSources });
|
|
867
|
-
return { ok: true, text, answer, sources, words, attempts, model: reply.model || null };
|
|
868
904
|
} catch (err) {
|
|
869
905
|
this.log.warn?.(`[AlexaAI] searchWeb failed: ${err.message}`);
|
|
870
|
-
return failure(err.code || 'SEARCH_FAILED', err.message);
|
|
906
|
+
if (!grounded) return failure(err.code || 'SEARCH_FAILED', err.message, { attempts });
|
|
907
|
+
// The search worked even though the model did not: show the results.
|
|
908
|
+
const sources = AlexaAI._groundedSources(results, []);
|
|
909
|
+
return {
|
|
910
|
+
ok: true,
|
|
911
|
+
text: WebAnswer.render(WebAnswer.digest(results), sources, { includeSources, maxSources }),
|
|
912
|
+
answer: WebAnswer.digest(results),
|
|
913
|
+
sources,
|
|
914
|
+
grounded,
|
|
915
|
+
providers,
|
|
916
|
+
words: 0,
|
|
917
|
+
attempts,
|
|
918
|
+
via: 'digest',
|
|
919
|
+
model: null,
|
|
920
|
+
error: err.code || 'SEARCH_FAILED',
|
|
921
|
+
message: err.message,
|
|
922
|
+
};
|
|
871
923
|
}
|
|
924
|
+
|
|
925
|
+
// ---- 3. assemble ----------------------------------------------------
|
|
926
|
+
const { answer, sources, words } = result;
|
|
927
|
+
if (!answer && !sources.length) return failure('DEEPAI_EMPTY', 'DeepAI returned no answer', { attempts, grounded, providers });
|
|
928
|
+
|
|
929
|
+
const text = WebAnswer.render(answer, sources, { includeSources: includeSources || !answer, maxSources });
|
|
930
|
+
return { ok: true, text, answer, sources, grounded, providers, words, attempts, via: 'model', model: reply.model || null };
|
|
872
931
|
}
|
|
873
932
|
|
|
874
933
|
/**
|
|
875
934
|
* @private Turn one chat reply into `{ answer, sources, words }`.
|
|
876
|
-
*
|
|
877
|
-
*
|
|
935
|
+
*
|
|
936
|
+
* Grounded (we searched): citation markers `[n]` are removed from the
|
|
937
|
+
* prose and decide the order of the sources; any URL the model wrote is
|
|
938
|
+
* discarded. Ungrounded: DeepAI's web-results packet comes first (it
|
|
939
|
+
* carries descriptions), then whatever the model listed; duplicates
|
|
940
|
+
* collapse by URL.
|
|
878
941
|
*/
|
|
879
|
-
_webAnswerFrom(reply, carried = []) {
|
|
942
|
+
_webAnswerFrom(reply, results = [], carried = []) {
|
|
880
943
|
let formatted = ResponseFormatter.format(MemoryExtractor.strip(reply.text));
|
|
881
944
|
// Keep third-party vendor names: this is research output, not the
|
|
882
945
|
// assistant introducing herself. WebAnswer removes the sentences in
|
|
883
946
|
// which the model talks about *itself*.
|
|
884
947
|
formatted = this.identityGuard.sanitise(formatted, false, { vendors: false });
|
|
885
948
|
const parsed = WebAnswer.parse(formatted);
|
|
949
|
+
|
|
950
|
+
if (results.length) {
|
|
951
|
+
const { text, cited } = WebAnswer.extractCitations(WebAnswer.stripUrls(parsed.text, results), results.length);
|
|
952
|
+
return { answer: text, sources: AlexaAI._groundedSources(results, cited), words: WebAnswer.wordCount(text) };
|
|
953
|
+
}
|
|
886
954
|
const sources = WebAnswer.mergeSources(AlexaAI._sources(reply.webResults), parsed.sources, carried);
|
|
887
955
|
return { answer: parsed.text, sources, words: WebAnswer.wordCount(parsed.text) };
|
|
888
956
|
}
|
|
889
957
|
|
|
958
|
+
/** @private search results as sources: cited ones first, in citation order. */
|
|
959
|
+
static _groundedSources(results, cited) {
|
|
960
|
+
const order = [...cited.map((n) => n - 1), ...results.map((_, i) => i).filter((i) => !cited.includes(i + 1))];
|
|
961
|
+
return order
|
|
962
|
+
.filter((i) => results[i])
|
|
963
|
+
.map((i) => ({
|
|
964
|
+
title: results[i].title || null,
|
|
965
|
+
url: results[i].url,
|
|
966
|
+
description: results[i].description || null,
|
|
967
|
+
date: results[i].date || null,
|
|
968
|
+
provider: results[i].provider || null,
|
|
969
|
+
cited: cited.includes(i + 1),
|
|
970
|
+
}));
|
|
971
|
+
}
|
|
972
|
+
|
|
890
973
|
/** Is DeepAI reachable and is the key still good? */
|
|
891
974
|
async deepaiHealth() {
|
|
892
975
|
const started = Date.now();
|
package/src/core/Config.js
CHANGED
|
@@ -107,6 +107,15 @@ class Config {
|
|
|
107
107
|
]);
|
|
108
108
|
this.imageModel = opts.imageModel || 'text2img';
|
|
109
109
|
|
|
110
|
+
// ---- Engine web search (searchWeb) ----------------------------------
|
|
111
|
+
// The engine searches first and hands real results to the model, so
|
|
112
|
+
// every URL the bot shows comes from a search, never from the model.
|
|
113
|
+
this.webSearch = opts.webSearch !== false;
|
|
114
|
+
this.webSearchProviders = Config._list(opts.webSearchProviders, ['bing', 'bing-news', 'wikipedia', 'google-news', 'duckduckgo']);
|
|
115
|
+
this.webSearchTimeout = Config._int(opts.webSearchTimeout, 8000, 500, 60000);
|
|
116
|
+
this.webSearchResults = Config._int(opts.webSearchResults, 8, 1, 30);
|
|
117
|
+
this.webSearchProvider = typeof opts.webSearchProvider === 'function' ? opts.webSearchProvider : null;
|
|
118
|
+
|
|
110
119
|
// ---- Chat request feature flags (mirrors the deepai.org client) -----
|
|
111
120
|
this.enabledTools = Config._list(opts.enabledTools, ['image_generator', 'image_editor']);
|
|
112
121
|
this.toolActivitySupport = opts.toolActivitySupport !== false;
|