diff --git a/docs/configuration.md b/docs/configuration.md index 0d767be..6360018 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -135,7 +135,7 @@ file-system: ### HTTP -Enable `http` for Athena to send HTTP requests, search the web via Jina Search or Exa Search, and download files from the Internet. +Enable `http` for Athena to send HTTP requests, search the web via Jina Search, Exa Search, or Tavily Search, and download files from the Internet. ```yaml http: @@ -145,6 +145,9 @@ http: exa: # Optional Exa config base_url: https://api.exa.ai # Optional, defaults to this api_key: your-exa-api-key # Required if using Exa + tavily: # Optional Tavily config + base_url: https://api.tavily.com # Optional, defaults to this + api_key: your-tavily-api-key # Required if using Tavily ``` - `jina`: Configuration for [Jina Search](https://jina.ai/). (Optional) @@ -153,6 +156,9 @@ http: - `exa`: Configuration for [Exa Search](https://exa.ai/). (Optional) - `base_url`: The base URL of the Exa Search API endpoint. (Optional, defaults to `https://api.exa.ai`) - `api_key`: The API key for the Exa Search API endpoint. (Required if using Exa) +- `tavily`: Configuration for [Tavily Search](https://tavily.com/). (Optional) + - `base_url`: The base URL of the Tavily Search API endpoint. (Optional, defaults to `https://api.tavily.com`) + - `api_key`: The API key for the Tavily Search API endpoint. (Required if using Tavily) ### LLM diff --git a/src/plugins/http/init.ts b/src/plugins/http/init.ts index 7b62172..8939532 100644 --- a/src/plugins/http/init.ts +++ b/src/plugins/http/init.ts @@ -7,6 +7,7 @@ import { convert } from "html-to-text"; import { Athena, Dict } from "../../core/athena.js"; import { JinaSearch } from "./jina.js"; import { ExaSearch } from "./exa.js"; +import { TavilySearch } from "./tavily.js"; import { PluginBase } from "../plugin-base.js"; export default class Http extends PluginBase { @@ -22,6 +23,7 @@ export default class Http extends PluginBase { jina!: JinaSearch; exa!: ExaSearch; + tavily!: TavilySearch; boundAthenaPrivateEventHandler!: (name: string, args: Dict) => void; async load(athena: Athena) { @@ -37,6 +39,12 @@ export default class Http extends PluginBase { apiKey: this.config.exa.api_key, }); } + if (this.config.tavily) { + this.tavily = new TavilySearch({ + baseUrl: this.config.tavily.base_url, + apiKey: this.config.tavily.api_key, + }); + } this.boundAthenaPrivateEventHandler = this.athenaPrivateEventHandler.bind(this); athena.on("private-event", this.boundAthenaPrivateEventHandler); @@ -214,6 +222,63 @@ export default class Http extends PluginBase { }, ); } + if (this.config.tavily) { + athena.registerTool( + { + name: "http/tavily-search", + desc: "Searches the web for information using Tavily API.", + args: { + query: { + type: "string", + desc: "The query to search for.", + required: true, + }, + }, + retvals: { + results: { + type: "array", + desc: "The results of the search.", + required: true, + of: { + type: "object", + desc: "A single search result.", + of: { + title: { + type: "string", + desc: "The title of the result.", + required: true, + }, + url: { + type: "string", + desc: "The URL of the result.", + required: true, + }, + content: { + type: "string", + desc: "Text content snippet.", + required: true, + }, + }, + required: true, + }, + }, + }, + }, + { + fn: async (args: Dict) => { + const results = await this.tavily.search(args.query); + return { results }; + }, + explain_args: (args: Dict) => ({ + summary: `Searching the web with Tavily for ${args.query}...`, + }), + explain_retvals: (args: Dict, retvals: Dict) => ({ + summary: `Found ${retvals.results.length} results with Tavily for ${args.query}.`, + details: JSON.stringify(retvals.results), + }), + }, + ); + } athena.registerTool( { name: "http/download-file", @@ -289,6 +354,9 @@ export default class Http extends PluginBase { if (this.config.exa) { athena.deregisterTool("http/exa-search"); } + if (this.config.tavily) { + athena.deregisterTool("http/tavily-search"); + } athena.deregisterTool("http/download-file"); } @@ -306,6 +374,12 @@ export default class Http extends PluginBase { apiKey: this.config.exa.api_key || args.token, }); } + if (this.config.tavily) { + this.tavily = new TavilySearch({ + baseUrl: this.config.tavily.base_url, + apiKey: args.token, + }); + } } } } diff --git a/src/plugins/http/tavily.ts b/src/plugins/http/tavily.ts new file mode 100644 index 0000000..834a7fd --- /dev/null +++ b/src/plugins/http/tavily.ts @@ -0,0 +1,53 @@ +export interface ITavilySearchInitParams { + baseUrl?: string; + apiKey: string; +} + +export interface ITavilySearchResult { + title: string; + url: string; + content: string; // Text content from the search result +} + +export class TavilySearch { + baseUrl: string; + apiKey: string; + + constructor(initParams: ITavilySearchInitParams) { + this.baseUrl = initParams.baseUrl || "https://api.tavily.com"; + this.apiKey = initParams.apiKey; + } + + async search(query: string): Promise { + const response = await fetch(`${this.baseUrl}/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + query: query, + max_results: 10, // Request 10 results, adjust as needed + }), + }); + + if (!response.ok) { + const errorBody = await response.text(); + console.error("Tavily API Error:", response.status, errorBody); + throw new Error( + `Failed to search with Tavily API: ${response.statusText}`, + ); + } + + const json = await response.json(); + + // Map the response structure to ITavilySearchResult + const formattedResults = json.results.map((result: any) => ({ + title: result.title || "No Title", + url: result.url, + content: result.content || "No content", + })); + + return formattedResults; + } +}