@demigodmode/pi-web-agent 1.0.0 → 1.1.0
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 +15 -0
- package/dist/backends/config.d.ts +17 -0
- package/dist/backends/config.js +65 -0
- package/dist/backends/doctor.js +22 -3
- package/dist/backends/factory.d.ts +13 -1
- package/dist/backends/factory.js +58 -11
- package/dist/commands/web-agent-config.js +21 -4
- package/dist/fetch/firecrawl-fetch.d.ts +3 -1
- package/dist/fetch/firecrawl-fetch.js +7 -2
- package/dist/presentation/fetch-presentation.js +6 -3
- package/dist/presentation/search-presentation.js +5 -2
- package/dist/search/searxng.d.ts +3 -1
- package/dist/search/searxng.js +9 -3
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,21 @@ The format is intentionally simple and release-oriented.
|
|
|
18
18
|
### Breaking
|
|
19
19
|
- None.
|
|
20
20
|
|
|
21
|
+
## [1.1.0] - 2026-05-25
|
|
22
|
+
### Added
|
|
23
|
+
- Added explicit opt-in fallback from SearXNG to DuckDuckGo and Firecrawl to HTTP.
|
|
24
|
+
- Added supported SearXNG and Firecrawl backend options in config.
|
|
25
|
+
- Added env-gated live tests for local SearXNG and Firecrawl instances.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- `/web-agent show` and `/web-agent doctor` now report configured backend fallback/options.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Nothing yet.
|
|
32
|
+
|
|
33
|
+
### Breaking
|
|
34
|
+
- None.
|
|
35
|
+
|
|
21
36
|
## [1.0.0] - 2026-05-08
|
|
22
37
|
### Added
|
|
23
38
|
- Added one-time `pi-web-agent` changelog notices after package updates and `/web-agent changelog` for manual viewing.
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
export type SearxngOptions = {
|
|
2
|
+
categories?: string[];
|
|
3
|
+
language?: string;
|
|
4
|
+
safesearch?: 0 | 1 | 2;
|
|
5
|
+
};
|
|
6
|
+
export type FirecrawlOptions = {
|
|
7
|
+
formats?: string[];
|
|
8
|
+
onlyMainContent?: boolean;
|
|
9
|
+
};
|
|
1
10
|
export type SearchBackendConfig = {
|
|
2
11
|
provider: 'duckduckgo' | 'searxng';
|
|
3
12
|
baseUrl?: string;
|
|
13
|
+
fallback?: 'duckduckgo';
|
|
14
|
+
options?: SearxngOptions;
|
|
4
15
|
};
|
|
5
16
|
export type FetchBackendConfig = {
|
|
6
17
|
provider: 'http' | 'firecrawl';
|
|
7
18
|
baseUrl?: string;
|
|
8
19
|
apiKey?: string;
|
|
20
|
+
fallback?: 'http';
|
|
21
|
+
options?: FirecrawlOptions;
|
|
9
22
|
};
|
|
10
23
|
export type HeadlessBackendConfig = {
|
|
11
24
|
provider: 'local-browser';
|
|
@@ -25,11 +38,15 @@ export type BackendConfigFile = {
|
|
|
25
38
|
search?: {
|
|
26
39
|
provider?: unknown;
|
|
27
40
|
baseUrl?: unknown;
|
|
41
|
+
fallback?: unknown;
|
|
42
|
+
options?: unknown;
|
|
28
43
|
};
|
|
29
44
|
fetch?: {
|
|
30
45
|
provider?: unknown;
|
|
31
46
|
baseUrl?: unknown;
|
|
32
47
|
apiKey?: unknown;
|
|
48
|
+
fallback?: unknown;
|
|
49
|
+
options?: unknown;
|
|
33
50
|
};
|
|
34
51
|
headless?: {
|
|
35
52
|
provider?: unknown;
|
package/dist/backends/config.js
CHANGED
|
@@ -3,6 +3,38 @@ export const DEFAULT_BACKEND_CONFIG = {
|
|
|
3
3
|
fetch: { provider: 'http' },
|
|
4
4
|
headless: { provider: 'local-browser' }
|
|
5
5
|
};
|
|
6
|
+
function extractStringArray(value) {
|
|
7
|
+
if (!Array.isArray(value))
|
|
8
|
+
return undefined;
|
|
9
|
+
const strings = value.filter((item) => typeof item === 'string');
|
|
10
|
+
return strings.length === value.length ? strings : undefined;
|
|
11
|
+
}
|
|
12
|
+
function extractSearxngOptions(value) {
|
|
13
|
+
if (!value || typeof value !== 'object')
|
|
14
|
+
return undefined;
|
|
15
|
+
const raw = value;
|
|
16
|
+
const options = {};
|
|
17
|
+
const categories = extractStringArray(raw.categories);
|
|
18
|
+
if (categories)
|
|
19
|
+
options.categories = categories;
|
|
20
|
+
if (typeof raw.language === 'string')
|
|
21
|
+
options.language = raw.language;
|
|
22
|
+
if (raw.safesearch === 0 || raw.safesearch === 1 || raw.safesearch === 2)
|
|
23
|
+
options.safesearch = raw.safesearch;
|
|
24
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
25
|
+
}
|
|
26
|
+
function extractFirecrawlOptions(value) {
|
|
27
|
+
if (!value || typeof value !== 'object')
|
|
28
|
+
return undefined;
|
|
29
|
+
const raw = value;
|
|
30
|
+
const options = {};
|
|
31
|
+
const formats = extractStringArray(raw.formats);
|
|
32
|
+
if (formats)
|
|
33
|
+
options.formats = formats;
|
|
34
|
+
if (typeof raw.onlyMainContent === 'boolean')
|
|
35
|
+
options.onlyMainContent = raw.onlyMainContent;
|
|
36
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
37
|
+
}
|
|
6
38
|
export function extractBackendConfigOverride(file) {
|
|
7
39
|
const backends = file?.backends;
|
|
8
40
|
const override = {};
|
|
@@ -11,6 +43,13 @@ export function extractBackendConfigOverride(file) {
|
|
|
11
43
|
if (typeof backends.search.baseUrl === 'string') {
|
|
12
44
|
override.search.baseUrl = backends.search.baseUrl;
|
|
13
45
|
}
|
|
46
|
+
if (backends.search.fallback === 'duckduckgo') {
|
|
47
|
+
override.search.fallback = 'duckduckgo';
|
|
48
|
+
}
|
|
49
|
+
const options = extractSearxngOptions(backends.search.options);
|
|
50
|
+
if (options) {
|
|
51
|
+
override.search.options = options;
|
|
52
|
+
}
|
|
14
53
|
}
|
|
15
54
|
if (backends?.fetch?.provider === 'http' || backends?.fetch?.provider === 'firecrawl') {
|
|
16
55
|
override.fetch = { provider: backends.fetch.provider };
|
|
@@ -20,6 +59,13 @@ export function extractBackendConfigOverride(file) {
|
|
|
20
59
|
if (typeof backends.fetch.apiKey === 'string') {
|
|
21
60
|
override.fetch.apiKey = backends.fetch.apiKey;
|
|
22
61
|
}
|
|
62
|
+
if (backends.fetch.fallback === 'http') {
|
|
63
|
+
override.fetch.fallback = 'http';
|
|
64
|
+
}
|
|
65
|
+
const options = extractFirecrawlOptions(backends.fetch.options);
|
|
66
|
+
if (options) {
|
|
67
|
+
override.fetch.options = options;
|
|
68
|
+
}
|
|
23
69
|
}
|
|
24
70
|
if (backends?.headless?.provider === 'local-browser') {
|
|
25
71
|
override.headless = { provider: 'local-browser' };
|
|
@@ -34,6 +80,25 @@ export function validateBackendConfig(config) {
|
|
|
34
80
|
if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
|
|
35
81
|
issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
|
|
36
82
|
}
|
|
83
|
+
if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng') {
|
|
84
|
+
issues.push('search fallback duckduckgo is only supported when search provider is searxng');
|
|
85
|
+
}
|
|
86
|
+
if (config.fetch.fallback === 'http' && config.fetch.provider !== 'firecrawl') {
|
|
87
|
+
issues.push('fetch fallback http is only supported when fetch provider is firecrawl');
|
|
88
|
+
}
|
|
89
|
+
if (config.search.options?.categories && config.search.options.categories.length === 0) {
|
|
90
|
+
issues.push('search options.categories must contain at least one category when provided');
|
|
91
|
+
}
|
|
92
|
+
if (config.search.options?.language !== undefined && !config.search.options.language.trim()) {
|
|
93
|
+
issues.push('search options.language must not be empty when provided');
|
|
94
|
+
}
|
|
95
|
+
if (config.search.options?.safesearch !== undefined &&
|
|
96
|
+
![0, 1, 2].includes(config.search.options.safesearch)) {
|
|
97
|
+
issues.push('search options.safesearch must be 0, 1, or 2 when provided');
|
|
98
|
+
}
|
|
99
|
+
if (config.fetch.options?.formats && config.fetch.options.formats.length === 0) {
|
|
100
|
+
issues.push('fetch options.formats must contain at least one format when provided');
|
|
101
|
+
}
|
|
37
102
|
return issues;
|
|
38
103
|
}
|
|
39
104
|
function mergeSearchConfig(current, override) {
|
package/dist/backends/doctor.js
CHANGED
|
@@ -6,12 +6,25 @@ function withTimeout(timeoutMs) {
|
|
|
6
6
|
function message(error) {
|
|
7
7
|
return error instanceof Error ? error.message : String(error);
|
|
8
8
|
}
|
|
9
|
-
function searxngDoctorUrl(baseUrl) {
|
|
9
|
+
function searxngDoctorUrl(baseUrl, options = {}) {
|
|
10
10
|
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
11
11
|
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
12
12
|
url.searchParams.set('format', 'json');
|
|
13
|
+
if (options.categories?.length)
|
|
14
|
+
url.searchParams.set('categories', options.categories.join(','));
|
|
15
|
+
if (options.language)
|
|
16
|
+
url.searchParams.set('language', options.language);
|
|
17
|
+
if (options.safesearch !== undefined)
|
|
18
|
+
url.searchParams.set('safesearch', String(options.safesearch));
|
|
13
19
|
return url.toString();
|
|
14
20
|
}
|
|
21
|
+
function firecrawlDoctorBody(options = {}) {
|
|
22
|
+
return {
|
|
23
|
+
url: 'https://example.com',
|
|
24
|
+
formats: options.formats ?? ['markdown'],
|
|
25
|
+
...(options.onlyMainContent !== undefined ? { onlyMainContent: options.onlyMainContent } : {})
|
|
26
|
+
};
|
|
27
|
+
}
|
|
15
28
|
export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs = 3_000 } = {}) {
|
|
16
29
|
const lines = [];
|
|
17
30
|
if (config.search.provider === 'duckduckgo') {
|
|
@@ -23,7 +36,7 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
23
36
|
else {
|
|
24
37
|
const timeout = withTimeout(timeoutMs);
|
|
25
38
|
try {
|
|
26
|
-
const response = await fetchImpl(searxngDoctorUrl(config.search.baseUrl), { signal: timeout.signal });
|
|
39
|
+
const response = await fetchImpl(searxngDoctorUrl(config.search.baseUrl, config.search.options), { signal: timeout.signal });
|
|
27
40
|
const json = (await response.json());
|
|
28
41
|
lines.push(response.ok && Array.isArray(json.results)
|
|
29
42
|
? 'search backend: searxng ok'
|
|
@@ -36,6 +49,9 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
36
49
|
timeout.done();
|
|
37
50
|
}
|
|
38
51
|
}
|
|
52
|
+
if (config.search.fallback) {
|
|
53
|
+
lines.push(`search fallback: ${config.search.fallback}`);
|
|
54
|
+
}
|
|
39
55
|
if (config.fetch.provider === 'http') {
|
|
40
56
|
lines.push('fetch backend: http');
|
|
41
57
|
}
|
|
@@ -52,7 +68,7 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
52
68
|
const response = await fetchImpl(new URL('/v1/scrape', config.fetch.baseUrl).toString(), {
|
|
53
69
|
method: 'POST',
|
|
54
70
|
headers,
|
|
55
|
-
body: JSON.stringify(
|
|
71
|
+
body: JSON.stringify(firecrawlDoctorBody(config.fetch.options)),
|
|
56
72
|
signal: timeout.signal
|
|
57
73
|
});
|
|
58
74
|
lines.push(response.ok ? 'fetch backend: firecrawl ok' : `fetch backend: firecrawl warning (HTTP ${response.status})`);
|
|
@@ -64,5 +80,8 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
64
80
|
timeout.done();
|
|
65
81
|
}
|
|
66
82
|
}
|
|
83
|
+
if (config.fetch.fallback) {
|
|
84
|
+
lines.push(`fetch fallback: ${config.fetch.fallback}`);
|
|
85
|
+
}
|
|
67
86
|
return lines;
|
|
68
87
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
|
|
2
|
+
import { createSearxngSearchTool } from '../search/searxng.js';
|
|
3
|
+
import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
|
|
4
|
+
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
5
|
+
import { createWebSearchTool } from '../tools/web-search.js';
|
|
1
6
|
import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
|
|
2
7
|
import { type BackendConfig } from './config.js';
|
|
3
8
|
export type BackendSet = {
|
|
@@ -11,4 +16,11 @@ export type BackendSet = {
|
|
|
11
16
|
url: string;
|
|
12
17
|
}) => Promise<WebFetchHeadlessResponse>;
|
|
13
18
|
};
|
|
14
|
-
export
|
|
19
|
+
export type BackendFactoryDeps = {
|
|
20
|
+
createDuckDuckGoSearch?: typeof createWebSearchTool;
|
|
21
|
+
createSearxngSearch?: typeof createSearxngSearchTool;
|
|
22
|
+
createHttpFetch?: typeof createWebFetchTool;
|
|
23
|
+
createFirecrawlFetch?: typeof createFirecrawlFetcher;
|
|
24
|
+
createHeadlessFetch?: typeof createWebFetchHeadlessTool;
|
|
25
|
+
};
|
|
26
|
+
export declare function createBackendSet(config?: BackendConfig, deps?: BackendFactoryDeps): BackendSet;
|
package/dist/backends/factory.js
CHANGED
|
@@ -34,25 +34,72 @@ function invalidFirecrawlFetch() {
|
|
|
34
34
|
return { ...result, presentation: buildFetchPresentation(result) };
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
function withSearchFallback(primary, fallback) {
|
|
38
|
+
return async (input) => {
|
|
39
|
+
const first = await primary(input);
|
|
40
|
+
if (first.status !== 'error')
|
|
41
|
+
return first;
|
|
42
|
+
const second = await fallback(input);
|
|
43
|
+
const result = {
|
|
44
|
+
...second,
|
|
45
|
+
metadata: {
|
|
46
|
+
...second.metadata,
|
|
47
|
+
fallbackFrom: 'searxng',
|
|
48
|
+
fallbackReason: first.error?.message ?? 'SearXNG search failed.'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function withFetchFallback(primary, fallback) {
|
|
55
|
+
return async (input) => {
|
|
56
|
+
const first = await primary(input);
|
|
57
|
+
if (first.status !== 'error' && first.status !== 'needs_headless')
|
|
58
|
+
return first;
|
|
59
|
+
const second = await fallback(input);
|
|
60
|
+
const result = {
|
|
61
|
+
...second,
|
|
62
|
+
metadata: {
|
|
63
|
+
...second.metadata,
|
|
64
|
+
fallbackFrom: 'firecrawl',
|
|
65
|
+
fallbackReason: first.error?.message ?? 'Firecrawl fetch failed.'
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return { ...result, presentation: buildFetchPresentation(result) };
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
|
|
72
|
+
const createDuckDuckGoSearch = deps.createDuckDuckGoSearch ?? createWebSearchTool;
|
|
73
|
+
const createSearxngSearch = deps.createSearxngSearch ?? createSearxngSearchTool;
|
|
74
|
+
const createHttpFetch = deps.createHttpFetch ?? createWebFetchTool;
|
|
75
|
+
const createFirecrawlFetch = deps.createFirecrawlFetch ?? createFirecrawlFetcher;
|
|
76
|
+
const createHeadlessFetch = deps.createHeadlessFetch ?? createWebFetchHeadlessTool;
|
|
77
|
+
let search = config.search.provider === 'searxng'
|
|
39
78
|
? config.search.baseUrl
|
|
40
|
-
?
|
|
79
|
+
? createSearxngSearch({ baseUrl: config.search.baseUrl, options: config.search.options })
|
|
41
80
|
: invalidSearxngSearch()
|
|
42
|
-
:
|
|
43
|
-
|
|
81
|
+
: createDuckDuckGoSearch();
|
|
82
|
+
if (config.search.provider === 'searxng' && config.search.fallback === 'duckduckgo') {
|
|
83
|
+
search = withSearchFallback(search, createDuckDuckGoSearch());
|
|
84
|
+
}
|
|
85
|
+
const httpFetch = createHttpFetch();
|
|
86
|
+
let fetchPage = config.fetch.provider === 'firecrawl'
|
|
44
87
|
? config.fetch.baseUrl
|
|
45
|
-
?
|
|
46
|
-
fetchPage:
|
|
88
|
+
? createHttpFetch({
|
|
89
|
+
fetchPage: createFirecrawlFetch({
|
|
47
90
|
baseUrl: config.fetch.baseUrl,
|
|
48
|
-
apiKey: config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY
|
|
91
|
+
apiKey: config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY,
|
|
92
|
+
options: config.fetch.options
|
|
49
93
|
})
|
|
50
94
|
})
|
|
51
|
-
:
|
|
52
|
-
:
|
|
95
|
+
: createHttpFetch({ fetchPage: invalidFirecrawlFetch() })
|
|
96
|
+
: httpFetch;
|
|
97
|
+
if (config.fetch.provider === 'firecrawl' && config.fetch.fallback === 'http') {
|
|
98
|
+
fetchPage = withFetchFallback(fetchPage, httpFetch);
|
|
99
|
+
}
|
|
53
100
|
return {
|
|
54
101
|
search,
|
|
55
102
|
fetchPage,
|
|
56
|
-
headlessFetch:
|
|
103
|
+
headlessFetch: createHeadlessFetch()
|
|
57
104
|
};
|
|
58
105
|
}
|
|
@@ -25,16 +25,33 @@ async function defaultCheckTypebox() {
|
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
function formatSearchOptions(config) {
|
|
29
|
+
return [
|
|
30
|
+
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
31
|
+
config.options?.categories?.length ? `categories ${config.options.categories.join(',')}` : undefined,
|
|
32
|
+
config.options?.language ? `language ${config.options.language}` : undefined,
|
|
33
|
+
config.options?.safesearch !== undefined ? `safesearch ${config.options.safesearch}` : undefined
|
|
34
|
+
].filter(Boolean).join(' ');
|
|
35
|
+
}
|
|
36
|
+
function formatFetchOptions(config) {
|
|
37
|
+
return [
|
|
38
|
+
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
39
|
+
config.options?.formats?.length ? `formats ${config.options.formats.join(',')}` : undefined,
|
|
40
|
+
config.options?.onlyMainContent !== undefined ? `onlyMainContent ${config.options.onlyMainContent}` : undefined
|
|
41
|
+
].filter(Boolean).join(' ');
|
|
42
|
+
}
|
|
28
43
|
function formatBackendSummary(config = DEFAULT_BACKEND_CONFIG) {
|
|
29
|
-
const
|
|
44
|
+
const searchSuffix = formatSearchOptions(config.search);
|
|
45
|
+
const fetchSuffix = formatFetchOptions(config.fetch);
|
|
46
|
+
const searchBase = config.search.baseUrl
|
|
30
47
|
? `search: ${config.search.provider} (${config.search.baseUrl})`
|
|
31
48
|
: `search: ${config.search.provider}`;
|
|
32
|
-
const
|
|
49
|
+
const fetchBase = config.fetch.baseUrl
|
|
33
50
|
? `fetch: ${config.fetch.provider} (${config.fetch.baseUrl})`
|
|
34
51
|
: `fetch: ${config.fetch.provider}`;
|
|
35
52
|
return [
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
searchSuffix ? `${searchBase} ${searchSuffix}` : searchBase,
|
|
54
|
+
fetchSuffix ? `${fetchBase} ${fetchSuffix}` : fetchBase,
|
|
38
55
|
`headless: ${config.headless.provider}`
|
|
39
56
|
].join('\n');
|
|
40
57
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { FirecrawlOptions } from '../backends/config.js';
|
|
1
2
|
import type { WebFetchResponse } from '../types.js';
|
|
2
|
-
export declare function createFirecrawlFetcher({ baseUrl, apiKey, fetchImpl }: {
|
|
3
|
+
export declare function createFirecrawlFetcher({ baseUrl, apiKey, options, fetchImpl }: {
|
|
3
4
|
baseUrl: string;
|
|
4
5
|
apiKey?: string;
|
|
6
|
+
options?: FirecrawlOptions;
|
|
5
7
|
fetchImpl?: typeof fetch;
|
|
6
8
|
}): (url: string) => Promise<WebFetchResponse>;
|
|
@@ -4,17 +4,22 @@ function buildScrapeUrl(baseUrl) {
|
|
|
4
4
|
function errorMessage(error) {
|
|
5
5
|
return error instanceof Error ? error.message : String(error);
|
|
6
6
|
}
|
|
7
|
-
export function createFirecrawlFetcher({ baseUrl, apiKey, fetchImpl = fetch }) {
|
|
7
|
+
export function createFirecrawlFetcher({ baseUrl, apiKey, options, fetchImpl = fetch }) {
|
|
8
8
|
return async function firecrawlFetch(url) {
|
|
9
9
|
try {
|
|
10
10
|
const headers = { 'content-type': 'application/json' };
|
|
11
11
|
if (apiKey) {
|
|
12
12
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
13
13
|
}
|
|
14
|
+
const body = {
|
|
15
|
+
url,
|
|
16
|
+
formats: options?.formats ?? ['markdown'],
|
|
17
|
+
...(options?.onlyMainContent !== undefined ? { onlyMainContent: options.onlyMainContent } : {})
|
|
18
|
+
};
|
|
14
19
|
const response = await fetchImpl(buildScrapeUrl(baseUrl), {
|
|
15
20
|
method: 'POST',
|
|
16
21
|
headers,
|
|
17
|
-
body: JSON.stringify(
|
|
22
|
+
body: JSON.stringify(body)
|
|
18
23
|
});
|
|
19
24
|
if (!response.ok) {
|
|
20
25
|
throw new Error(`HTTP ${response.status}`);
|
|
@@ -9,11 +9,14 @@ function firstExcerpt(text, maxChars = 240) {
|
|
|
9
9
|
}
|
|
10
10
|
export function buildFetchPresentation(result) {
|
|
11
11
|
const wordCount = countWords(result.content?.text);
|
|
12
|
+
const fallbackPrefix = result.metadata.fallbackFrom
|
|
13
|
+
? `${result.metadata.fallbackFrom} failed; used ${result.metadata.method} fallback. `
|
|
14
|
+
: '';
|
|
12
15
|
const compact = result.status === 'ok'
|
|
13
|
-
?
|
|
16
|
+
? `${fallbackPrefix}Fetched page · article extracted${wordCount ? ` · ${wordCount} words` : ''}`
|
|
14
17
|
: result.status === 'needs_headless'
|
|
15
|
-
?
|
|
16
|
-
:
|
|
18
|
+
? `${fallbackPrefix}Needs headless rendering: ${result.error?.message ?? 'Headless rendering recommended.'}`
|
|
19
|
+
: `${fallbackPrefix}Fetch failed: ${result.error?.message ?? 'Unknown fetch failure.'}`;
|
|
17
20
|
return {
|
|
18
21
|
mode: 'compact',
|
|
19
22
|
views: {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
function formatCompact(result) {
|
|
2
|
+
const fallbackPrefix = result.metadata.fallbackFrom
|
|
3
|
+
? `${result.metadata.fallbackFrom} failed; used ${result.metadata.backend} fallback. `
|
|
4
|
+
: '';
|
|
2
5
|
if (result.status === 'error') {
|
|
3
|
-
return
|
|
6
|
+
return `${fallbackPrefix}Search failed: ${result.error?.message ?? 'Unknown search failure.'}`;
|
|
4
7
|
}
|
|
5
8
|
const suffix = result.results.length === 1 ? 'result' : 'results';
|
|
6
|
-
return
|
|
9
|
+
return `${fallbackPrefix}Found ${result.results.length} ${suffix}`;
|
|
7
10
|
}
|
|
8
11
|
export function buildSearchPresentation(result) {
|
|
9
12
|
const preview = result.results
|
package/dist/search/searxng.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { SearxngOptions } from '../backends/config.js';
|
|
1
2
|
import type { WebSearchResponse } from '../types.js';
|
|
2
|
-
export declare function createSearxngSearchTool({ baseUrl, fetchImpl }: {
|
|
3
|
+
export declare function createSearxngSearchTool({ baseUrl, options, fetchImpl }: {
|
|
3
4
|
baseUrl: string;
|
|
5
|
+
options?: SearxngOptions;
|
|
4
6
|
fetchImpl?: typeof fetch;
|
|
5
7
|
}): ({ query }: {
|
|
6
8
|
query: string;
|
package/dist/search/searxng.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
2
|
-
function buildSearchUrl(baseUrl, query) {
|
|
2
|
+
function buildSearchUrl(baseUrl, query, options = {}) {
|
|
3
3
|
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
4
4
|
url.searchParams.set('q', query);
|
|
5
5
|
url.searchParams.set('format', 'json');
|
|
6
|
+
if (options.categories?.length)
|
|
7
|
+
url.searchParams.set('categories', options.categories.join(','));
|
|
8
|
+
if (options.language)
|
|
9
|
+
url.searchParams.set('language', options.language);
|
|
10
|
+
if (options.safesearch !== undefined)
|
|
11
|
+
url.searchParams.set('safesearch', String(options.safesearch));
|
|
6
12
|
return url.toString();
|
|
7
13
|
}
|
|
8
14
|
function normalizeResults(response) {
|
|
@@ -19,7 +25,7 @@ function normalizeResults(response) {
|
|
|
19
25
|
];
|
|
20
26
|
});
|
|
21
27
|
}
|
|
22
|
-
export function createSearxngSearchTool({ baseUrl, fetchImpl = fetch }) {
|
|
28
|
+
export function createSearxngSearchTool({ baseUrl, options, fetchImpl = fetch }) {
|
|
23
29
|
return async function searxngSearch({ query }) {
|
|
24
30
|
const normalizedQuery = query.trim();
|
|
25
31
|
if (!normalizedQuery) {
|
|
@@ -32,7 +38,7 @@ export function createSearxngSearchTool({ baseUrl, fetchImpl = fetch }) {
|
|
|
32
38
|
return { ...result, presentation: buildSearchPresentation(result) };
|
|
33
39
|
}
|
|
34
40
|
try {
|
|
35
|
-
const response = await fetchImpl(buildSearchUrl(baseUrl, normalizedQuery));
|
|
41
|
+
const response = await fetchImpl(buildSearchUrl(baseUrl, normalizedQuery, options));
|
|
36
42
|
if (!response.ok) {
|
|
37
43
|
throw new Error(`HTTP ${response.status}`);
|
|
38
44
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -13,10 +13,14 @@ export type ToolError = {
|
|
|
13
13
|
export type SearchMetadata = {
|
|
14
14
|
backend: 'duckduckgo' | 'searxng';
|
|
15
15
|
cacheHit: boolean;
|
|
16
|
+
fallbackFrom?: 'searxng';
|
|
17
|
+
fallbackReason?: string;
|
|
16
18
|
};
|
|
17
19
|
export type FetchMetadata = {
|
|
18
20
|
method: 'http' | 'headless' | 'firecrawl';
|
|
19
21
|
cacheHit: boolean;
|
|
22
|
+
fallbackFrom?: 'firecrawl';
|
|
23
|
+
fallbackReason?: string;
|
|
20
24
|
contentType?: string;
|
|
21
25
|
truncated?: boolean;
|
|
22
26
|
browser?: 'configured' | 'chrome' | 'edge' | 'brave' | 'chromium';
|
package/package.json
CHANGED