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"