@demigodmode/pi-web-agent 1.6.2 → 1.7.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/README.md +1 -1
- package/dist/backends/config.d.ts +1 -1
- package/dist/backends/config.js +5 -3
- package/dist/backends/doctor.js +78 -0
- package/dist/backends/factory.d.ts +4 -0
- package/dist/backends/factory.js +15 -1
- package/dist/commands/web-agent-config.js +17 -5
- package/dist/search/exa.d.ts +7 -0
- package/dist/search/exa.js +81 -0
- package/dist/search/tavily.d.ts +7 -0
- package/dist/search/tavily.js +81 -0
- package/dist/types.d.ts +2 -2
- package/package.json +5 -3
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.7.0] - 2026-07-17
|
|
22
|
+
### Added
|
|
23
|
+
- Exa is now available as a hosted search backend for `web_explore`, aimed at research-quality source discovery. Set `EXA_API_KEY` in the environment and choose `exa` from **Settings → Backends**. Closes #2.
|
|
24
|
+
- Tavily is now available as a hosted search backend for `web_explore` alongside Exa, DuckDuckGo, SearXNG, Brave, and You.com. Set `TAVILY_API_KEY` and choose `tavily` from **Settings → Backends**. Closes #2.
|
|
25
|
+
- Both new backends support optional fallback to DuckDuckGo, and `/web-agent doctor` reports their setup status and validates configured access when a key is present.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Bumped @earendil-works/pi-coding-agent and pi-tui devDependencies to 0.80.10.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- None.
|
|
32
|
+
|
|
33
|
+
### Breaking
|
|
34
|
+
- None.
|
|
35
|
+
|
|
21
36
|
## [1.6.2] - 2026-07-10
|
|
22
37
|
### Added
|
|
23
38
|
- None.
|
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ Backend settings can be changed from:
|
|
|
119
119
|
/web-agent settings
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
Choose **Backends** to edit search/fetch providers, fallback behavior, and SearXNG or Firecrawl base URLs interactively. Brave Search uses `PI_WEB_AGENT_BRAVE_API_KEY
|
|
122
|
+
Choose **Backends** to edit search/fetch providers, fallback behavior, and SearXNG or Firecrawl base URLs interactively. Brave Search uses `PI_WEB_AGENT_BRAVE_API_KEY`, You.com uses `YDC_API_KEY`, Exa uses `EXA_API_KEY`, and Tavily uses `TAVILY_API_KEY`. Firecrawl API keys should also stay in environment variables rather than being written into config files.
|
|
123
123
|
|
|
124
124
|
For the full backend config shape, including SearXNG, Brave, You.com, Firecrawl, and fallback behavior, see:
|
|
125
125
|
|
|
@@ -8,7 +8,7 @@ export type FirecrawlOptions = {
|
|
|
8
8
|
onlyMainContent?: boolean;
|
|
9
9
|
};
|
|
10
10
|
export type SearchBackendConfig = {
|
|
11
|
-
provider: 'duckduckgo' | 'searxng' | 'brave' | 'youcom';
|
|
11
|
+
provider: 'duckduckgo' | 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
|
|
12
12
|
baseUrl?: string;
|
|
13
13
|
fallback?: 'duckduckgo';
|
|
14
14
|
options?: SearxngOptions;
|
package/dist/backends/config.js
CHANGED
|
@@ -41,7 +41,9 @@ export function extractBackendConfigOverride(file) {
|
|
|
41
41
|
if (backends?.search?.provider === 'duckduckgo' ||
|
|
42
42
|
backends?.search?.provider === 'searxng' ||
|
|
43
43
|
backends?.search?.provider === 'brave' ||
|
|
44
|
-
backends?.search?.provider === 'youcom'
|
|
44
|
+
backends?.search?.provider === 'youcom' ||
|
|
45
|
+
backends?.search?.provider === 'exa' ||
|
|
46
|
+
backends?.search?.provider === 'tavily') {
|
|
45
47
|
override.search = { provider: backends.search.provider };
|
|
46
48
|
if (backends.search.provider === 'searxng' && typeof backends.search.baseUrl === 'string') {
|
|
47
49
|
override.search.baseUrl = backends.search.baseUrl;
|
|
@@ -85,8 +87,8 @@ export function validateBackendConfig(config) {
|
|
|
85
87
|
if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
|
|
86
88
|
issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
|
|
87
89
|
}
|
|
88
|
-
if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng' && config.search.provider !== 'brave' && config.search.provider !== 'youcom') {
|
|
89
|
-
issues.push('search fallback duckduckgo is only supported when search provider is searxng, brave, or
|
|
90
|
+
if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng' && config.search.provider !== 'brave' && config.search.provider !== 'youcom' && config.search.provider !== 'exa' && config.search.provider !== 'tavily') {
|
|
91
|
+
issues.push('search fallback duckduckgo is only supported when search provider is searxng, brave, youcom, exa, or tavily');
|
|
90
92
|
}
|
|
91
93
|
if (config.fetch.fallback === 'http' && config.fetch.provider !== 'firecrawl') {
|
|
92
94
|
issues.push('fetch fallback http is only supported when fetch provider is firecrawl');
|
package/dist/backends/doctor.js
CHANGED
|
@@ -15,6 +15,12 @@ function braveDoctorUrl() {
|
|
|
15
15
|
function youcomDoctorBody() {
|
|
16
16
|
return JSON.stringify({ query: 'pi-web-agent-doctor', max_results: 1 });
|
|
17
17
|
}
|
|
18
|
+
function exaDoctorBody() {
|
|
19
|
+
return JSON.stringify({ query: 'pi-web-agent-doctor', numResults: 1 });
|
|
20
|
+
}
|
|
21
|
+
function tavilyDoctorBody() {
|
|
22
|
+
return JSON.stringify({ query: 'pi-web-agent-doctor', max_results: 1 });
|
|
23
|
+
}
|
|
18
24
|
function searxngDoctorUrl(baseUrl, options = {}) {
|
|
19
25
|
const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
|
|
20
26
|
url.searchParams.set('q', 'pi-web-agent-doctor');
|
|
@@ -108,6 +114,78 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
|
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
}
|
|
117
|
+
else if (config.search.provider === 'exa') {
|
|
118
|
+
const apiKey = process.env.EXA_API_KEY;
|
|
119
|
+
if (!apiKey?.trim()) {
|
|
120
|
+
lines.push('search backend: exa warning (missing EXA_API_KEY)');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const timeout = withTimeout(timeoutMs);
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetchImpl('https://api.exa.ai/search', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
Accept: 'application/json',
|
|
129
|
+
'Content-Type': 'application/json',
|
|
130
|
+
'x-api-key': apiKey
|
|
131
|
+
},
|
|
132
|
+
body: exaDoctorBody(),
|
|
133
|
+
signal: timeout.signal
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
lines.push(`search backend: exa warning (HTTP ${response.status})`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const json = (await response.json());
|
|
140
|
+
lines.push(Array.isArray(json.results)
|
|
141
|
+
? 'search backend: exa ok'
|
|
142
|
+
: 'search backend: exa warning (unexpected response)');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
lines.push(`search backend: exa warning (${message(error)})`);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
timeout.done();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (config.search.provider === 'tavily') {
|
|
154
|
+
const apiKey = process.env.TAVILY_API_KEY;
|
|
155
|
+
if (!apiKey?.trim()) {
|
|
156
|
+
lines.push('search backend: tavily warning (missing TAVILY_API_KEY)');
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const timeout = withTimeout(timeoutMs);
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetchImpl('https://api.tavily.com/search', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
Accept: 'application/json',
|
|
165
|
+
'Content-Type': 'application/json',
|
|
166
|
+
Authorization: `Bearer ${apiKey}`
|
|
167
|
+
},
|
|
168
|
+
body: tavilyDoctorBody(),
|
|
169
|
+
signal: timeout.signal
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
lines.push(`search backend: tavily warning (HTTP ${response.status})`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const json = (await response.json());
|
|
176
|
+
lines.push(Array.isArray(json.results)
|
|
177
|
+
? 'search backend: tavily ok'
|
|
178
|
+
: 'search backend: tavily warning (unexpected response)');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
lines.push(`search backend: tavily warning (${message(error)})`);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
timeout.done();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
111
189
|
else if (!config.search.baseUrl) {
|
|
112
190
|
lines.push('search backend: searxng warning (missing baseUrl)');
|
|
113
191
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
|
|
2
2
|
import { createBraveSearchTool } from '../search/brave.js';
|
|
3
3
|
import { createYouComSearchTool } from '../search/youcom.js';
|
|
4
|
+
import { createExaSearchTool } from '../search/exa.js';
|
|
5
|
+
import { createTavilySearchTool } from '../search/tavily.js';
|
|
4
6
|
import { createSearxngSearchTool } from '../search/searxng.js';
|
|
5
7
|
import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
|
|
6
8
|
import { createWebFetchTool } from '../tools/web-fetch.js';
|
|
@@ -23,6 +25,8 @@ export type BackendFactoryDeps = {
|
|
|
23
25
|
createSearxngSearch?: typeof createSearxngSearchTool;
|
|
24
26
|
createBraveSearch?: typeof createBraveSearchTool;
|
|
25
27
|
createYouComSearch?: typeof createYouComSearchTool;
|
|
28
|
+
createExaSearch?: typeof createExaSearchTool;
|
|
29
|
+
createTavilySearch?: typeof createTavilySearchTool;
|
|
26
30
|
createHttpFetch?: typeof createWebFetchTool;
|
|
27
31
|
createFirecrawlFetch?: typeof createFirecrawlFetcher;
|
|
28
32
|
createHeadlessFetch?: typeof createWebFetchHeadlessTool;
|
package/dist/backends/factory.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
|
|
2
2
|
import { createBraveSearchTool } from '../search/brave.js';
|
|
3
3
|
import { createYouComSearchTool } from '../search/youcom.js';
|
|
4
|
+
import { createExaSearchTool } from '../search/exa.js';
|
|
5
|
+
import { createTavilySearchTool } from '../search/tavily.js';
|
|
4
6
|
import { createSearxngSearchTool } from '../search/searxng.js';
|
|
5
7
|
import { buildFetchPresentation } from '../presentation/fetch-presentation.js';
|
|
6
8
|
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
@@ -75,6 +77,8 @@ export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
|
|
|
75
77
|
const createSearxngSearch = deps.createSearxngSearch ?? createSearxngSearchTool;
|
|
76
78
|
const createBraveSearch = deps.createBraveSearch ?? createBraveSearchTool;
|
|
77
79
|
const createYouComSearch = deps.createYouComSearch ?? createYouComSearchTool;
|
|
80
|
+
const createExaSearch = deps.createExaSearch ?? createExaSearchTool;
|
|
81
|
+
const createTavilySearch = deps.createTavilySearch ?? createTavilySearchTool;
|
|
78
82
|
const createHttpFetch = deps.createHttpFetch ?? createWebFetchTool;
|
|
79
83
|
const createFirecrawlFetch = deps.createFirecrawlFetch ?? createFirecrawlFetcher;
|
|
80
84
|
const createHeadlessFetch = deps.createHeadlessFetch ?? createWebFetchHeadlessTool;
|
|
@@ -86,7 +90,11 @@ export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
|
|
|
86
90
|
? createBraveSearch({ apiKey: process.env.PI_WEB_AGENT_BRAVE_API_KEY })
|
|
87
91
|
: config.search.provider === 'youcom'
|
|
88
92
|
? createYouComSearch({ apiKey: process.env.YDC_API_KEY })
|
|
89
|
-
:
|
|
93
|
+
: config.search.provider === 'exa'
|
|
94
|
+
? createExaSearch({ apiKey: process.env.EXA_API_KEY })
|
|
95
|
+
: config.search.provider === 'tavily'
|
|
96
|
+
? createTavilySearch({ apiKey: process.env.TAVILY_API_KEY })
|
|
97
|
+
: createDuckDuckGoSearch();
|
|
90
98
|
if (config.search.provider === 'searxng' && config.search.fallback === 'duckduckgo') {
|
|
91
99
|
search = withSearchFallback(search, createDuckDuckGoSearch(), 'searxng');
|
|
92
100
|
}
|
|
@@ -96,6 +104,12 @@ export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
|
|
|
96
104
|
if (config.search.provider === 'youcom' && config.search.fallback === 'duckduckgo') {
|
|
97
105
|
search = withSearchFallback(search, createDuckDuckGoSearch(), 'youcom');
|
|
98
106
|
}
|
|
107
|
+
if (config.search.provider === 'exa' && config.search.fallback === 'duckduckgo') {
|
|
108
|
+
search = withSearchFallback(search, createDuckDuckGoSearch(), 'exa');
|
|
109
|
+
}
|
|
110
|
+
if (config.search.provider === 'tavily' && config.search.fallback === 'duckduckgo') {
|
|
111
|
+
search = withSearchFallback(search, createDuckDuckGoSearch(), 'tavily');
|
|
112
|
+
}
|
|
99
113
|
const httpFetch = createHttpFetch();
|
|
100
114
|
let fetchPage = config.fetch.provider === 'firecrawl'
|
|
101
115
|
? config.fetch.baseUrl
|
|
@@ -166,7 +166,7 @@ function buildBackendSettingsItems(scope, backends, theme, onUrlEditorOpenChange
|
|
|
166
166
|
id: 'backend:search:provider',
|
|
167
167
|
label: 'Search backend',
|
|
168
168
|
currentValue: backends.search.provider,
|
|
169
|
-
values: ['duckduckgo', 'searxng', 'brave', 'youcom']
|
|
169
|
+
values: ['duckduckgo', 'searxng', 'brave', 'youcom', 'exa', 'tavily']
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
id: 'backend:search:baseUrl',
|
|
@@ -177,8 +177,8 @@ function buildBackendSettingsItems(scope, backends, theme, onUrlEditorOpenChange
|
|
|
177
177
|
{
|
|
178
178
|
id: 'backend:search:fallback',
|
|
179
179
|
label: 'Search fallback',
|
|
180
|
-
currentValue: backends.search.provider === 'searxng' || backends.search.provider === 'brave' || backends.search.provider === 'youcom' ? backends.search.fallback ?? 'off' : 'off',
|
|
181
|
-
values: backends.search.provider === 'searxng' || backends.search.provider === 'brave' || backends.search.provider === 'youcom' ? ['off', 'duckduckgo'] : ['off']
|
|
180
|
+
currentValue: backends.search.provider === 'searxng' || backends.search.provider === 'brave' || backends.search.provider === 'youcom' || backends.search.provider === 'exa' || backends.search.provider === 'tavily' ? backends.search.fallback ?? 'off' : 'off',
|
|
181
|
+
values: backends.search.provider === 'searxng' || backends.search.provider === 'brave' || backends.search.provider === 'youcom' || backends.search.provider === 'exa' || backends.search.provider === 'tavily' ? ['off', 'duckduckgo'] : ['off']
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
id: 'backend:secret:brave',
|
|
@@ -192,6 +192,18 @@ function buildBackendSettingsItems(scope, backends, theme, onUrlEditorOpenChange
|
|
|
192
192
|
currentValue: 'env var',
|
|
193
193
|
values: ['env var']
|
|
194
194
|
},
|
|
195
|
+
{
|
|
196
|
+
id: 'backend:secret:exa',
|
|
197
|
+
label: 'Exa API key',
|
|
198
|
+
currentValue: 'env var',
|
|
199
|
+
values: ['env var']
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'backend:secret:tavily',
|
|
203
|
+
label: 'Tavily API key',
|
|
204
|
+
currentValue: 'env var',
|
|
205
|
+
values: ['env var']
|
|
206
|
+
},
|
|
195
207
|
{
|
|
196
208
|
id: 'backend:fetch:provider',
|
|
197
209
|
label: 'Fetch backend',
|
|
@@ -303,7 +315,7 @@ export function applySettingsValue(state, id, newValue) {
|
|
|
303
315
|
}
|
|
304
316
|
currentDraft.tools = nextTools;
|
|
305
317
|
}
|
|
306
|
-
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave' || newValue === 'youcom')) {
|
|
318
|
+
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave' || newValue === 'youcom' || newValue === 'exa' || newValue === 'tavily')) {
|
|
307
319
|
currentBackends.search.provider = newValue;
|
|
308
320
|
if (newValue !== 'searxng') {
|
|
309
321
|
delete currentBackends.search.baseUrl;
|
|
@@ -314,7 +326,7 @@ export function applySettingsValue(state, id, newValue) {
|
|
|
314
326
|
}
|
|
315
327
|
}
|
|
316
328
|
if (id === 'backend:search:fallback') {
|
|
317
|
-
if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave' || currentBackends.search.provider === 'youcom')) {
|
|
329
|
+
if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave' || currentBackends.search.provider === 'youcom' || currentBackends.search.provider === 'exa' || currentBackends.search.provider === 'tavily')) {
|
|
318
330
|
currentBackends.search.fallback = 'duckduckgo';
|
|
319
331
|
}
|
|
320
332
|
else {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
2
|
+
const EXA_SEARCH_URL = 'https://api.exa.ai/search';
|
|
3
|
+
function resultWithPresentation(result) {
|
|
4
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
5
|
+
}
|
|
6
|
+
function normalizeResults(response) {
|
|
7
|
+
return (response.results ?? []).flatMap((item) => {
|
|
8
|
+
if (typeof item.title !== 'string' || typeof item.url !== 'string') {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
title: item.title,
|
|
14
|
+
url: item.url,
|
|
15
|
+
snippet: typeof item.text === 'string' ? item.text : ''
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function createExaSearchTool({ apiKey, fetchImpl = fetch }) {
|
|
21
|
+
return async function exaSearch({ query }) {
|
|
22
|
+
const normalizedQuery = query.trim();
|
|
23
|
+
if (!normalizedQuery) {
|
|
24
|
+
return resultWithPresentation({
|
|
25
|
+
status: 'error',
|
|
26
|
+
results: [],
|
|
27
|
+
metadata: { backend: 'exa', cacheHit: false },
|
|
28
|
+
error: { code: 'INVALID_QUERY', message: 'Query must not be empty.' }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!apiKey?.trim()) {
|
|
32
|
+
return resultWithPresentation({
|
|
33
|
+
status: 'error',
|
|
34
|
+
results: [],
|
|
35
|
+
metadata: { backend: 'exa', cacheHit: false },
|
|
36
|
+
error: {
|
|
37
|
+
code: 'BACKEND_CONFIG_INVALID',
|
|
38
|
+
message: 'Exa search requires EXA_API_KEY.'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetchImpl(EXA_SEARCH_URL, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
Accept: 'application/json',
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'x-api-key': apiKey
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({ query: normalizedQuery, numResults: 10 })
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`HTTP ${response.status}`);
|
|
54
|
+
}
|
|
55
|
+
const parsed = (await response.json());
|
|
56
|
+
const results = normalizeResults(parsed);
|
|
57
|
+
if (results.length === 0) {
|
|
58
|
+
return resultWithPresentation({
|
|
59
|
+
status: 'error',
|
|
60
|
+
results: [],
|
|
61
|
+
metadata: { backend: 'exa', cacheHit: false },
|
|
62
|
+
error: { code: 'NO_RESULTS', message: 'Exa returned no usable results for this query.' }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return resultWithPresentation({
|
|
66
|
+
status: 'ok',
|
|
67
|
+
results,
|
|
68
|
+
metadata: { backend: 'exa', cacheHit: false }
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
73
|
+
return resultWithPresentation({
|
|
74
|
+
status: 'error',
|
|
75
|
+
results: [],
|
|
76
|
+
metadata: { backend: 'exa', cacheHit: false },
|
|
77
|
+
error: { code: 'FETCH_FAILED', message: `Exa search request failed: ${rawMessage}` }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { buildSearchPresentation } from '../presentation/search-presentation.js';
|
|
2
|
+
const TAVILY_SEARCH_URL = 'https://api.tavily.com/search';
|
|
3
|
+
function resultWithPresentation(result) {
|
|
4
|
+
return { ...result, presentation: buildSearchPresentation(result) };
|
|
5
|
+
}
|
|
6
|
+
function normalizeResults(response) {
|
|
7
|
+
return (response.results ?? []).flatMap((item) => {
|
|
8
|
+
if (typeof item.title !== 'string' || typeof item.url !== 'string') {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
title: item.title,
|
|
14
|
+
url: item.url,
|
|
15
|
+
snippet: typeof item.content === 'string' ? item.content : ''
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function createTavilySearchTool({ apiKey, fetchImpl = fetch }) {
|
|
21
|
+
return async function tavilySearch({ query }) {
|
|
22
|
+
const normalizedQuery = query.trim();
|
|
23
|
+
if (!normalizedQuery) {
|
|
24
|
+
return resultWithPresentation({
|
|
25
|
+
status: 'error',
|
|
26
|
+
results: [],
|
|
27
|
+
metadata: { backend: 'tavily', cacheHit: false },
|
|
28
|
+
error: { code: 'INVALID_QUERY', message: 'Query must not be empty.' }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!apiKey?.trim()) {
|
|
32
|
+
return resultWithPresentation({
|
|
33
|
+
status: 'error',
|
|
34
|
+
results: [],
|
|
35
|
+
metadata: { backend: 'tavily', cacheHit: false },
|
|
36
|
+
error: {
|
|
37
|
+
code: 'BACKEND_CONFIG_INVALID',
|
|
38
|
+
message: 'Tavily search requires TAVILY_API_KEY.'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetchImpl(TAVILY_SEARCH_URL, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
Accept: 'application/json',
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
Authorization: `Bearer ${apiKey}`
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({ query: normalizedQuery, max_results: 10 })
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`HTTP ${response.status}`);
|
|
54
|
+
}
|
|
55
|
+
const parsed = (await response.json());
|
|
56
|
+
const results = normalizeResults(parsed);
|
|
57
|
+
if (results.length === 0) {
|
|
58
|
+
return resultWithPresentation({
|
|
59
|
+
status: 'error',
|
|
60
|
+
results: [],
|
|
61
|
+
metadata: { backend: 'tavily', cacheHit: false },
|
|
62
|
+
error: { code: 'NO_RESULTS', message: 'Tavily returned no usable results for this query.' }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return resultWithPresentation({
|
|
66
|
+
status: 'ok',
|
|
67
|
+
results,
|
|
68
|
+
metadata: { backend: 'tavily', cacheHit: false }
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
73
|
+
return resultWithPresentation({
|
|
74
|
+
status: 'error',
|
|
75
|
+
results: [],
|
|
76
|
+
metadata: { backend: 'tavily', cacheHit: false },
|
|
77
|
+
error: { code: 'FETCH_FAILED', message: `Tavily search request failed: ${rawMessage}` }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ export type ToolError = {
|
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
13
|
export type SearchMetadata = {
|
|
14
|
-
backend: 'duckduckgo' | 'searxng' | 'brave' | 'youcom';
|
|
14
|
+
backend: 'duckduckgo' | 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
|
|
15
15
|
cacheHit: boolean;
|
|
16
|
-
fallbackFrom?: 'searxng' | 'brave' | 'youcom';
|
|
16
|
+
fallbackFrom?: 'searxng' | 'brave' | 'youcom' | 'exa' | 'tavily';
|
|
17
17
|
fallbackReason?: string;
|
|
18
18
|
};
|
|
19
19
|
export type FetchMetadata = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demigodmode/pi-web-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Pi package for reliable web access with explicit search, fetch, and headless boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/extension.js",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"searxng",
|
|
28
28
|
"brave-search",
|
|
29
29
|
"you-com",
|
|
30
|
+
"exa",
|
|
31
|
+
"tavily",
|
|
30
32
|
"firecrawl"
|
|
31
33
|
],
|
|
32
34
|
"repository": {
|
|
@@ -68,8 +70,8 @@
|
|
|
68
70
|
"typebox": "^1.1.37"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
71
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
72
|
-
"@earendil-works/pi-tui": "^0.
|
|
73
|
+
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
74
|
+
"@earendil-works/pi-tui": "^0.80.10",
|
|
73
75
|
"@types/jsdom": "^21.1.7",
|
|
74
76
|
"@types/node": "^24.0.0",
|
|
75
77
|
"@vitest/coverage-v8": "^3.2.6",
|