From ccafc4c84c55628c1958abff31fccff1807901a2 Mon Sep 17 00:00:00 2001 From: Teo Gonzalez Collazo Date: Thu, 9 Apr 2026 11:48:35 -0700 Subject: [PATCH] feat(example): add Exa-powered web search via ChatService Implements ExaChatService using the exa-js SDK so Eko's built-in WebSearchTool returns real search results instead of empty arrays. Includes the x-exa-integration header for usage tracking. Co-Authored-By: Claude Opus 4.6 (1M context) --- example/nodejs/package.json | 1 + example/nodejs/src/exa-chat-service.ts | 91 ++++++++++++++++++++++++++ example/nodejs/src/index.ts | 7 +- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 example/nodejs/src/exa-chat-service.ts diff --git a/example/nodejs/package.json b/example/nodejs/package.json index 939cb36d..dc345850 100644 --- a/example/nodejs/package.json +++ b/example/nodejs/package.json @@ -18,6 +18,7 @@ "dependencies": { "@eko-ai/eko": "workspace:*", "@eko-ai/eko-nodejs": "workspace:*", + "exa-js": "^2.11.0", "canvas": "^3.2.0", "glob": "^11.0.2", "keytar": "^7.9.0", diff --git a/example/nodejs/src/exa-chat-service.ts b/example/nodejs/src/exa-chat-service.ts new file mode 100644 index 00000000..4ed49fd6 --- /dev/null +++ b/example/nodejs/src/exa-chat-service.ts @@ -0,0 +1,91 @@ +import Exa from 'exa-js'; +import { ChatService, uuidv4 } from '@eko-ai/eko'; +import { EkoMessage, WebSearchResult } from '@eko-ai/eko/types'; + +/** + * ChatService implementation powered by Exa (https://exa.ai) for web search. + * + * Exa is an AI-native search engine that returns clean, structured results + * optimised for agent pipelines. Set the EXA_API_KEY environment variable + * before constructing this service. + * + * Usage: + * import { ExaChatService } from './exa-chat-service'; + * global.chatService = new ExaChatService(); + */ +export class ExaChatService implements ChatService { + private readonly exa: Exa; + + public constructor(apiKey?: string) { + const key = apiKey ?? process.env.EXA_API_KEY; + if (!key) { + throw new Error( + 'EXA_API_KEY is required. Get one at https://dashboard.exa.ai/api-keys' + ); + } + this.exa = new Exa(key); + // Integration header for Exa usage tracking + const headers = (this.exa as any).headers; + if (headers && typeof headers.set === 'function') { + headers.set('x-exa-integration', 'eko'); + } + } + + // -- ChatService: message persistence (no-op, override if needed) ---------- + + public loadMessages(_chatId: string): Promise { + return Promise.resolve([]); + } + + public addMessage(_chatId: string, _messages: EkoMessage[]): Promise { + return Promise.resolve(); + } + + public memoryRecall(_chatId: string, _prompt: string): Promise { + return Promise.resolve(''); + } + + public async uploadFile( + file: { base64Data: string; mimeType: string; filename?: string }, + _chatId: string, + _taskId?: string | undefined + ): Promise<{ fileId: string; url: string }> { + return { + fileId: uuidv4(), + url: file.base64Data.startsWith('data:') + ? file.base64Data + : `data:${file.mimeType};base64,${file.base64Data}`, + }; + } + + // -- ChatService: web search via Exa -------------------------------------- + + public async websearch( + _chatId: string, + query: string, + site?: string, + _language?: string, + maxResults?: number + ): Promise { + const numResults = Math.min(maxResults ?? 10, 50); + + const response = await this.exa.search(query, { + type: 'auto', + numResults, + contents: { + highlights: true, + text: true, + }, + ...(site ? { includeDomains: [site] } : {}), + }); + + return response.results.map((result) => ({ + title: result.title ?? '', + url: result.url, + snippet: + result.highlights?.join(' ') ?? + (result.text ? result.text.slice(0, 300) : ''), + content: result.text ?? undefined, + })); + } +} diff --git a/example/nodejs/src/index.ts b/example/nodejs/src/index.ts index e60b6e98..3d40a3ec 100644 --- a/example/nodejs/src/index.ts +++ b/example/nodejs/src/index.ts @@ -1,8 +1,9 @@ import dotenv from "dotenv"; import FileAgent from "./file-agent"; import LocalCookiesBrowserAgent from "./browser"; +import { ExaChatService } from "./exa-chat-service"; import { BrowserAgent } from "@eko-ai/eko-nodejs"; -import { Eko, Log, LLMs, Agent, AgentStreamMessage } from "@eko-ai/eko"; +import { Eko, Log, LLMs, Agent, AgentStreamMessage, global } from "@eko-ai/eko"; dotenv.config(); @@ -44,6 +45,10 @@ function testBrowserLoginStatus() { async function run() { Log.setLevel(1); + // Enable Exa-powered web search (requires EXA_API_KEY in .env) + if (process.env.EXA_API_KEY) { + global.chatService = new ExaChatService(); + } // Use local browser cookie login state, will read local Chrome's cookie and localStorage information // If a password dialog pops up, please enter your computer password and click "Always Allow" const agents: Agent[] = [