This guide provides step-by-step instructions for implementing search plugins for the Eliza AI agent framework, with specific focus on Tavily and Exa search implementations.
- Node.js 23 or higher
- pnpm package manager
- TypeScript knowledge
- API keys for the search service you want to implement:
-
Clone the starter repository:
git clone https://github.com/tmc/eliza-plugin-starter cd eliza-plugin-starter pnpm install
-
Set up your environment:
# Create a .env file touch .env # Add your API keys echo "TAVILY_API_KEY=your_key_here" >> .env echo "EXA_API_KEY=your_key_here" >> .env
Each search plugin follows this structure:
// PLACEHOLDER: imports
export interface SearchPluginConfig {
apiKey: string;
maxResults?: number;
searchType?: string;
filters?: Record<string, unknown>;
}
export class SearchPlugin implements Plugin {
name: string;
description: string;
config: SearchPluginConfig;
actions: SearchAction[];
}
Please refer to the official documentation for further details about how to implement a plugin for your specific use case.
-
Create Plugin Configuration
interface TavilyPluginConfig extends SearchPluginConfig { searchType?: "search" | "news" | "academic"; } const DEFAULT_CONFIG: Partial<TavilyPluginConfig> = { maxResults: 5, searchType: "search", };
-
Implement Search Action
const TAVILY_SEARCH: SearchAction = { name: "TAVILY_SEARCH", description: "Search the web using Tavily API", // Refer to the documentation for proper examples of example array format, these should show the agent how the plugin is used, not be a single message that indicates what might trigger it. examples: [ [ { user: "user", content: { text: "Search for recent AI developments" }, }, ], ], // the similes array is just in case the model decides the action it's trying to perform is called something different today, which occasionally happens with small models. Mostly superfluous these days, but we still keep it around just in case. similes: ["tavilysearch", "tavily"], validate: async (runtime, message, state) => { try { validateSearchQuery(message.content); return true; } catch { return false; } }, handler: async (runtime, message, state) => { // Implementation details below }, };
-
Implement API Integration
async handler(runtime, message, state) { try { const query = validateSearchQuery(message.content); const response = await fetch('https://api.tavily.com/search', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.apiKey}`, }, body: JSON.stringify({ query, search_type: this.config.searchType, max_results: this.config.maxResults, }), }); // Process results const data = await response.json(); return { success: true, response: formatSearchResults(data.results), }; } catch (error) { return handleApiError(error); } }
-
Create Plugin Configuration
// It will be helpful here to examine the SearchPlugin that already exists in the @ai16z/eliza main repository to see what fields are required when defining a plugin interface, as well as how we extend them here. interface ExaPluginConfig extends SearchPluginConfig { searchType?: "semantic" | "code" | "document"; filters?: { language?: string; fileType?: string; }; } const DEFAULT_CONFIG: Partial<ExaPluginConfig> = { maxResults: 5, searchType: "semantic", };
-
Implement Search Action
const EXA_SEARCH: SearchAction = { name: "EXA_SEARCH", description: "Search using Exa API for semantic, code, and document search", // this is also a bad example of an examples array. I'll update this in the future to be more useful, but for now, refer to the official documenation for how the examples array ought to be formatted. examples: [ [ { user: "user", content: { text: "Find code examples for implementing OAuth" }, }, ], ], similes: ["exa", "exasearch"], validate: async (runtime, message, state) => { try { validateSearchQuery(message.content); return true; } catch { return false; } }, handler: async (runtime, message, state) => { // Implementation details below }, };
-
Implement API Integration
async handler(runtime, message, state) { try { const query = validateSearchQuery(message.content); const response = await fetch('https://api.exa.ai/search', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.apiKey}`, }, body: JSON.stringify({ query, search_type: this.config.searchType, max_results: this.config.maxResults, filters: this.config.filters, }), }); // Process results const data = await response.json(); return { success: true, response: formatSearchResults(data.results), }; } catch (error) { return handleApiError(error); } }
-
Unit Tests
describe("SearchPlugin", () => { it("should validate search queries", async () => { const plugin = new SearchPlugin(config); const result = await plugin.actions[0].validate(runtime, { content: { text: "test query" }, }); expect(result).toBe(true); }); });
-
Integration Testing
describe("API Integration", () => { it("should return search results", async () => { const plugin = new SearchPlugin(config); const results = await plugin.actions[0].handler(runtime, { content: { text: "test query" }, }); expect(results.success).toBe(true); expect(Array.isArray(results.response)).toBe(true); }); });
-
Rate Limiting
const rateLimiter = createRateLimiter(60, 60000); // 60 requests per minute if (!rateLimiter.checkLimit()) { return { success: false, response: "Rate limit exceeded. Please try again later.", }; }
-
Error Handling
try { // API calls } catch (error) { return handleApiError(error); }
-
Input Validation
function validateSearchQuery(content: Content): string { if (!content?.text) { throw new Error("Search query is required"); } return content.text.trim(); }
The core abstractions in the Eliza frawework are Action, Provider, and Evaluator. Both of these plugins only implement the Action abstraction -- they may function a lot more smoothly if returning the results as a Provider, and adding Evaluators that determine when or if these specific search services ought to be called. Further development on this repo will illustrate adding plugins which add additional Providers and Evaluators, as well as the preferred way to add external services not directly implemented within the plugin code. Hopefully, this is enough to get you started on the right track -- I STRONGLY encourage you to review the documentation as well as the first few episodes of Agent Dev School on youtube to guide you along your journey with Eliza. She'll be right there with you. :)