feat(chat): add Querit web search provider (#17813)

This commit is contained in:
EthanZhang
2026-08-05 09:54:46 +08:00
committed by GitHub
parent 4d68e154ce
commit bdcd8aadde
32 changed files with 1253 additions and 134 deletions

View File

@@ -6,8 +6,6 @@ import { MetadataFilter } from '@/components/metadata-filter';
import { RerankFormFields } from '@/components/rerank';
import { SimilaritySliderFormField } from '@/components/similarity-slider';
import { SwitchFormField } from '@/components/switch-fom-field';
import { TavilyFormField } from '@/components/tavily-form-field';
import { TopNFormField } from '@/components/top-n-item';
import {
FormControl,
@@ -19,7 +17,7 @@ import {
import { MultiSelect } from '@/components/ui/multi-select';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { WebSearchFormField } from '@/components/web-search-form-field';
import { useFetchKnowledgeMetadataKeys } from '@/hooks/use-knowledge-request';
import { prefixName } from '@/utils/form';
import { getDirAttribute } from '@/utils/text-direction';
@@ -127,9 +125,7 @@ export function ChatPromptEngine({ prefix = '' }: ChatPromptEngineProps) {
label={t('chat.tts')}
tooltip={t('chat.ttsTip')}
></SwitchFormField>
<TavilyFormField
name={prefixName(prefix, 'prompt_config.tavily_api_key')}
></TavilyFormField>
<WebSearchFormField prefix={prefix} />
<MetadataFilter></MetadataFilter>
<FormField
control={form.control}

View File

@@ -22,6 +22,7 @@ import ChatBasicSetting from './chat-basic-settings';
import { ChatPromptEngine } from './chat-prompt-engine';
import { SavingButton } from './saving-button';
import { useChatSettingSchema } from './use-chat-setting-schema';
import { getWebSearchProvider } from '../web-search-api-key';
type ChatSettingsProps = { hasSingleChatBox: boolean };
@@ -135,6 +136,7 @@ export function ChatSettings({ hasSingleChatBox }: ChatSettingsProps) {
...data,
prompt_config: {
...data.prompt_config,
web_search_provider: getWebSearchProvider(data.prompt_config),
reference_metadata: normalizedReferenceMetadata,
},
...llmSettingEnabledValues,

View File

@@ -9,6 +9,7 @@ import {
vectorSimilarityWeightSchema,
} from '@/components/similarity-slider';
import { topnSchema } from '@/components/top-n-item';
import { WebSearchProvider } from '@/constants/chat';
import { useTranslate } from '@/hooks/common-hooks';
import { z } from 'zod';
@@ -32,6 +33,10 @@ export function useChatSettingSchema() {
)
.optional(),
tavily_api_key: z.string().optional(),
querit_api_key: z.string().optional(),
web_search_provider: z
.enum([WebSearchProvider.Tavily, WebSearchProvider.Querit])
.optional(),
reasoning: z.boolean().optional(),
cross_languages: z.array(z.string()).optional(),
reference_metadata: z

View File

@@ -0,0 +1,73 @@
import { WebSearchProvider } from '@/constants/chat';
import type { PromptConfig } from '@/interfaces/database/chat';
import { getWebSearchApiKey, getWebSearchProvider } from './web-search-api-key';
describe('getWebSearchProvider', () => {
it('does not select a provider for a new unconfigured dialog', () => {
expect(getWebSearchProvider({} as PromptConfig)).toBeUndefined();
});
it('selects Tavily for a legacy dialog with a Tavily key', () => {
const promptConfig = {
tavily_api_key: 'tvly-test',
} as PromptConfig;
expect(getWebSearchProvider(promptConfig)).toBe(WebSearchProvider.Tavily);
});
});
describe('getWebSearchApiKey', () => {
it('uses Tavily for dialogs saved before provider selection existed', () => {
const promptConfig = {
tavily_api_key: 'tvly-test',
} as PromptConfig;
expect(getWebSearchApiKey(promptConfig)).toBe('tvly-test');
});
it('uses only the selected Querit key', () => {
const promptConfig = {
web_search_provider: WebSearchProvider.Querit,
querit_api_key: 'querit-test',
tavily_api_key: 'tvly-test',
} as PromptConfig;
expect(getWebSearchApiKey(promptConfig)).toBe('querit-test');
});
it('does not fall back to Tavily when Querit is selected without a key', () => {
const promptConfig = {
web_search_provider: WebSearchProvider.Querit,
tavily_api_key: 'tvly-test',
} as PromptConfig;
expect(getWebSearchApiKey(promptConfig)).toBeUndefined();
});
it('treats a whitespace-only key as unconfigured', () => {
const promptConfig = {
web_search_provider: WebSearchProvider.Querit,
querit_api_key: ' ',
} as PromptConfig;
expect(getWebSearchApiKey(promptConfig)).toBe('');
});
it('does not fall back to Tavily for an unsupported provider', () => {
const promptConfig = {
web_search_provider: 'unsupported',
tavily_api_key: 'tvly-test',
} as unknown as PromptConfig;
expect(getWebSearchApiKey(promptConfig)).toBeUndefined();
});
it('treats a non-string key as unconfigured', () => {
const promptConfig = {
web_search_provider: WebSearchProvider.Querit,
querit_api_key: 123,
} as unknown as PromptConfig;
expect(getWebSearchApiKey(promptConfig)).toBeUndefined();
});
});

View File

@@ -1,8 +1,9 @@
import { useFetchChat } from '@/hooks/use-chat-request';
import { isEmpty } from 'lodash';
import { getWebSearchApiKey } from './web-search-api-key';
export function useShowInternet() {
const { data: currentDialog } = useFetchChat();
return !isEmpty(currentDialog?.prompt_config?.tavily_api_key);
return !isEmpty(getWebSearchApiKey(currentDialog?.prompt_config));
}

View File

@@ -0,0 +1,41 @@
import { WebSearchProvider } from '@/constants/chat';
import type { PromptConfig } from '@/interfaces/database/chat';
export function getWebSearchProvider(promptConfig?: PromptConfig) {
const provider = promptConfig?.web_search_provider;
if (
provider === WebSearchProvider.Tavily ||
provider === WebSearchProvider.Querit
) {
return provider;
}
if (
provider === undefined &&
typeof promptConfig?.tavily_api_key === 'string' &&
promptConfig.tavily_api_key.trim()
) {
return WebSearchProvider.Tavily;
}
return undefined;
}
export function getWebSearchApiKey(promptConfig?: PromptConfig) {
const provider = getWebSearchProvider(promptConfig);
let apiKey: unknown;
switch (provider) {
case WebSearchProvider.Tavily:
apiKey = promptConfig?.tavily_api_key;
break;
case WebSearchProvider.Querit:
apiKey = promptConfig?.querit_api_key;
break;
default:
return undefined;
}
return typeof apiKey === 'string' ? apiKey.trim() : undefined;
}

View File

@@ -115,7 +115,9 @@ const ChatContainer = () => {
showUploadIcon={false}
stopOutputMessage={stopOutputMessage}
showReasoning
showInternet={chatInfo?.has_tavily_key}
showInternet={
chatInfo?.has_web_search_provider ?? chatInfo?.has_tavily_key
}
></NextMessageInput>
</div>
</div>