mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-10 09:21:25 +08:00
feat(chat): add Querit web search provider (#17813)
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
@@ -1,51 +0,0 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import PasswordInput from './originui/password-input';
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form';
|
||||
|
||||
interface IProps {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function TavilyFormField({
|
||||
name = 'prompt_config.tavily_api_key',
|
||||
}: IProps) {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('tavilyApiKeyTip')}>Tavily API Key</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
{...field}
|
||||
placeholder={t('tavilyApiKeyMessage')}
|
||||
autoComplete="new-password"
|
||||
></PasswordInput>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<a
|
||||
href="https://app.tavily.com/home"
|
||||
target={'_blank'}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('tavilyApiKeyHelp')}
|
||||
</a>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
131
web/src/components/web-search-form-field.tsx
Normal file
131
web/src/components/web-search-form-field.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import queritLogo from '@/assets/querit.png';
|
||||
import tavilyLogo from '@/assets/svg/tavily.svg';
|
||||
import { RAGFlowSelect } from '@/components/ui/select';
|
||||
import { WebSearchProvider } from '@/constants/chat';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { prefixName } from '@/utils/form';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import PasswordInput from './originui/password-input';
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form';
|
||||
|
||||
interface IProps {
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
const providerOptions = [
|
||||
{
|
||||
name: 'Tavily',
|
||||
logo: tavilyLogo,
|
||||
value: WebSearchProvider.Tavily,
|
||||
},
|
||||
{
|
||||
name: 'Querit',
|
||||
logo: queritLogo,
|
||||
value: WebSearchProvider.Querit,
|
||||
},
|
||||
]
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
.map(({ name, logo, value }) => ({
|
||||
label: (
|
||||
<span className="flex items-center gap-2">
|
||||
<img
|
||||
src={logo}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="size-4 shrink-0 object-contain"
|
||||
/>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
value,
|
||||
}));
|
||||
|
||||
const providerKeyConfig = {
|
||||
[WebSearchProvider.Tavily]: {
|
||||
name: 'prompt_config.tavily_api_key',
|
||||
label: 'Tavily API Key',
|
||||
tip: 'tavilyApiKeyTip',
|
||||
placeholder: 'tavilyApiKeyMessage',
|
||||
helpUrl: 'https://app.tavily.com/home',
|
||||
},
|
||||
[WebSearchProvider.Querit]: {
|
||||
name: 'prompt_config.querit_api_key',
|
||||
label: 'Querit API Key',
|
||||
tip: 'queritApiKeyTip',
|
||||
placeholder: 'queritApiKeyMessage',
|
||||
helpUrl: 'https://querit.ai',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function WebSearchFormField({ prefix = '' }: IProps) {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslate('chat');
|
||||
const providerName = prefixName(prefix, 'prompt_config.web_search_provider');
|
||||
const selectedProvider = useWatch({
|
||||
control: form.control,
|
||||
name: providerName,
|
||||
});
|
||||
const keyConfig = providerKeyConfig[selectedProvider as WebSearchProvider];
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={providerName}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('webSearchProviderTip')}>
|
||||
{t('webSearchProvider')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
value={field.value}
|
||||
options={providerOptions}
|
||||
placeholder={t('webSearchProviderPlaceholder')}
|
||||
triggerTestId="web-search-provider"
|
||||
optionTestIdPrefix="web-search-provider-option"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{keyConfig && (
|
||||
<FormField
|
||||
key={selectedProvider}
|
||||
control={form.control}
|
||||
name={prefixName(prefix, keyConfig.name)}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t(keyConfig.tip)}>
|
||||
{keyConfig.label}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
placeholder={t(keyConfig.placeholder)}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<a href={keyConfig.helpUrl} target="_blank" rel="noreferrer">
|
||||
{t('tavilyApiKeyHelp')}
|
||||
</a>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -39,3 +39,8 @@ export enum DatasetMetadata {
|
||||
SemiAutomatic = 'semi_auto',
|
||||
Manual = 'manual',
|
||||
}
|
||||
|
||||
export enum WebSearchProvider {
|
||||
Tavily = 'tavily',
|
||||
Querit = 'querit',
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MessageType } from '@/constants/chat';
|
||||
import { MessageType, WebSearchProvider } from '@/constants/chat';
|
||||
import { IAttachment } from '@/hooks/use-send-message';
|
||||
|
||||
export interface IDocumentDownloadInfo {
|
||||
@@ -21,6 +21,8 @@ export interface PromptConfig {
|
||||
reasoning?: boolean;
|
||||
cross_languages?: Array<string>;
|
||||
tavily_api_key?: string;
|
||||
querit_api_key?: string;
|
||||
web_search_provider?: WebSearchProvider;
|
||||
toc_enhance?: boolean;
|
||||
reference_metadata?: {
|
||||
include?: boolean;
|
||||
@@ -202,6 +204,7 @@ export interface IExternalChatInfo {
|
||||
title: string;
|
||||
prologue?: string;
|
||||
has_tavily_key?: boolean;
|
||||
has_web_search_provider?: boolean;
|
||||
llm_id?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1244,6 +1244,13 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
tavilyApiKeyTip:
|
||||
'If an API Key is correctly set here, Tavily-based web searches will be used to supplement dataset retrieval.',
|
||||
tavilyApiKeyMessage: 'Please enter your Tavily API Key',
|
||||
webSearchProvider: 'Web search provider',
|
||||
webSearchProviderTip:
|
||||
'Select the service used when Internet search is enabled.',
|
||||
webSearchProviderPlaceholder: 'Select a web search provider',
|
||||
queritApiKeyTip:
|
||||
'When Querit is selected, its web search results supplement dataset retrieval.',
|
||||
queritApiKeyMessage: 'Please enter your Querit API Key',
|
||||
tavilyApiKeyHelp: 'How to get it?',
|
||||
crossLanguage: 'Cross-language search',
|
||||
crossLanguagePlaceholder: 'Select value',
|
||||
|
||||
@@ -1131,6 +1131,12 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
|
||||
tavilyApiKeyTip:
|
||||
'如果 API 密钥设置正确,它将利用 Tavily 进行网络搜索作为知识库的补充。',
|
||||
tavilyApiKeyMessage: '请输入你的 Tavily API Key',
|
||||
webSearchProvider: '网络搜索服务',
|
||||
webSearchProviderTip: '选择启用联网搜索时使用的搜索服务。',
|
||||
webSearchProviderPlaceholder: '请选择网络搜索服务',
|
||||
queritApiKeyTip:
|
||||
'选择 Querit 后,将使用 Querit 的网络搜索结果补充知识库检索。',
|
||||
queritApiKeyMessage: '请输入你的 Querit API Key',
|
||||
tavilyApiKeyHelp: '如何获取?',
|
||||
crossLanguage: '跨语言搜索',
|
||||
crossLanguagePlaceholder: '请选择',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
73
web/src/pages/next-chats/chat/use-show-internet.test.ts
Normal file
73
web/src/pages/next-chats/chat/use-show-internet.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
41
web/src/pages/next-chats/chat/web-search-api-key.ts
Normal file
41
web/src/pages/next-chats/chat/web-search-api-key.ts
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user