You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
152 lines
5.1 KiB
152 lines
5.1 KiB
|
4 weeks ago
|
/**
|
||
|
|
* Tavily AI Search - OpenClaw Plugin (OpenClaw 2026 api.registerTool API)
|
||
|
|
* Provides web search optimized for AI/LLM consumption.
|
||
|
|
* @see https://docs.openclaw.ai/plugins/agent-tools
|
||
|
|
*/
|
||
|
|
|
||
|
|
const https = require('https');
|
||
|
|
|
||
|
|
const TAVILY_API_KEY = process.env.TAVILY_API_KEY || '';
|
||
|
|
const TAVILY_API_HOST = 'api.tavily.com';
|
||
|
|
|
||
|
|
function tavilySearch(query, options = {}) {
|
||
|
|
const {
|
||
|
|
search_depth = 'basic',
|
||
|
|
topic = 'general',
|
||
|
|
max_results = 5,
|
||
|
|
include_answer = true,
|
||
|
|
include_raw_content = false,
|
||
|
|
include_images = false,
|
||
|
|
include_domains = null,
|
||
|
|
exclude_domains = null,
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const requestBody = JSON.stringify({
|
||
|
|
api_key: TAVILY_API_KEY,
|
||
|
|
query,
|
||
|
|
search_depth,
|
||
|
|
topic,
|
||
|
|
max_results,
|
||
|
|
include_answer,
|
||
|
|
include_raw_content,
|
||
|
|
include_images,
|
||
|
|
include_domains,
|
||
|
|
exclude_domains,
|
||
|
|
});
|
||
|
|
|
||
|
|
const reqOptions = {
|
||
|
|
hostname: TAVILY_API_HOST,
|
||
|
|
port: 443,
|
||
|
|
path: '/search',
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Content-Length': Buffer.byteLength(requestBody),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const req = https.request(reqOptions, (res) => {
|
||
|
|
let data = '';
|
||
|
|
res.on('data', (chunk) => (data += chunk));
|
||
|
|
res.on('end', () => {
|
||
|
|
try {
|
||
|
|
resolve(JSON.parse(data));
|
||
|
|
} catch (e) {
|
||
|
|
reject(new Error(`Failed to parse Tavily response: ${e.message}`));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
req.on('error', (e) => reject(new Error(`Tavily API request failed: ${e.message}`)));
|
||
|
|
req.write(requestBody);
|
||
|
|
req.end();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* OpenClaw 2026 plugin entry: register tool via api.registerTool()
|
||
|
|
*/
|
||
|
|
function register(api) {
|
||
|
|
const hasRegisterTool = typeof api === 'object' && typeof api.registerTool === 'function';
|
||
|
|
if (api && api.logger && typeof api.logger.info === 'function') {
|
||
|
|
api.logger.info('[Tavily] plugin loaded, hasRegisterTool=' + hasRegisterTool);
|
||
|
|
} else {
|
||
|
|
console.log('[Tavily] plugin loaded, hasRegisterTool=' + hasRegisterTool);
|
||
|
|
}
|
||
|
|
if (!hasRegisterTool) {
|
||
|
|
// Legacy loader: return tools array for older gateways
|
||
|
|
const tool = {
|
||
|
|
name: 'tavily_search',
|
||
|
|
description: 'AI-optimized web search using Tavily. Best for research, news, fact-checking, and gathering authoritative sources.',
|
||
|
|
input_schema: {
|
||
|
|
type: 'object',
|
||
|
|
properties: {
|
||
|
|
query: { type: 'string', description: 'Search query string' },
|
||
|
|
search_depth: { type: 'string', enum: ['basic', 'advanced'], default: 'basic' },
|
||
|
|
topic: { type: 'string', enum: ['general', 'news'], default: 'general' },
|
||
|
|
max_results: { type: 'integer', minimum: 1, maximum: 10, default: 5 },
|
||
|
|
include_answer: { type: 'boolean', default: true },
|
||
|
|
},
|
||
|
|
required: ['query'],
|
||
|
|
},
|
||
|
|
async execute(params) {
|
||
|
|
const out = await runTavily(params);
|
||
|
|
return { content: [{ type: 'text', text: typeof out === 'string' ? out : JSON.stringify(out, null, 2) }] };
|
||
|
|
},
|
||
|
|
};
|
||
|
|
return { tools: [tool] };
|
||
|
|
}
|
||
|
|
|
||
|
|
api.registerTool(
|
||
|
|
{
|
||
|
|
name: 'tavily_search',
|
||
|
|
description: 'AI-optimized web search using Tavily. Best for research, news, fact-checking, and gathering authoritative sources.',
|
||
|
|
parameters: {
|
||
|
|
type: 'object',
|
||
|
|
properties: {
|
||
|
|
query: { type: 'string', description: 'Search query string' },
|
||
|
|
search_depth: { type: 'string', enum: ['basic', 'advanced'], description: 'basic (fast) or advanced (comprehensive)', default: 'basic' },
|
||
|
|
topic: { type: 'string', enum: ['general', 'news'], description: 'general or news (last 7 days)', default: 'general' },
|
||
|
|
max_results: { type: 'integer', minimum: 1, maximum: 10, description: 'Number of results', default: 5 },
|
||
|
|
include_answer: { type: 'boolean', description: 'Include AI-generated answer summary', default: true },
|
||
|
|
},
|
||
|
|
required: ['query'],
|
||
|
|
},
|
||
|
|
async execute(_id, params) {
|
||
|
|
const out = await runTavily(params);
|
||
|
|
return { content: [{ type: 'text', text: typeof out === 'string' ? out : JSON.stringify(out, null, 2) }] };
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{ optional: true }
|
||
|
|
);
|
||
|
|
if (api.logger && typeof api.logger.info === 'function') {
|
||
|
|
api.logger.info('[Tavily] tavily_search registered via api.registerTool');
|
||
|
|
} else {
|
||
|
|
console.log('[Tavily] tavily_search registered via api.registerTool');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function runTavily(params) {
|
||
|
|
if (!TAVILY_API_KEY) {
|
||
|
|
return { success: false, error: 'TAVILY_API_KEY is not set. Set it in openclaw.json env or gateway environment.' };
|
||
|
|
}
|
||
|
|
const { query, ...options } = params || {};
|
||
|
|
try {
|
||
|
|
const result = await tavilySearch(query || '', options);
|
||
|
|
if (result.error) return { success: false, error: result.error };
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
query: result.query,
|
||
|
|
answer: result.answer,
|
||
|
|
results: (result.results || []).map((r) => ({ title: r.title, url: r.url, content: r.content, score: r.score })),
|
||
|
|
images: result.images || [],
|
||
|
|
response_time: result.response_time,
|
||
|
|
};
|
||
|
|
} catch (err) {
|
||
|
|
return { success: false, error: err.message };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = register;
|