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.
151 lines
5.1 KiB
151 lines
5.1 KiB
/** |
|
* 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;
|
|
|