openclaw-tavily 0.1.0 → 0.2.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/README.md +178 -14
- package/index.ts +820 -40
- package/openclaw.plugin.json +18 -11
- package/package.json +6 -3
- package/skills/tavily-search/SKILL.md +130 -0
- package/skills/tavily-search/scripts/crawl.mjs +86 -0
- package/skills/tavily-search/scripts/extract.mjs +76 -0
- package/skills/tavily-search/scripts/map.mjs +73 -0
- package/skills/tavily-search/scripts/research.mjs +121 -0
- package/skills/tavily-search/scripts/search.mjs +94 -0
package/index.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* openclaw-tavily — Tavily web
|
|
2
|
+
* openclaw-tavily — Tavily web tools plugin for OpenClaw
|
|
3
3
|
*
|
|
4
|
-
* Exposes
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Exposes five tools:
|
|
5
|
+
* - `tavily_search` — web search with structured results and AI answers
|
|
6
|
+
* - `tavily_extract` — extract clean content from URLs
|
|
7
|
+
* - `tavily_crawl` — crawl a website and extract page content
|
|
8
|
+
* - `tavily_map` — discover and list URLs from a website
|
|
9
|
+
* - `tavily_research` — deep agentic research returning comprehensive reports
|
|
7
10
|
*
|
|
8
|
-
* API reference: https://docs.tavily.com/documentation/api-reference
|
|
11
|
+
* API reference: https://docs.tavily.com/documentation/api-reference
|
|
9
12
|
*
|
|
10
13
|
* Config (openclaw.json → plugins.entries.openclaw-tavily.config):
|
|
11
14
|
* apiKey - Tavily API key (or set TAVILY_API_KEY env var)
|
|
12
|
-
* searchDepth - "basic" | "advanced" (default: "advanced")
|
|
15
|
+
* searchDepth - "basic" | "advanced" | "fast" | "ultra-fast" (default: "advanced")
|
|
13
16
|
* maxResults - 1-20 (default: 5)
|
|
14
|
-
* includeAnswer - boolean (default: true)
|
|
15
|
-
* includeRawContent - boolean (default: false)
|
|
17
|
+
* includeAnswer - boolean | "basic" | "advanced" (default: true)
|
|
18
|
+
* includeRawContent - boolean | "markdown" | "text" (default: false)
|
|
16
19
|
* timeoutSeconds - number (default: 30)
|
|
17
20
|
* cacheTtlMinutes - number (default: 15)
|
|
18
21
|
*/
|
|
@@ -42,6 +45,7 @@ type TavilySearchResult = {
|
|
|
42
45
|
content: string; // snippet
|
|
43
46
|
raw_content?: string;
|
|
44
47
|
score: number;
|
|
48
|
+
favicon?: string;
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
type TavilySearchResponse = {
|
|
@@ -62,6 +66,10 @@ type CacheEntry = {
|
|
|
62
66
|
// ---------------------------------------------------------------------------
|
|
63
67
|
|
|
64
68
|
const TAVILY_SEARCH_ENDPOINT = "https://api.tavily.com/search";
|
|
69
|
+
const TAVILY_EXTRACT_ENDPOINT = "https://api.tavily.com/extract";
|
|
70
|
+
const TAVILY_CRAWL_ENDPOINT = "https://api.tavily.com/crawl";
|
|
71
|
+
const TAVILY_MAP_ENDPOINT = "https://api.tavily.com/map";
|
|
72
|
+
const TAVILY_RESEARCH_ENDPOINT = "https://api.tavily.com/research";
|
|
65
73
|
const DEFAULT_SEARCH_DEPTH = "advanced";
|
|
66
74
|
const DEFAULT_MAX_RESULTS = 5;
|
|
67
75
|
const MAX_RESULTS_CAP = 20;
|
|
@@ -105,9 +113,12 @@ function resolveApiKey(cfg: Record<string, unknown>): string | undefined {
|
|
|
105
113
|
return fromConfig || fromEnv || undefined;
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
type SearchDepth = "basic" | "advanced" | "fast" | "ultra-fast";
|
|
117
|
+
|
|
118
|
+
function resolveSearchDepth(cfg: Record<string, unknown>): SearchDepth {
|
|
109
119
|
const v = typeof cfg.searchDepth === "string" ? cfg.searchDepth.trim().toLowerCase() : "";
|
|
110
|
-
|
|
120
|
+
if (v === "basic" || v === "fast" || v === "ultra-fast") return v as SearchDepth;
|
|
121
|
+
return DEFAULT_SEARCH_DEPTH;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
function resolveMaxResults(cfg: Record<string, unknown>): number {
|
|
@@ -115,12 +126,18 @@ function resolveMaxResults(cfg: Record<string, unknown>): number {
|
|
|
115
126
|
return Math.max(1, Math.min(MAX_RESULTS_CAP, Math.floor(v)));
|
|
116
127
|
}
|
|
117
128
|
|
|
118
|
-
function resolveIncludeAnswer(cfg: Record<string, unknown>): boolean {
|
|
119
|
-
|
|
129
|
+
function resolveIncludeAnswer(cfg: Record<string, unknown>): boolean | string {
|
|
130
|
+
const v = cfg.includeAnswer;
|
|
131
|
+
if (typeof v === "string" && ["basic", "advanced"].includes(v)) return v;
|
|
132
|
+
if (v === false) return false;
|
|
133
|
+
return true; // default true
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
function resolveIncludeRawContent(cfg: Record<string, unknown>): boolean {
|
|
123
|
-
|
|
136
|
+
function resolveIncludeRawContent(cfg: Record<string, unknown>): boolean | string {
|
|
137
|
+
const v = cfg.includeRawContent;
|
|
138
|
+
if (typeof v === "string" && ["markdown", "text"].includes(v)) return v;
|
|
139
|
+
if (v === true) return true;
|
|
140
|
+
return false; // default false
|
|
124
141
|
}
|
|
125
142
|
|
|
126
143
|
function resolveTimeout(cfg: Record<string, unknown>): number {
|
|
@@ -158,29 +175,41 @@ const TavilySearchSchema = Type.Object({
|
|
|
158
175
|
search_depth: Type.Optional(
|
|
159
176
|
Type.String({
|
|
160
177
|
description:
|
|
161
|
-
'Search depth: "
|
|
178
|
+
'Search depth: "ultra-fast", "fast", "basic", or "advanced" (thorough). Default: from config.',
|
|
162
179
|
}),
|
|
163
180
|
),
|
|
164
181
|
include_answer: Type.Optional(
|
|
165
|
-
Type.Boolean({
|
|
166
|
-
description:
|
|
182
|
+
Type.Union([Type.Boolean(), Type.String()], {
|
|
183
|
+
description:
|
|
184
|
+
'Include an AI-generated short answer. Boolean or "basic"/"advanced". Default: from config.',
|
|
167
185
|
}),
|
|
168
186
|
),
|
|
169
187
|
include_raw_content: Type.Optional(
|
|
170
|
-
Type.Boolean({
|
|
171
|
-
description:
|
|
188
|
+
Type.Union([Type.Boolean(), Type.String()], {
|
|
189
|
+
description:
|
|
190
|
+
'Include raw page content. Boolean or "markdown"/"text". Default: from config.',
|
|
172
191
|
}),
|
|
173
192
|
),
|
|
174
193
|
topic: Type.Optional(
|
|
175
194
|
Type.String({
|
|
176
195
|
description:
|
|
177
|
-
'Category of search: "general" or "
|
|
196
|
+
'Category of search: "general", "news", or "finance". Default: "general".',
|
|
178
197
|
}),
|
|
179
198
|
),
|
|
180
|
-
|
|
181
|
-
Type.
|
|
199
|
+
time_range: Type.Optional(
|
|
200
|
+
Type.String({
|
|
182
201
|
description:
|
|
183
|
-
|
|
202
|
+
'Time range filter: "day", "week", "month", or "year".',
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
start_date: Type.Optional(
|
|
206
|
+
Type.String({
|
|
207
|
+
description: "Start date for results (ISO date string, e.g. \"2024-01-01\").",
|
|
208
|
+
}),
|
|
209
|
+
),
|
|
210
|
+
end_date: Type.Optional(
|
|
211
|
+
Type.String({
|
|
212
|
+
description: "End date for results (ISO date string, e.g. \"2024-12-31\").",
|
|
184
213
|
}),
|
|
185
214
|
),
|
|
186
215
|
include_domains: Type.Optional(
|
|
@@ -193,6 +222,241 @@ const TavilySearchSchema = Type.Object({
|
|
|
193
222
|
description: "Exclude results from these domains.",
|
|
194
223
|
}),
|
|
195
224
|
),
|
|
225
|
+
chunks_per_source: Type.Optional(
|
|
226
|
+
Type.Number({
|
|
227
|
+
description: "Number of content chunks per source (1-3, only for advanced depth).",
|
|
228
|
+
minimum: 1,
|
|
229
|
+
maximum: 3,
|
|
230
|
+
}),
|
|
231
|
+
),
|
|
232
|
+
include_images: Type.Optional(
|
|
233
|
+
Type.Boolean({
|
|
234
|
+
description: "Include images in results. Default: false.",
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
include_image_descriptions: Type.Optional(
|
|
238
|
+
Type.Boolean({
|
|
239
|
+
description: "Include descriptions for images. Default: false.",
|
|
240
|
+
}),
|
|
241
|
+
),
|
|
242
|
+
include_favicon: Type.Optional(
|
|
243
|
+
Type.Boolean({
|
|
244
|
+
description: "Include favicon URLs for each result. Default: false.",
|
|
245
|
+
}),
|
|
246
|
+
),
|
|
247
|
+
country: Type.Optional(
|
|
248
|
+
Type.String({
|
|
249
|
+
description: "Country code for geo-boosted results (only for general topic), e.g. \"us\", \"gb\".",
|
|
250
|
+
}),
|
|
251
|
+
),
|
|
252
|
+
auto_parameters: Type.Optional(
|
|
253
|
+
Type.Boolean({
|
|
254
|
+
description: "Let Tavily automatically set optimal search parameters. Default: false.",
|
|
255
|
+
}),
|
|
256
|
+
),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Extract schema
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
const TavilyExtractSchema = Type.Object({
|
|
264
|
+
urls: Type.Array(Type.String(), {
|
|
265
|
+
description: "URLs to extract content from.",
|
|
266
|
+
}),
|
|
267
|
+
query: Type.Optional(
|
|
268
|
+
Type.String({
|
|
269
|
+
description: "Optional query to rerank extracted chunks for relevance.",
|
|
270
|
+
}),
|
|
271
|
+
),
|
|
272
|
+
chunks_per_source: Type.Optional(
|
|
273
|
+
Type.Number({
|
|
274
|
+
description: "Max content snippets per source.",
|
|
275
|
+
}),
|
|
276
|
+
),
|
|
277
|
+
format: Type.Optional(
|
|
278
|
+
Type.String({
|
|
279
|
+
description: 'Output format: "markdown" or "text". Default: "markdown".',
|
|
280
|
+
}),
|
|
281
|
+
),
|
|
282
|
+
include_favicon: Type.Optional(
|
|
283
|
+
Type.Boolean({
|
|
284
|
+
description: "Include favicon URLs. Default: false.",
|
|
285
|
+
}),
|
|
286
|
+
),
|
|
287
|
+
timeout: Type.Optional(
|
|
288
|
+
Type.Number({
|
|
289
|
+
description: "Max seconds before timeout.",
|
|
290
|
+
}),
|
|
291
|
+
),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Crawl schema
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
const TavilyCrawlSchema = Type.Object({
|
|
299
|
+
url: Type.String({ description: "Root URL to start crawling from." }),
|
|
300
|
+
instructions: Type.Optional(
|
|
301
|
+
Type.String({
|
|
302
|
+
description: "Natural language guidance for the crawl.",
|
|
303
|
+
}),
|
|
304
|
+
),
|
|
305
|
+
max_depth: Type.Optional(
|
|
306
|
+
Type.Number({
|
|
307
|
+
description: "Crawl depth (1-5).",
|
|
308
|
+
minimum: 1,
|
|
309
|
+
maximum: 5,
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
max_breadth: Type.Optional(
|
|
313
|
+
Type.Number({
|
|
314
|
+
description: "Max links to follow per level (1-500).",
|
|
315
|
+
minimum: 1,
|
|
316
|
+
maximum: 500,
|
|
317
|
+
}),
|
|
318
|
+
),
|
|
319
|
+
limit: Type.Optional(
|
|
320
|
+
Type.Number({
|
|
321
|
+
description: "Total URL cap.",
|
|
322
|
+
}),
|
|
323
|
+
),
|
|
324
|
+
select_paths: Type.Optional(
|
|
325
|
+
Type.Array(Type.String(), {
|
|
326
|
+
description: "Regex include filters for URL paths.",
|
|
327
|
+
}),
|
|
328
|
+
),
|
|
329
|
+
select_domains: Type.Optional(
|
|
330
|
+
Type.Array(Type.String(), {
|
|
331
|
+
description: "Regex include filters for domains.",
|
|
332
|
+
}),
|
|
333
|
+
),
|
|
334
|
+
exclude_paths: Type.Optional(
|
|
335
|
+
Type.Array(Type.String(), {
|
|
336
|
+
description: "Regex exclude filters for URL paths.",
|
|
337
|
+
}),
|
|
338
|
+
),
|
|
339
|
+
exclude_domains: Type.Optional(
|
|
340
|
+
Type.Array(Type.String(), {
|
|
341
|
+
description: "Regex exclude filters for domains.",
|
|
342
|
+
}),
|
|
343
|
+
),
|
|
344
|
+
allow_external: Type.Optional(
|
|
345
|
+
Type.Boolean({
|
|
346
|
+
description: "Follow external links. Default: false.",
|
|
347
|
+
}),
|
|
348
|
+
),
|
|
349
|
+
include_images: Type.Optional(
|
|
350
|
+
Type.Boolean({
|
|
351
|
+
description: "Include images in results. Default: false.",
|
|
352
|
+
}),
|
|
353
|
+
),
|
|
354
|
+
extract_depth: Type.Optional(
|
|
355
|
+
Type.String({
|
|
356
|
+
description: 'Extraction depth: "basic" or "advanced".',
|
|
357
|
+
}),
|
|
358
|
+
),
|
|
359
|
+
format: Type.Optional(
|
|
360
|
+
Type.String({
|
|
361
|
+
description: 'Output format: "markdown" or "text".',
|
|
362
|
+
}),
|
|
363
|
+
),
|
|
364
|
+
include_favicon: Type.Optional(
|
|
365
|
+
Type.Boolean({
|
|
366
|
+
description: "Include favicons. Default: false.",
|
|
367
|
+
}),
|
|
368
|
+
),
|
|
369
|
+
timeout: Type.Optional(
|
|
370
|
+
Type.Number({
|
|
371
|
+
description: "Timeout in seconds (10-150).",
|
|
372
|
+
minimum: 10,
|
|
373
|
+
maximum: 150,
|
|
374
|
+
}),
|
|
375
|
+
),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// Map schema
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
const TavilyMapSchema = Type.Object({
|
|
383
|
+
url: Type.String({ description: "URL to map." }),
|
|
384
|
+
instructions: Type.Optional(
|
|
385
|
+
Type.String({
|
|
386
|
+
description: "Natural language guidance for the map.",
|
|
387
|
+
}),
|
|
388
|
+
),
|
|
389
|
+
max_depth: Type.Optional(
|
|
390
|
+
Type.Number({
|
|
391
|
+
description: "Crawl depth (1-5).",
|
|
392
|
+
minimum: 1,
|
|
393
|
+
maximum: 5,
|
|
394
|
+
}),
|
|
395
|
+
),
|
|
396
|
+
max_breadth: Type.Optional(
|
|
397
|
+
Type.Number({
|
|
398
|
+
description: "Max links to follow per level.",
|
|
399
|
+
}),
|
|
400
|
+
),
|
|
401
|
+
limit: Type.Optional(
|
|
402
|
+
Type.Number({
|
|
403
|
+
description: "Total URL cap.",
|
|
404
|
+
}),
|
|
405
|
+
),
|
|
406
|
+
select_paths: Type.Optional(
|
|
407
|
+
Type.Array(Type.String(), {
|
|
408
|
+
description: "Include filters for URL paths.",
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
411
|
+
select_domains: Type.Optional(
|
|
412
|
+
Type.Array(Type.String(), {
|
|
413
|
+
description: "Include filters for domains.",
|
|
414
|
+
}),
|
|
415
|
+
),
|
|
416
|
+
exclude_paths: Type.Optional(
|
|
417
|
+
Type.Array(Type.String(), {
|
|
418
|
+
description: "Exclude filters for URL paths.",
|
|
419
|
+
}),
|
|
420
|
+
),
|
|
421
|
+
exclude_domains: Type.Optional(
|
|
422
|
+
Type.Array(Type.String(), {
|
|
423
|
+
description: "Exclude filters for domains.",
|
|
424
|
+
}),
|
|
425
|
+
),
|
|
426
|
+
allow_external: Type.Optional(
|
|
427
|
+
Type.Boolean({
|
|
428
|
+
description: "Follow external links. Default: false.",
|
|
429
|
+
}),
|
|
430
|
+
),
|
|
431
|
+
categories: Type.Optional(
|
|
432
|
+
Type.String({
|
|
433
|
+
description: "Category filters.",
|
|
434
|
+
}),
|
|
435
|
+
),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// Research schema
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
const TavilyResearchSchema = Type.Object({
|
|
443
|
+
input: Type.String({ description: "Research question or topic." }),
|
|
444
|
+
model: Type.Optional(
|
|
445
|
+
Type.String({
|
|
446
|
+
description: 'Research model: "mini", "pro", or "auto". Default: "auto".',
|
|
447
|
+
}),
|
|
448
|
+
),
|
|
449
|
+
output_schema: Type.Optional(
|
|
450
|
+
Type.Object({}, {
|
|
451
|
+
description: "JSON schema for structured output.",
|
|
452
|
+
additionalProperties: true,
|
|
453
|
+
}),
|
|
454
|
+
),
|
|
455
|
+
citation_format: Type.Optional(
|
|
456
|
+
Type.String({
|
|
457
|
+
description: 'Citation format: "numbered", "mla", "apa", or "chicago".',
|
|
458
|
+
}),
|
|
459
|
+
),
|
|
196
460
|
});
|
|
197
461
|
|
|
198
462
|
// ---------------------------------------------------------------------------
|
|
@@ -203,7 +467,7 @@ const tavilyPlugin = {
|
|
|
203
467
|
id: "openclaw-tavily",
|
|
204
468
|
name: "Tavily Search",
|
|
205
469
|
description:
|
|
206
|
-
"Web search via Tavily API. Provides
|
|
470
|
+
"Web search, extraction, crawling, mapping, and research via Tavily API. Provides tavily_search, tavily_extract, tavily_crawl, tavily_map, and tavily_research tools.",
|
|
207
471
|
kind: "tools" as const,
|
|
208
472
|
|
|
209
473
|
register(api: PluginApi) {
|
|
@@ -270,31 +534,44 @@ const tavilyPlugin = {
|
|
|
270
534
|
|
|
271
535
|
const searchDepth =
|
|
272
536
|
typeof params.search_depth === "string" &&
|
|
273
|
-
["basic", "advanced"].includes(params.search_depth)
|
|
274
|
-
? (params.search_depth as
|
|
537
|
+
["basic", "advanced", "fast", "ultra-fast"].includes(params.search_depth)
|
|
538
|
+
? (params.search_depth as SearchDepth)
|
|
275
539
|
: defaultSearchDepth;
|
|
276
540
|
|
|
277
|
-
const includeAnswer =
|
|
278
|
-
typeof params.include_answer === "
|
|
541
|
+
const includeAnswer: boolean | string =
|
|
542
|
+
typeof params.include_answer === "string" &&
|
|
543
|
+
["basic", "advanced"].includes(params.include_answer)
|
|
279
544
|
? params.include_answer
|
|
280
|
-
:
|
|
545
|
+
: typeof params.include_answer === "boolean"
|
|
546
|
+
? params.include_answer
|
|
547
|
+
: defaultIncludeAnswer;
|
|
281
548
|
|
|
282
|
-
const includeRawContent =
|
|
283
|
-
typeof params.include_raw_content === "
|
|
549
|
+
const includeRawContent: boolean | string =
|
|
550
|
+
typeof params.include_raw_content === "string" &&
|
|
551
|
+
["markdown", "text"].includes(params.include_raw_content)
|
|
284
552
|
? params.include_raw_content
|
|
285
|
-
:
|
|
553
|
+
: typeof params.include_raw_content === "boolean"
|
|
554
|
+
? params.include_raw_content
|
|
555
|
+
: defaultIncludeRawContent;
|
|
286
556
|
|
|
287
557
|
const topic =
|
|
288
558
|
typeof params.topic === "string" &&
|
|
289
|
-
["general", "news"].includes(params.topic)
|
|
559
|
+
["general", "news", "finance"].includes(params.topic)
|
|
290
560
|
? params.topic
|
|
291
561
|
: "general";
|
|
292
562
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
563
|
+
const timeRange =
|
|
564
|
+
typeof params.time_range === "string" &&
|
|
565
|
+
["day", "week", "month", "year"].includes(params.time_range)
|
|
566
|
+
? params.time_range
|
|
296
567
|
: undefined;
|
|
297
568
|
|
|
569
|
+
const startDate =
|
|
570
|
+
typeof params.start_date === "string" ? params.start_date.trim() : undefined;
|
|
571
|
+
|
|
572
|
+
const endDate =
|
|
573
|
+
typeof params.end_date === "string" ? params.end_date.trim() : undefined;
|
|
574
|
+
|
|
298
575
|
const includeDomains = Array.isArray(params.include_domains)
|
|
299
576
|
? (params.include_domains as string[]).filter(
|
|
300
577
|
(d) => typeof d === "string" && d.trim(),
|
|
@@ -307,6 +584,31 @@ const tavilyPlugin = {
|
|
|
307
584
|
)
|
|
308
585
|
: undefined;
|
|
309
586
|
|
|
587
|
+
const chunksPerSource =
|
|
588
|
+
typeof params.chunks_per_source === "number" &&
|
|
589
|
+
searchDepth === "advanced"
|
|
590
|
+
? Math.max(1, Math.min(3, Math.floor(params.chunks_per_source)))
|
|
591
|
+
: undefined;
|
|
592
|
+
|
|
593
|
+
const includeImages =
|
|
594
|
+
typeof params.include_images === "boolean" ? params.include_images : undefined;
|
|
595
|
+
|
|
596
|
+
const includeImageDescriptions =
|
|
597
|
+
typeof params.include_image_descriptions === "boolean"
|
|
598
|
+
? params.include_image_descriptions
|
|
599
|
+
: undefined;
|
|
600
|
+
|
|
601
|
+
const includeFavicon =
|
|
602
|
+
typeof params.include_favicon === "boolean" ? params.include_favicon : undefined;
|
|
603
|
+
|
|
604
|
+
const country =
|
|
605
|
+
typeof params.country === "string" && topic === "general"
|
|
606
|
+
? params.country.trim()
|
|
607
|
+
: undefined;
|
|
608
|
+
|
|
609
|
+
const autoParameters =
|
|
610
|
+
typeof params.auto_parameters === "boolean" ? params.auto_parameters : undefined;
|
|
611
|
+
|
|
310
612
|
// --- cache ---
|
|
311
613
|
const cacheKey = [
|
|
312
614
|
"tavily",
|
|
@@ -316,9 +618,17 @@ const tavilyPlugin = {
|
|
|
316
618
|
includeAnswer,
|
|
317
619
|
includeRawContent,
|
|
318
620
|
topic,
|
|
319
|
-
|
|
621
|
+
timeRange ?? "",
|
|
622
|
+
startDate ?? "",
|
|
623
|
+
endDate ?? "",
|
|
320
624
|
(includeDomains ?? []).join(","),
|
|
321
625
|
(excludeDomains ?? []).join(","),
|
|
626
|
+
chunksPerSource ?? "",
|
|
627
|
+
includeImages ?? "",
|
|
628
|
+
includeImageDescriptions ?? "",
|
|
629
|
+
includeFavicon ?? "",
|
|
630
|
+
country ?? "",
|
|
631
|
+
autoParameters ?? "",
|
|
322
632
|
]
|
|
323
633
|
.join(":")
|
|
324
634
|
.toLowerCase();
|
|
@@ -339,18 +649,26 @@ const tavilyPlugin = {
|
|
|
339
649
|
// --- build Tavily API request body ---
|
|
340
650
|
const body: Record<string, unknown> = {
|
|
341
651
|
query,
|
|
342
|
-
api_key: apiKey,
|
|
343
652
|
search_depth: searchDepth,
|
|
344
653
|
max_results: count,
|
|
345
654
|
include_answer: includeAnswer,
|
|
346
655
|
include_raw_content: includeRawContent,
|
|
347
656
|
topic,
|
|
348
657
|
};
|
|
349
|
-
if (
|
|
658
|
+
if (timeRange !== undefined) body.time_range = timeRange;
|
|
659
|
+
if (startDate) body.start_date = startDate;
|
|
660
|
+
if (endDate) body.end_date = endDate;
|
|
350
661
|
if (includeDomains && includeDomains.length > 0)
|
|
351
662
|
body.include_domains = includeDomains;
|
|
352
663
|
if (excludeDomains && excludeDomains.length > 0)
|
|
353
664
|
body.exclude_domains = excludeDomains;
|
|
665
|
+
if (chunksPerSource !== undefined) body.chunks_per_source = chunksPerSource;
|
|
666
|
+
if (includeImages !== undefined) body.include_images = includeImages;
|
|
667
|
+
if (includeImageDescriptions !== undefined)
|
|
668
|
+
body.include_image_descriptions = includeImageDescriptions;
|
|
669
|
+
if (includeFavicon !== undefined) body.include_favicon = includeFavicon;
|
|
670
|
+
if (country) body.country = country;
|
|
671
|
+
if (autoParameters !== undefined) body.auto_parameters = autoParameters;
|
|
354
672
|
|
|
355
673
|
// --- call Tavily ---
|
|
356
674
|
const start = Date.now();
|
|
@@ -364,7 +682,10 @@ const tavilyPlugin = {
|
|
|
364
682
|
|
|
365
683
|
const res = await fetch(TAVILY_SEARCH_ENDPOINT, {
|
|
366
684
|
method: "POST",
|
|
367
|
-
headers: {
|
|
685
|
+
headers: {
|
|
686
|
+
"Content-Type": "application/json",
|
|
687
|
+
Authorization: `Bearer ${apiKey}`,
|
|
688
|
+
},
|
|
368
689
|
body: JSON.stringify(body),
|
|
369
690
|
signal: controller.signal,
|
|
370
691
|
});
|
|
@@ -426,6 +747,7 @@ const tavilyPlugin = {
|
|
|
426
747
|
: {}),
|
|
427
748
|
score: r.score,
|
|
428
749
|
siteName: siteName(r.url) || undefined,
|
|
750
|
+
...(includeFavicon && r.favicon ? { favicon: r.favicon } : {}),
|
|
429
751
|
}));
|
|
430
752
|
|
|
431
753
|
const payload: Record<string, unknown> = {
|
|
@@ -465,6 +787,464 @@ const tavilyPlugin = {
|
|
|
465
787
|
{ source: "openclaw-tavily" },
|
|
466
788
|
);
|
|
467
789
|
|
|
790
|
+
// -----------------------------------------------------------------
|
|
791
|
+
// tavily_extract
|
|
792
|
+
// -----------------------------------------------------------------
|
|
793
|
+
api.registerTool(
|
|
794
|
+
{
|
|
795
|
+
name: "tavily_extract",
|
|
796
|
+
label: "Tavily Extract",
|
|
797
|
+
description:
|
|
798
|
+
"Extract and clean content from one or more URLs. Returns markdown or text content. " +
|
|
799
|
+
"Use when you need the full content of specific web pages.",
|
|
800
|
+
parameters: TavilyExtractSchema,
|
|
801
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
802
|
+
const urls = Array.isArray(params.urls)
|
|
803
|
+
? (params.urls as string[]).filter((u) => typeof u === "string" && u.trim())
|
|
804
|
+
: [];
|
|
805
|
+
if (urls.length === 0) {
|
|
806
|
+
return {
|
|
807
|
+
content: [
|
|
808
|
+
{
|
|
809
|
+
type: "text" as const,
|
|
810
|
+
text: JSON.stringify({
|
|
811
|
+
error: "missing_urls",
|
|
812
|
+
message: "At least one URL is required.",
|
|
813
|
+
}),
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
details: {},
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const body: Record<string, unknown> = { urls };
|
|
821
|
+
if (typeof params.query === "string" && params.query.trim())
|
|
822
|
+
body.query = params.query.trim();
|
|
823
|
+
if (typeof params.chunks_per_source === "number")
|
|
824
|
+
body.chunks_per_source = params.chunks_per_source;
|
|
825
|
+
if (typeof params.format === "string" && ["markdown", "text"].includes(params.format))
|
|
826
|
+
body.format = params.format;
|
|
827
|
+
if (typeof params.include_favicon === "boolean")
|
|
828
|
+
body.include_favicon = params.include_favicon;
|
|
829
|
+
if (typeof params.timeout === "number")
|
|
830
|
+
body.timeout = params.timeout;
|
|
831
|
+
|
|
832
|
+
const start = Date.now();
|
|
833
|
+
try {
|
|
834
|
+
const controller = new AbortController();
|
|
835
|
+
const timer = setTimeout(() => controller.abort(), defaultTimeout * 1000);
|
|
836
|
+
|
|
837
|
+
const res = await fetch(TAVILY_EXTRACT_ENDPOINT, {
|
|
838
|
+
method: "POST",
|
|
839
|
+
headers: {
|
|
840
|
+
"Content-Type": "application/json",
|
|
841
|
+
Authorization: `Bearer ${apiKey}`,
|
|
842
|
+
},
|
|
843
|
+
body: JSON.stringify(body),
|
|
844
|
+
signal: controller.signal,
|
|
845
|
+
});
|
|
846
|
+
clearTimeout(timer);
|
|
847
|
+
|
|
848
|
+
if (!res.ok) {
|
|
849
|
+
let detail = "";
|
|
850
|
+
try { detail = await res.text(); } catch {}
|
|
851
|
+
api.logger.warn(`tavily extract: API error ${res.status}: ${detail || res.statusText}`);
|
|
852
|
+
return {
|
|
853
|
+
content: [{
|
|
854
|
+
type: "text" as const,
|
|
855
|
+
text: JSON.stringify({ error: "tavily_api_error", status: res.status, message: detail || res.statusText }, null, 2),
|
|
856
|
+
}],
|
|
857
|
+
details: {},
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const data = await res.json() as Record<string, unknown>;
|
|
862
|
+
const tookMs = Date.now() - start;
|
|
863
|
+
const payload = { ...data, provider: "tavily", tookMs };
|
|
864
|
+
|
|
865
|
+
api.logger.info(`tavily extract: ${urls.length} URL(s) in ${tookMs}ms`);
|
|
866
|
+
return {
|
|
867
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
868
|
+
details: {},
|
|
869
|
+
};
|
|
870
|
+
} catch (err) {
|
|
871
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
872
|
+
api.logger.warn(`tavily extract: fetch error: ${msg}`);
|
|
873
|
+
return {
|
|
874
|
+
content: [{
|
|
875
|
+
type: "text" as const,
|
|
876
|
+
text: JSON.stringify({ error: "tavily_fetch_error", message: msg }, null, 2),
|
|
877
|
+
}],
|
|
878
|
+
details: {},
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
{ source: "openclaw-tavily" },
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
// -----------------------------------------------------------------
|
|
887
|
+
// tavily_crawl
|
|
888
|
+
// -----------------------------------------------------------------
|
|
889
|
+
api.registerTool(
|
|
890
|
+
{
|
|
891
|
+
name: "tavily_crawl",
|
|
892
|
+
label: "Tavily Crawl",
|
|
893
|
+
description:
|
|
894
|
+
"Crawl a website starting from a root URL. Traverses links and extracts content " +
|
|
895
|
+
"from discovered pages. Use for comprehensive site analysis.",
|
|
896
|
+
parameters: TavilyCrawlSchema,
|
|
897
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
898
|
+
const url = typeof params.url === "string" ? params.url.trim() : "";
|
|
899
|
+
if (!url) {
|
|
900
|
+
return {
|
|
901
|
+
content: [{
|
|
902
|
+
type: "text" as const,
|
|
903
|
+
text: JSON.stringify({ error: "missing_url", message: "A non-empty url is required." }),
|
|
904
|
+
}],
|
|
905
|
+
details: {},
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const body: Record<string, unknown> = { url };
|
|
910
|
+
if (typeof params.instructions === "string" && params.instructions.trim())
|
|
911
|
+
body.instructions = params.instructions.trim();
|
|
912
|
+
if (typeof params.max_depth === "number")
|
|
913
|
+
body.max_depth = Math.max(1, Math.min(5, Math.floor(params.max_depth)));
|
|
914
|
+
if (typeof params.max_breadth === "number")
|
|
915
|
+
body.max_breadth = Math.max(1, Math.min(500, Math.floor(params.max_breadth)));
|
|
916
|
+
if (typeof params.limit === "number")
|
|
917
|
+
body.limit = Math.floor(params.limit);
|
|
918
|
+
if (Array.isArray(params.select_paths) && params.select_paths.length > 0)
|
|
919
|
+
body.select_paths = params.select_paths;
|
|
920
|
+
if (Array.isArray(params.select_domains) && params.select_domains.length > 0)
|
|
921
|
+
body.select_domains = params.select_domains;
|
|
922
|
+
if (Array.isArray(params.exclude_paths) && params.exclude_paths.length > 0)
|
|
923
|
+
body.exclude_paths = params.exclude_paths;
|
|
924
|
+
if (Array.isArray(params.exclude_domains) && params.exclude_domains.length > 0)
|
|
925
|
+
body.exclude_domains = params.exclude_domains;
|
|
926
|
+
if (typeof params.allow_external === "boolean")
|
|
927
|
+
body.allow_external = params.allow_external;
|
|
928
|
+
if (typeof params.include_images === "boolean")
|
|
929
|
+
body.include_images = params.include_images;
|
|
930
|
+
if (typeof params.extract_depth === "string" && ["basic", "advanced"].includes(params.extract_depth))
|
|
931
|
+
body.extract_depth = params.extract_depth;
|
|
932
|
+
if (typeof params.format === "string" && ["markdown", "text"].includes(params.format))
|
|
933
|
+
body.format = params.format;
|
|
934
|
+
if (typeof params.include_favicon === "boolean")
|
|
935
|
+
body.include_favicon = params.include_favicon;
|
|
936
|
+
if (typeof params.timeout === "number")
|
|
937
|
+
body.timeout = Math.max(10, Math.min(150, params.timeout));
|
|
938
|
+
|
|
939
|
+
const start = Date.now();
|
|
940
|
+
try {
|
|
941
|
+
const controller = new AbortController();
|
|
942
|
+
const timer = setTimeout(() => controller.abort(), defaultTimeout * 1000);
|
|
943
|
+
|
|
944
|
+
const res = await fetch(TAVILY_CRAWL_ENDPOINT, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
headers: {
|
|
947
|
+
"Content-Type": "application/json",
|
|
948
|
+
Authorization: `Bearer ${apiKey}`,
|
|
949
|
+
},
|
|
950
|
+
body: JSON.stringify(body),
|
|
951
|
+
signal: controller.signal,
|
|
952
|
+
});
|
|
953
|
+
clearTimeout(timer);
|
|
954
|
+
|
|
955
|
+
if (!res.ok) {
|
|
956
|
+
let detail = "";
|
|
957
|
+
try { detail = await res.text(); } catch {}
|
|
958
|
+
api.logger.warn(`tavily crawl: API error ${res.status}: ${detail || res.statusText}`);
|
|
959
|
+
return {
|
|
960
|
+
content: [{
|
|
961
|
+
type: "text" as const,
|
|
962
|
+
text: JSON.stringify({ error: "tavily_api_error", status: res.status, message: detail || res.statusText }, null, 2),
|
|
963
|
+
}],
|
|
964
|
+
details: {},
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const data = await res.json() as Record<string, unknown>;
|
|
969
|
+
const tookMs = Date.now() - start;
|
|
970
|
+
const payload = { ...data, provider: "tavily", tookMs };
|
|
971
|
+
|
|
972
|
+
const resultCount = Array.isArray(data.results) ? data.results.length : 0;
|
|
973
|
+
api.logger.info(`tavily crawl: ${url} → ${resultCount} pages in ${tookMs}ms`);
|
|
974
|
+
return {
|
|
975
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
976
|
+
details: {},
|
|
977
|
+
};
|
|
978
|
+
} catch (err) {
|
|
979
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
980
|
+
api.logger.warn(`tavily crawl: fetch error: ${msg}`);
|
|
981
|
+
return {
|
|
982
|
+
content: [{
|
|
983
|
+
type: "text" as const,
|
|
984
|
+
text: JSON.stringify({ error: "tavily_fetch_error", message: msg }, null, 2),
|
|
985
|
+
}],
|
|
986
|
+
details: {},
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
},
|
|
990
|
+
},
|
|
991
|
+
{ source: "openclaw-tavily" },
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
// -----------------------------------------------------------------
|
|
995
|
+
// tavily_map
|
|
996
|
+
// -----------------------------------------------------------------
|
|
997
|
+
api.registerTool(
|
|
998
|
+
{
|
|
999
|
+
name: "tavily_map",
|
|
1000
|
+
label: "Tavily Map",
|
|
1001
|
+
description:
|
|
1002
|
+
"Generate a site map — discover and list all URLs from a website. " +
|
|
1003
|
+
"Use to understand site structure before targeted extraction.",
|
|
1004
|
+
parameters: TavilyMapSchema,
|
|
1005
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
1006
|
+
const url = typeof params.url === "string" ? params.url.trim() : "";
|
|
1007
|
+
if (!url) {
|
|
1008
|
+
return {
|
|
1009
|
+
content: [{
|
|
1010
|
+
type: "text" as const,
|
|
1011
|
+
text: JSON.stringify({ error: "missing_url", message: "A non-empty url is required." }),
|
|
1012
|
+
}],
|
|
1013
|
+
details: {},
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const body: Record<string, unknown> = { url };
|
|
1018
|
+
if (typeof params.instructions === "string" && params.instructions.trim())
|
|
1019
|
+
body.instructions = params.instructions.trim();
|
|
1020
|
+
if (typeof params.max_depth === "number")
|
|
1021
|
+
body.max_depth = Math.max(1, Math.min(5, Math.floor(params.max_depth)));
|
|
1022
|
+
if (typeof params.max_breadth === "number")
|
|
1023
|
+
body.max_breadth = Math.floor(params.max_breadth);
|
|
1024
|
+
if (typeof params.limit === "number")
|
|
1025
|
+
body.limit = Math.floor(params.limit);
|
|
1026
|
+
if (Array.isArray(params.select_paths) && params.select_paths.length > 0)
|
|
1027
|
+
body.select_paths = params.select_paths;
|
|
1028
|
+
if (Array.isArray(params.select_domains) && params.select_domains.length > 0)
|
|
1029
|
+
body.select_domains = params.select_domains;
|
|
1030
|
+
if (Array.isArray(params.exclude_paths) && params.exclude_paths.length > 0)
|
|
1031
|
+
body.exclude_paths = params.exclude_paths;
|
|
1032
|
+
if (Array.isArray(params.exclude_domains) && params.exclude_domains.length > 0)
|
|
1033
|
+
body.exclude_domains = params.exclude_domains;
|
|
1034
|
+
if (typeof params.allow_external === "boolean")
|
|
1035
|
+
body.allow_external = params.allow_external;
|
|
1036
|
+
if (typeof params.categories === "string" && params.categories.trim())
|
|
1037
|
+
body.categories = params.categories.trim();
|
|
1038
|
+
|
|
1039
|
+
const start = Date.now();
|
|
1040
|
+
try {
|
|
1041
|
+
const controller = new AbortController();
|
|
1042
|
+
const timer = setTimeout(() => controller.abort(), defaultTimeout * 1000);
|
|
1043
|
+
|
|
1044
|
+
const res = await fetch(TAVILY_MAP_ENDPOINT, {
|
|
1045
|
+
method: "POST",
|
|
1046
|
+
headers: {
|
|
1047
|
+
"Content-Type": "application/json",
|
|
1048
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1049
|
+
},
|
|
1050
|
+
body: JSON.stringify(body),
|
|
1051
|
+
signal: controller.signal,
|
|
1052
|
+
});
|
|
1053
|
+
clearTimeout(timer);
|
|
1054
|
+
|
|
1055
|
+
if (!res.ok) {
|
|
1056
|
+
let detail = "";
|
|
1057
|
+
try { detail = await res.text(); } catch {}
|
|
1058
|
+
api.logger.warn(`tavily map: API error ${res.status}: ${detail || res.statusText}`);
|
|
1059
|
+
return {
|
|
1060
|
+
content: [{
|
|
1061
|
+
type: "text" as const,
|
|
1062
|
+
text: JSON.stringify({ error: "tavily_api_error", status: res.status, message: detail || res.statusText }, null, 2),
|
|
1063
|
+
}],
|
|
1064
|
+
details: {},
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const data = await res.json() as Record<string, unknown>;
|
|
1069
|
+
const tookMs = Date.now() - start;
|
|
1070
|
+
const payload = { ...data, provider: "tavily", tookMs };
|
|
1071
|
+
|
|
1072
|
+
const urlCount = Array.isArray(data.results) ? data.results.length : 0;
|
|
1073
|
+
api.logger.info(`tavily map: ${url} → ${urlCount} URLs in ${tookMs}ms`);
|
|
1074
|
+
return {
|
|
1075
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
1076
|
+
details: {},
|
|
1077
|
+
};
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1080
|
+
api.logger.warn(`tavily map: fetch error: ${msg}`);
|
|
1081
|
+
return {
|
|
1082
|
+
content: [{
|
|
1083
|
+
type: "text" as const,
|
|
1084
|
+
text: JSON.stringify({ error: "tavily_fetch_error", message: msg }, null, 2),
|
|
1085
|
+
}],
|
|
1086
|
+
details: {},
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
{ source: "openclaw-tavily" },
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
// -----------------------------------------------------------------
|
|
1095
|
+
// tavily_research
|
|
1096
|
+
// -----------------------------------------------------------------
|
|
1097
|
+
api.registerTool(
|
|
1098
|
+
{
|
|
1099
|
+
name: "tavily_research",
|
|
1100
|
+
label: "Tavily Research",
|
|
1101
|
+
description:
|
|
1102
|
+
"Run a deep agentic research task. Tavily performs multi-step search and analysis, " +
|
|
1103
|
+
"returning a comprehensive report. Use for complex research questions.",
|
|
1104
|
+
parameters: TavilyResearchSchema,
|
|
1105
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
1106
|
+
const input = typeof params.input === "string" ? params.input.trim() : "";
|
|
1107
|
+
if (!input) {
|
|
1108
|
+
return {
|
|
1109
|
+
content: [{
|
|
1110
|
+
type: "text" as const,
|
|
1111
|
+
text: JSON.stringify({ error: "missing_input", message: "A non-empty input is required." }),
|
|
1112
|
+
}],
|
|
1113
|
+
details: {},
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const body: Record<string, unknown> = { input };
|
|
1118
|
+
if (typeof params.model === "string" && ["mini", "pro", "auto"].includes(params.model))
|
|
1119
|
+
body.model = params.model;
|
|
1120
|
+
if (params.output_schema && typeof params.output_schema === "object")
|
|
1121
|
+
body.output_schema = params.output_schema;
|
|
1122
|
+
if (typeof params.citation_format === "string" && ["numbered", "mla", "apa", "chicago"].includes(params.citation_format))
|
|
1123
|
+
body.citation_format = params.citation_format;
|
|
1124
|
+
|
|
1125
|
+
// Research is async — POST to create, then poll GET until complete
|
|
1126
|
+
const RESEARCH_POLL_INTERVAL = 2000; // 2s between polls
|
|
1127
|
+
const RESEARCH_MAX_WAIT = defaultTimeout * 5 * 1000; // 5x default timeout
|
|
1128
|
+
|
|
1129
|
+
const start = Date.now();
|
|
1130
|
+
try {
|
|
1131
|
+
// Step 1: Create the research task
|
|
1132
|
+
const createRes = await fetch(TAVILY_RESEARCH_ENDPOINT, {
|
|
1133
|
+
method: "POST",
|
|
1134
|
+
headers: {
|
|
1135
|
+
"Content-Type": "application/json",
|
|
1136
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1137
|
+
},
|
|
1138
|
+
body: JSON.stringify(body),
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
if (!createRes.ok) {
|
|
1142
|
+
let detail = "";
|
|
1143
|
+
try { detail = await createRes.text(); } catch {}
|
|
1144
|
+
api.logger.warn(`tavily research: API error ${createRes.status}: ${detail || createRes.statusText}`);
|
|
1145
|
+
return {
|
|
1146
|
+
content: [{
|
|
1147
|
+
type: "text" as const,
|
|
1148
|
+
text: JSON.stringify({ error: "tavily_api_error", status: createRes.status, message: detail || createRes.statusText }, null, 2),
|
|
1149
|
+
}],
|
|
1150
|
+
details: {},
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const createData = await createRes.json() as Record<string, unknown>;
|
|
1155
|
+
const requestId = createData.request_id as string | undefined;
|
|
1156
|
+
|
|
1157
|
+
// If the response already has content/output (not pending), return it
|
|
1158
|
+
if (createData.status !== "pending" || !requestId) {
|
|
1159
|
+
const tookMs = Date.now() - start;
|
|
1160
|
+
const payload = { ...createData, provider: "tavily", tookMs };
|
|
1161
|
+
api.logger.info(`tavily research: "${input.slice(0, 60)}" immediate in ${tookMs}ms`);
|
|
1162
|
+
return {
|
|
1163
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
1164
|
+
details: {},
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Step 2: Poll until complete or timeout
|
|
1169
|
+
api.logger.info(`tavily research: polling ${requestId} for "${input.slice(0, 60)}"...`);
|
|
1170
|
+
const pollUrl = `${TAVILY_RESEARCH_ENDPOINT}/${requestId}`;
|
|
1171
|
+
|
|
1172
|
+
while (Date.now() - start < RESEARCH_MAX_WAIT) {
|
|
1173
|
+
await new Promise((r) => setTimeout(r, RESEARCH_POLL_INTERVAL));
|
|
1174
|
+
|
|
1175
|
+
const pollRes = await fetch(pollUrl, {
|
|
1176
|
+
method: "GET",
|
|
1177
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
if (!pollRes.ok) {
|
|
1181
|
+
let detail = "";
|
|
1182
|
+
try { detail = await pollRes.text(); } catch {}
|
|
1183
|
+
api.logger.warn(`tavily research: poll error ${pollRes.status}: ${detail || pollRes.statusText}`);
|
|
1184
|
+
return {
|
|
1185
|
+
content: [{
|
|
1186
|
+
type: "text" as const,
|
|
1187
|
+
text: JSON.stringify({ error: "tavily_api_error", status: pollRes.status, message: detail || pollRes.statusText }, null, 2),
|
|
1188
|
+
}],
|
|
1189
|
+
details: {},
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const pollData = await pollRes.json() as Record<string, unknown>;
|
|
1194
|
+
|
|
1195
|
+
if (pollData.status === "completed" || pollData.content || pollData.output) {
|
|
1196
|
+
const tookMs = Date.now() - start;
|
|
1197
|
+
const payload = { ...pollData, provider: "tavily", tookMs };
|
|
1198
|
+
api.logger.info(`tavily research: "${input.slice(0, 60)}" completed in ${tookMs}ms`);
|
|
1199
|
+
return {
|
|
1200
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
1201
|
+
details: {},
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (pollData.status === "failed" || pollData.status === "error") {
|
|
1206
|
+
api.logger.warn(`tavily research: task failed: ${JSON.stringify(pollData)}`);
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{
|
|
1209
|
+
type: "text" as const,
|
|
1210
|
+
text: JSON.stringify({ error: "tavily_research_failed", ...pollData }, null, 2),
|
|
1211
|
+
}],
|
|
1212
|
+
details: {},
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
// still pending — continue polling
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Timeout
|
|
1219
|
+
const tookMs = Date.now() - start;
|
|
1220
|
+
api.logger.warn(`tavily research: timed out after ${tookMs}ms for "${input.slice(0, 60)}"`);
|
|
1221
|
+
return {
|
|
1222
|
+
content: [{
|
|
1223
|
+
type: "text" as const,
|
|
1224
|
+
text: JSON.stringify({
|
|
1225
|
+
error: "tavily_research_timeout",
|
|
1226
|
+
message: `Research task ${requestId} still pending after ${Math.round(tookMs / 1000)}s. Try again later.`,
|
|
1227
|
+
requestId,
|
|
1228
|
+
}, null, 2),
|
|
1229
|
+
}],
|
|
1230
|
+
details: {},
|
|
1231
|
+
};
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1234
|
+
api.logger.warn(`tavily research: fetch error: ${msg}`);
|
|
1235
|
+
return {
|
|
1236
|
+
content: [{
|
|
1237
|
+
type: "text" as const,
|
|
1238
|
+
text: JSON.stringify({ error: "tavily_fetch_error", message: msg }, null, 2),
|
|
1239
|
+
}],
|
|
1240
|
+
details: {},
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
{ source: "openclaw-tavily" },
|
|
1246
|
+
);
|
|
1247
|
+
|
|
468
1248
|
api.registerService({
|
|
469
1249
|
id: "openclaw-tavily",
|
|
470
1250
|
start: () => api.logger.info("tavily: service started"),
|