Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions chromium-extension/src/background/agent/chat-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChatService, uuidv4, ExaSearchService } from "@openbrowser-ai/core";
import { ChatService, uuidv4, ExaSearchService, TavilySearchService } from "@openbrowser-ai/core";
import {
OpenBrowserMessage,
WebSearchResult
Expand All @@ -17,9 +17,14 @@ export class SimpleChatService implements ChatService {
}
) => Promise<WebSearchResult[]>;

private searchProvider: "exa" | "tavily" = "exa";
private tavilyApiKey?: string;

constructor() {
chrome.storage.sync.get(["webSearchConfig"], (result) => {
if (result.webSearchConfig?.enabled) {
this.searchProvider = result.webSearchConfig.provider || "exa";
this.tavilyApiKey = result.webSearchConfig.tavilyApiKey;
this.websearch = (chatId, options) =>
this.websearchImpl(chatId, result.webSearchConfig.apiKey, options);
}
Expand Down Expand Up @@ -69,16 +74,28 @@ export class SimpleChatService implements ChatService {
}
): Promise<WebSearchResult[]> {
try {
const content = await ExaSearchService.search(
{
query: options.query,
numResults: options.numResults || 8,
type: options.type || "auto",
livecrawl: options.livecrawl || "fallback",
contextMaxCharacters: options.contextMaxCharacters || 10000
},
apiKey
);
let content: string;

if (this.searchProvider === "tavily" && this.tavilyApiKey) {
content = await TavilySearchService.search(
{
query: options.query,
numResults: options.numResults || 8
},
this.tavilyApiKey
);
} else {
content = await ExaSearchService.search(
{
query: options.query,
numResults: options.numResults || 8,
type: options.type || "auto",
livecrawl: options.livecrawl || "fallback",
contextMaxCharacters: options.contextMaxCharacters || 10000
},
apiKey
);
}

return [
{
Expand Down
111 changes: 82 additions & 29 deletions chromium-extension/src/options/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { Form, Input, Button, message, Select, Checkbox, Spin } from "antd";
import { Form, Input, Button, message, Select, Checkbox, Spin, Radio } from "antd";
import { SaveOutlined, LoadingOutlined } from "@ant-design/icons";
import "../sidebar/index.css";
import { ThemeProvider } from "../sidebar/providers/ThemeProvider";
Expand Down Expand Up @@ -34,7 +34,9 @@ const OptionsPage = () => {

const [webSearchConfig, setWebSearchConfig] = useState({
enabled: false,
apiKey: ""
apiKey: "",
provider: "exa" as "exa" | "tavily",
tavilyApiKey: ""
});

const [historyLLMConfig, setHistoryLLMConfig] = useState<Record<string, any>>(
Expand Down Expand Up @@ -124,10 +126,17 @@ const OptionsPage = () => {
setHistoryLLMConfig(result.historyLLMConfig);
}
if (result.webSearchConfig) {
setWebSearchConfig(result.webSearchConfig);
setWebSearchConfig({
enabled: result.webSearchConfig.enabled || false,
apiKey: result.webSearchConfig.apiKey || "",
provider: result.webSearchConfig.provider || "exa",
tavilyApiKey: result.webSearchConfig.tavilyApiKey || ""
});
form.setFieldsValue({
webSearchEnabled: result.webSearchConfig.enabled,
exaApiKey: result.webSearchConfig.apiKey
exaApiKey: result.webSearchConfig.apiKey,
webSearchProvider: result.webSearchConfig.provider || "exa",
tavilyApiKey: result.webSearchConfig.tavilyApiKey
});
}
}
Expand All @@ -138,7 +147,7 @@ const OptionsPage = () => {
form
.validateFields()
.then((value) => {
const { webSearchEnabled, exaApiKey, ...llmConfigValue } = value;
const { webSearchEnabled, exaApiKey, webSearchProvider, tavilyApiKey, ...llmConfigValue } = value;

setConfig(llmConfigValue);
setHistoryLLMConfig({
Expand All @@ -148,7 +157,9 @@ const OptionsPage = () => {

const newWebSearchConfig = {
enabled: webSearchEnabled || false,
apiKey: exaApiKey || ""
apiKey: exaApiKey || "",
provider: webSearchProvider || "exa",
tavilyApiKey: tavilyApiKey || ""
};
setWebSearchConfig(newWebSearchConfig);

Expand Down Expand Up @@ -401,41 +412,83 @@ const OptionsPage = () => {
>
<Checkbox className="checkbox-theme text-theme-primary">
<span className="text-sm font-medium text-theme-primary">
Enable web search (Exa AI)
Enable web search
</span>
</Checkbox>
</Form.Item>

<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.webSearchEnabled !== currentValues.webSearchEnabled
prevValues.webSearchEnabled !== currentValues.webSearchEnabled ||
prevValues.webSearchProvider !== currentValues.webSearchProvider
}
>
{({ getFieldValue }) =>
getFieldValue("webSearchEnabled") ? (
<Form.Item
name="exaApiKey"
label={
<span className="text-sm font-medium text-theme-primary">
Exa API Key{" "}
<span
className="text-theme-primary"
style={{ opacity: 0.5 }}
>
(Optional)
<>
<Form.Item
name="webSearchProvider"
label={
<span className="text-sm font-medium text-theme-primary">
Search Provider
</span>
</span>
}
tooltip="Uses free tier if not provided"
>
<Input.Password
placeholder="sk-..."
size="large"
className="w-full bg-theme-input border-theme-input text-theme-primary input-theme-focus radius-8px"
allowClear
/>
</Form.Item>
}
initialValue="exa"
>
<Radio.Group>
<Radio value="exa" className="text-theme-primary">Exa AI</Radio>
<Radio value="tavily" className="text-theme-primary">Tavily</Radio>
</Radio.Group>
</Form.Item>

{getFieldValue("webSearchProvider") === "tavily" ? (
<Form.Item
name="tavilyApiKey"
label={
<span className="text-sm font-medium text-theme-primary">
Tavily API Key
</span>
}
rules={[
{
required: true,
message: "Tavily API key is required"
}
]}
>
<Input.Password
placeholder="tvly-..."
size="large"
className="w-full bg-theme-input border-theme-input text-theme-primary input-theme-focus radius-8px"
allowClear
/>
</Form.Item>
) : (
<Form.Item
name="exaApiKey"
label={
<span className="text-sm font-medium text-theme-primary">
Exa API Key{" "}
<span
className="text-theme-primary"
style={{ opacity: 0.5 }}
>
(Optional)
</span>
</span>
}
tooltip="Uses free tier if not provided"
>
<Input.Password
placeholder="sk-..."
size="large"
className="w-full bg-theme-input border-theme-input text-theme-primary input-theme-focus radius-8px"
allowClear
/>
</Form.Item>
)}
</>
) : null
}
</Form.Item>
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export {
} from "./tools";

export type { ChatService, BrowserService } from "./service";
export { ExaSearchService } from "./service";
export { ExaSearchService, TavilySearchService } from "./service";

export {
sub,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/service/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChatService } from "./chat-service";
import { BrowserService } from "./browser-service";
export { ExaSearchService } from "./exa-search";
export { TavilySearchService } from "./tavily-search";

export type { ChatService, BrowserService };
99 changes: 99 additions & 0 deletions packages/core/src/service/tavily-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
export interface TavilySearchOptions {
query: string;
numResults?: number;
searchDepth?: "basic" | "advanced";
topic?: "general" | "news" | "finance";
}

interface TavilySearchResult {
title: string;
url: string;
content: string;
score: number;
}

interface TavilyResponse {
results: TavilySearchResult[];
answer?: string;
}

/**
* Service for performing web searches using the Tavily Search API
* Returns formatted content optimized for LLM consumption
*/
export class TavilySearchService {
private static readonly BASE_API_URL = "https://api.tavily.com/search";
private static readonly TIMEOUT_MS = 25000;

/**
* Performs a web search and returns formatted content from Tavily
* @param options Search options
* @param apiKey Tavily API key for authentication
* @returns Formatted text content for LLM consumption
*/
static async search(
options: TavilySearchOptions,
apiKey: string
): Promise<string> {
const {
query,
numResults = 8,
searchDepth = "basic",
topic = "general"
} = options;

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS);

try {
const response = await fetch(this.BASE_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
api_key: apiKey,
query,
max_results: numResults,
search_depth: searchDepth,
topic,
include_answer: false,
include_raw_content: false,
include_images: false
}),
signal: controller.signal
});

clearTimeout(timeoutId);

if (!response.ok) {
throw new Error(
`Tavily search failed: ${response.status} ${response.statusText}`
);
}

const data: TavilyResponse = await response.json();

if (!data.results || data.results.length === 0) {
return "No search results found. Please try a different query.";
}

// Format results for LLM consumption
const formattedResults = data.results
.map(
(result, index) =>
`[${index + 1}] ${result.title}\nURL: ${result.url}\n${result.content}`
)
.join("\n\n");

return formattedResults;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error("Tavily search timed out");
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
}