diff --git a/package.json b/package.json index 740aec3f..44d4d04f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "test": "tsx --test src/**/*.test.ts" }, "dependencies": { + "@agentic/exa": "^7.3.3", "@agentic/searxng": "^7.3.3", "@agentic/tavily": "^7.3.3", "@electron-toolkit/preload": "^3.0.0", diff --git a/src/renderer/src/assets/images/search/exa.png b/src/renderer/src/assets/images/search/exa.png new file mode 100644 index 00000000..f6c2fe89 Binary files /dev/null and b/src/renderer/src/assets/images/search/exa.png differ diff --git a/src/renderer/src/assets/images/search/tavily-dark.svg b/src/renderer/src/assets/images/search/tavily-dark.svg deleted file mode 100644 index bd1995a9..00000000 --- a/src/renderer/src/assets/images/search/tavily-dark.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/renderer/src/config/webSearchProviders.ts b/src/renderer/src/config/webSearchProviders.ts index f65a0045..7901adee 100644 --- a/src/renderer/src/config/webSearchProviders.ts +++ b/src/renderer/src/config/webSearchProviders.ts @@ -1,15 +1,14 @@ +import ExaLogo from '@renderer/assets/images/search/exa.png' import SearxngLogo from '@renderer/assets/images/search/searxng.svg' import TavilyLogo from '@renderer/assets/images/search/tavily.png' -import TavilyLogoDark from '@renderer/assets/images/search/tavily-dark.svg' export function getWebSearchProviderLogo(providerId: string) { switch (providerId) { case 'tavily': return TavilyLogo - case 'tavily-dark': - return TavilyLogoDark case 'searxng': return SearxngLogo - + case 'exa': + return ExaLogo default: return undefined } @@ -26,5 +25,11 @@ export const WEB_SEARCH_PROVIDER_CONFIG = { websites: { official: 'https://docs.searxng.org' } + }, + exa: { + websites: { + official: 'https://exa.ai', + apiKey: 'https://dashboard.exa.ai/api-keys' + } } } diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 82a458ce..53b16c2f 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -588,6 +588,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic }) => { } const onEnableWebSearch = () => { + console.log(assistant) if (!isWebSearchModel(model)) { if (!WebSearchService.isWebSearchEnabled()) { window.modal.confirm({ diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx index 331cc645..b546bee8 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx @@ -1,10 +1,10 @@ import { CheckOutlined, ExportOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons' -import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders' +import { getWebSearchProviderLogo, WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders' import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders' import WebSearchService from '@renderer/services/WebSearchService' import { WebSearchProvider } from '@renderer/types' import { hasObjectKey } from '@renderer/utils' -import { Button, Divider, Flex, Input } from 'antd' +import { Avatar, Button, Divider, Flex, Input } from 'antd' import Link from 'antd/es/typography/Link' import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -68,7 +68,6 @@ const WebSearchProviderSetting: FC = ({ provider: _provider }) => { key: 'search-check-error' }) } - updateProvider({ ...provider, enabled: true }) } catch (err) { console.error('Check search error:', err) setApiValid(false) @@ -92,6 +91,8 @@ const WebSearchProviderSetting: FC = ({ provider: _provider }) => { <> + + {provider.name} {officialWebsite && webSearchProviderConfig?.websites && ( @@ -151,5 +152,8 @@ const ProviderName = styled.span` font-size: 14px; font-weight: 500; ` +const ProviderLogo = styled(Avatar)` + border: 0.5px solid var(--color-border); +` export default WebSearchProviderSetting diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index b9c02699..55028bfe 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -68,14 +68,11 @@ export async function fetchChatCompletion({ }) } onResponse({ ...message, status: 'searching' }) - console.log('webSearchProvider', webSearchProvider) const webSearch = await WebSearchService.search(webSearchProvider, lastMessage.content) - console.log('webSearch', webSearch) message.metadata = { ...message.metadata, webSearch: webSearch } - console.log('message', message) window.keyv.set(`web-search-${lastMessage?.id}`, webSearch) } } diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 49a854bb..9d6ccc29 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1231,6 +1231,15 @@ const migrateConfig = { }) } + return state + }, + '77': (state: RootState) => { + state.websearch.providers.push({ + id: 'exa', + name: 'Exa', + enabled: false, + apiKey: '' + }) return state } } diff --git a/src/renderer/src/store/websearch.ts b/src/renderer/src/store/websearch.ts index 6eeb83d1..110ef45f 100644 --- a/src/renderer/src/store/websearch.ts +++ b/src/renderer/src/store/websearch.ts @@ -20,6 +20,18 @@ const initialState: WebSearchState = { id: 'searxng', name: 'Searxng', apiHost: '' + }, + { + id: 'exa', + name: 'Exa', + enabled: false, + apiKey: '' + }, + { + id: 'searxng', + name: 'Searxng', + enabled: false, + apiHost: '' } ], searchWithTime: true, diff --git a/src/renderer/src/webSearchProvider/ExaProvider.ts b/src/renderer/src/webSearchProvider/ExaProvider.ts new file mode 100644 index 00000000..820845d7 --- /dev/null +++ b/src/renderer/src/webSearchProvider/ExaProvider.ts @@ -0,0 +1,41 @@ +import { ExaClient } from '@agentic/exa' +import { WebSearchProvider, WebSearchResponse } from '@renderer/types' + +import BaseWebSearchProvider from './BaseWebSearchProvider' + +export default class ExaProvider extends BaseWebSearchProvider { + private exa: ExaClient + + constructor(provider: WebSearchProvider) { + super(provider) + if (!provider.apiKey) { + throw new Error('API key is required for Exa provider') + } + this.exa = new ExaClient({ apiKey: provider.apiKey }) + } + + public async search(query: string, maxResults: number): Promise { + try { + if (!query.trim()) { + throw new Error('Search query cannot be empty') + } + + const response = await this.exa.search({ + query, + numResults: Math.max(1, maxResults) + }) + + return { + query: response.autopromptString, + results: response.results.map((result) => ({ + title: result.title || 'No title', + content: result.text || '', + url: result.url || '' + })) + } + } catch (error) { + console.error('Exa search failed:', error) + throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + } +} diff --git a/src/renderer/src/webSearchProvider/WebSearchProviderFactory.ts b/src/renderer/src/webSearchProvider/WebSearchProviderFactory.ts index f2735959..6c57f34b 100644 --- a/src/renderer/src/webSearchProvider/WebSearchProviderFactory.ts +++ b/src/renderer/src/webSearchProvider/WebSearchProviderFactory.ts @@ -2,6 +2,7 @@ import { WebSearchProvider } from '@renderer/types' import BaseWebSearchProvider from './BaseWebSearchProvider' import DefaultProvider from './DefaultProvider' +import ExaProvider from './ExaProvider' import SearxngProvider from './SearxngProvider' import TavilyProvider from './TavilyProvider' @@ -12,6 +13,9 @@ export default class WebSearchProviderFactory { return new TavilyProvider(provider) case 'searxng': return new SearxngProvider(provider) + case 'exa': + return new ExaProvider(provider) + default: return new DefaultProvider(provider) } diff --git a/yarn.lock b/yarn.lock index afcaf616..6bf2cd90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,6 +31,18 @@ __metadata: languageName: node linkType: hard +"@agentic/exa@npm:^7.3.3": + version: 7.3.3 + resolution: "@agentic/exa@npm:7.3.3" + dependencies: + "@agentic/core": "npm:7.3.3" + ky: "npm:^1.7.5" + peerDependencies: + zod: ^3.24.2 + checksum: 10c0/0293d9f7cda2b17669c853f0834a189988ab8bd8d219f0916d894786143ac732a5c2a669ba112b5edf45b8574473c25a0bf5537c23d666df3132624fca16741e + languageName: node + linkType: hard + "@agentic/searxng@npm:^7.3.3": version: 7.3.3 resolution: "@agentic/searxng@npm:7.3.3" @@ -3094,6 +3106,7 @@ __metadata: version: 0.0.0-use.local resolution: "CherryStudio@workspace:." dependencies: + "@agentic/exa": "npm:^7.3.3" "@agentic/searxng": "npm:^7.3.3" "@agentic/tavily": "npm:^7.3.3" "@anthropic-ai/sdk": "npm:^0.38.0"