From 910bd30b24bac48bcce3acd69559f2d428983194 Mon Sep 17 00:00:00 2001 From: Chen Tao <70054568+eeee0717@users.noreply.github.com> Date: Thu, 6 Mar 2025 22:44:43 +0800 Subject: [PATCH] feat: support exa engine (#2870) * feat: support exa engine * chore --- package.json | 1 + src/renderer/src/assets/images/search/exa.png | Bin 0 -> 1739 bytes .../src/assets/images/search/tavily-dark.svg | 14 ------ src/renderer/src/config/webSearchProviders.ts | 13 ++++-- .../src/pages/home/Inputbar/Inputbar.tsx | 1 + .../WebSearchProviderSetting.tsx | 10 +++-- src/renderer/src/services/ApiService.ts | 3 -- src/renderer/src/store/migrate.ts | 9 ++++ src/renderer/src/store/websearch.ts | 12 +++++ .../src/webSearchProvider/ExaProvider.ts | 41 ++++++++++++++++++ .../WebSearchProviderFactory.ts | 4 ++ yarn.lock | 13 ++++++ 12 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 src/renderer/src/assets/images/search/exa.png delete mode 100644 src/renderer/src/assets/images/search/tavily-dark.svg create mode 100644 src/renderer/src/webSearchProvider/ExaProvider.ts 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 0000000000000000000000000000000000000000..f6c2fe8937079706443ca91665c8fd659087ec11 GIT binary patch literal 1739 zcmV;+1~mDJP)BE1ZQLxAY_vg;$?V;oX(lt{{G@3qJ`_O^!KmPq;Qw2}Cn0L9?Tam?khm;cmfj@-EE=vY<#k5o0f^m&*sM6pBI0EL{uRZtU4R5Z z`^X8`V)zNfJ8YaF;u6EP09P2E5Ad?jnUOMxUqvh^s*L``+lU9}BlcPZ2XpY-jyyG} zttleQMaed+noc9ry@UIIwng1*#M^^UkFzj+6yF25_LnMhmf?pGpH<}CbcnAmX|u%; z|7E#p3GpKUUAw%6atiT2#A8)^yd3Cssj4^8z$>TJnP(8+gLrkpJj%xIMEtceUz`ec zeo~wa<_{hK;NmJRuzL3+9o0iVC08s$)vACv7|KExH6jmbd4gn8^0FkllzkRT6fd|P0n zrwSVAXrbt(XrcJk;OyVRDf#{Yw^VBW$m{6QFhZvPBk<6w!tZERBfJcLd1$M3&)TehHS09YRtkN(Z(GDi^F2;vKBX;HY+Q*UEnTqUvgh@A9J5aYSek}G;8j! zqp=F+X_rylpF&#LEj%b>g+0Q~5^H*5N-?MwSLAe5T{?GXqX;9WfX;*4pXw6&V*io?m|<6;W0GAb4f<-njQ>$>Q>9MPr@ zwU=n+0I+*9IKpp9{{0u*d8K7@p9VPmCp-6AwmA>rtOL;dXv;Q$XXCx^0IWWyc=l3w zC&Xgk0yx!!!@*Zi8qcdA>-AfBZYACT*nGWS|NH%Vee(m<-vGFv{{#Oecnig2*2@3@ z061k>NoGw=04e|g00;m9hiL!=000010000Q0000000N)_00aO40096103e_P00aO4 z0096103ZMW0056pK*<0A0yIfPK~z`??UyS|13?gmr{z)JK_W;nC=?Qj15X7GQh@;h z2?>e{P$VSOU=R%`5Qi_-6-X2ci2#8q&+_KmvT(Gwd%Gl1(@C1`-R{i)@67DKZDMHr z>P;GG$p&I8=q7=NXQ0*A1L=vBq$g9hC9!isp(4Ri#kkH&U~nKQW24n2=NGTS$JMmo z@yV0b#Y!OT3HbC-M6FRCh@GCjNN(@GcG~#tOn?aapd!W6ql^r<{OE_wREJO= znr?3O+6Wdk;YEM|%Ny0&0+^m`SG%U^Fu;9|Dn8x>=m&rpv-91e>rQz{zppvz0BSfk z+Ey2ve&E9?p*+7a?+p9~#OC9a0rWiF*>rnpzh9MsQs@SZQ@to6-WdR#x?|KGc~<@K zAwZlCU-Kq_&O<_r4PBqxyHmI}kunkUA^QF2nS3~*6}HKf+ZO5ptRMP0+SPA<<}-xx7$mT3Mpv!&-h h=A2K#Uy*4zfp^zM?bdo{x1<07002ovPDHLkV1m%pL(c#J literal 0 HcmV?d00001 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"