Feat: Allow chat to use meta data #3221 (#9393)

### What problem does this PR solve?

Feat:  Allow chat to use meta data #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-08-12 10:15:10 +08:00
committed by GitHub
parent 9433f64fe2
commit 76118000c1
13 changed files with 240 additions and 31 deletions

View File

@ -53,14 +53,14 @@ export default defineConfig({
memo.optimization.minimizer('terser').use(TerserPlugin); // Fixed the issue that the page displayed an error after packaging lexical with terser memo.optimization.minimizer('terser').use(TerserPlugin); // Fixed the issue that the page displayed an error after packaging lexical with terser
memo.plugin('eslint').use(ESLintPlugin, [ // memo.plugin('eslint').use(ESLintPlugin, [
{ // {
extensions: ['js', 'ts', 'tsx'], // extensions: ['js', 'ts', 'tsx'],
failOnError: true, // failOnError: true,
exclude: ['**/node_modules/**', '**/mfsu**', '**/mfsu-virtual-entry**'], // exclude: ['**/node_modules/**', '**/mfsu**', '**/mfsu-virtual-entry**'],
files: ['src/**/*.{js,ts,tsx}'], // files: ['src/**/*.{js,ts,tsx}'],
}, // },
]); // ]);
return memo; return memo;
}, },

View File

@ -4,8 +4,8 @@ import isEqual from 'lodash/isEqual';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const useSetModalState = () => { export const useSetModalState = (initialVisible = false) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(initialVisible);
const showModal = useCallback(() => { const showModal = useCallback(() => {
setVisible(true); setVisible(true);

View File

@ -28,6 +28,8 @@ export const enum KnowledgeApiAction {
DeleteKnowledge = 'deleteKnowledge', DeleteKnowledge = 'deleteKnowledge',
SaveKnowledge = 'saveKnowledge', SaveKnowledge = 'saveKnowledge',
FetchKnowledgeDetail = 'fetchKnowledgeDetail', FetchKnowledgeDetail = 'fetchKnowledgeDetail',
FetchKnowledgeGraph = 'fetchKnowledgeGraph',
FetchMetadata = 'fetchMetadata',
} }
export const useKnowledgeBaseId = (): string => { export const useKnowledgeBaseId = (): string => {
@ -263,7 +265,7 @@ export function useFetchKnowledgeGraph() {
const knowledgeBaseId = useKnowledgeBaseId(); const knowledgeBaseId = useKnowledgeBaseId();
const { data, isFetching: loading } = useQuery<IKnowledgeGraph>({ const { data, isFetching: loading } = useQuery<IKnowledgeGraph>({
queryKey: ['fetchKnowledgeGraph', knowledgeBaseId], queryKey: [KnowledgeApiAction.FetchKnowledgeGraph, knowledgeBaseId],
initialData: { graph: {}, mind_map: {} } as IKnowledgeGraph, initialData: { graph: {}, mind_map: {} } as IKnowledgeGraph,
enabled: !!knowledgeBaseId, enabled: !!knowledgeBaseId,
gcTime: 0, gcTime: 0,
@ -275,3 +277,20 @@ export function useFetchKnowledgeGraph() {
return { data, loading }; return { data, loading };
} }
export function useFetchKnowledgeMetadata(kbIds: string[] = []) {
const { data, isFetching: loading } = useQuery<
Record<string, Record<string, string[]>>
>({
queryKey: [KnowledgeApiAction.FetchMetadata, kbIds],
initialData: {},
enabled: kbIds.length > 0,
gcTime: 0,
queryFn: async () => {
const { data } = await kbService.getMeta({ kb_ids: kbIds.join(',') });
return data?.data ?? {};
},
});
return { data, loading };
}

View File

@ -563,6 +563,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
crossLanguage: 'Cross-language search', crossLanguage: 'Cross-language search',
crossLanguageTip: `Select one or more languages for crosslanguage search. If no language is selected, the system searches with the original query.`, crossLanguageTip: `Select one or more languages for crosslanguage search. If no language is selected, the system searches with the original query.`,
createChat: 'Create chat', createChat: 'Create chat',
metadata: 'Metadata',
metadataTip: 'Metadata',
conditions: 'Conditions',
}, },
setting: { setting: {
profile: 'Profile', profile: 'Profile',

View File

@ -8,10 +8,25 @@ import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { ISegmentedContentProps } from '../interface'; import { ISegmentedContentProps } from '../interface';
import { DatasetMetadata } from '../constants';
import styles from './index.less'; import styles from './index.less';
import { MetadataFilterConditions } from './metadata-filter-conditions';
const emptyResponseField = ['prompt_config', 'empty_response']; const emptyResponseField = ['prompt_config', 'empty_response'];
const MetadataOptions = Object.values(DatasetMetadata).map((x) => {
let value: DatasetMetadata | boolean = x;
if (x === DatasetMetadata.Disabled) {
value = false;
} else if (x === DatasetMetadata.Automatic) {
value = true;
}
return {
value,
label: x,
};
});
const AssistantSetting = ({ const AssistantSetting = ({
show, show,
form, form,
@ -20,6 +35,11 @@ const AssistantSetting = ({
const { t } = useTranslate('chat'); const { t } = useTranslate('chat');
const { data } = useFetchTenantInfo(true); const { data } = useFetchTenantInfo(true);
const metadata = Form.useWatch(['meta_data_filter', 'auto'], form);
const kbIds = Form.useWatch(['kb_ids'], form);
const hasKnowledge = Array.isArray(kbIds) && kbIds.length > 0;
const handleChange = useCallback(() => { const handleChange = useCallback(() => {
const kbIds = form.getFieldValue('kb_ids'); const kbIds = form.getFieldValue('kb_ids');
const emptyResponse = form.getFieldValue(emptyResponseField); const emptyResponse = form.getFieldValue(emptyResponseField);
@ -153,6 +173,24 @@ const AssistantSetting = ({
required={false} required={false}
onChange={handleChange} onChange={handleChange}
></KnowledgeBaseItem> ></KnowledgeBaseItem>
{hasKnowledge && (
<Form.Item
label={t('metadata')}
name={['meta_data_filter', 'auto']}
tooltip={t('metadataTip')}
>
<Select options={MetadataOptions} />
</Form.Item>
)}
{hasKnowledge && metadata === DatasetMetadata.Manual && (
<Form.Item
label={t('conditions')}
tooltip={t('ttsTip')}
initialValue={false}
>
<MetadataFilterConditions kbIds={kbIds}></MetadataFilterConditions>
</Form.Item>
)}
</section> </section>
); );
}; };

View File

@ -0,0 +1,84 @@
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
import { SwitchOperatorOptions } from '@/pages/agent/constant';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import {
Button,
Dropdown,
Empty,
Form,
FormListOperation,
Input,
Select,
Space,
} from 'antd';
import { useCallback } from 'react';
export function MetadataFilterConditions({ kbIds }: { kbIds: string[] }) {
const metadata = useFetchKnowledgeMetadata(kbIds);
const renderItems = useCallback(
(add: FormListOperation['add']) => {
if (Object.keys(metadata.data).length === 0) {
return [{ key: 'noData', label: <Empty></Empty> }];
}
return Object.keys(metadata.data).map((key) => {
return {
key,
onClick: () => {
add({
key,
value: '',
op: SwitchOperatorOptions[0].value,
});
},
label: key,
};
});
},
[metadata],
);
return (
<Form.List name={['meta_data_filter', 'manual']}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space
key={key}
style={{ display: 'flex', marginBottom: 8 }}
align="baseline"
>
<Form.Item
{...restField}
name={[name, 'key']}
rules={[{ required: true, message: 'Missing first name' }]}
>
<Input placeholder="First Name" />
</Form.Item>
<Form.Item {...restField} name={[name, 'op']} className="w-20">
<Select
options={SwitchOperatorOptions}
popupMatchSelectWidth={false}
/>
</Form.Item>
<Form.Item
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: 'Missing last name' }]}
>
<Input placeholder="Last Name" />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
))}
<Form.Item>
<Dropdown trigger={['click']} menu={{ items: renderItems(add) }}>
<Button type="dashed" block icon={<PlusOutlined />}>
Add Condition
</Button>
</Dropdown>
</Form.Item>
</>
)}
</Form.List>
);
}

View File

@ -1 +1,7 @@
export const EmptyConversationId = 'empty'; export const EmptyConversationId = 'empty';
export enum DatasetMetadata {
Disabled = 'disabled',
Automatic = 'automatic',
Manual = 'manual',
}

View File

@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { PanelRightClose } from 'lucide-react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import ChatBasicSetting from './chat-basic-settings'; import ChatBasicSetting from './chat-basic-settings';
@ -7,7 +8,8 @@ import { ChatModelSettings } from './chat-model-settings';
import { ChatPromptEngine } from './chat-prompt-engine'; import { ChatPromptEngine } from './chat-prompt-engine';
import { useChatSettingSchema } from './use-chat-setting-schema'; import { useChatSettingSchema } from './use-chat-setting-schema';
export function ChatSettings() { type ChatSettingsProps = { switchSettingVisible(): void };
export function ChatSettings({ switchSettingVisible }: ChatSettingsProps) {
const formSchema = useChatSettingSchema(); const formSchema = useChatSettingSchema();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@ -33,11 +35,18 @@ export function ChatSettings() {
} }
return ( return (
<section className="py-6"> <section className="p-5 w-[400px] max-w-[20%]">
<div className="flex justify-between items-center text-base">
Chat Settings
<PanelRightClose
className="size-4 cursor-pointer"
onClick={switchSettingVisible}
/>
</div>
<FormProvider {...form}> <FormProvider {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 overflow-auto max-h-[88vh] pr-4" className="space-y-6 overflow-auto max-h-[87vh] pr-4"
> >
<ChatBasicSetting></ChatBasicSetting> <ChatBasicSetting></ChatBasicSetting>
<ChatPromptEngine></ChatPromptEngine> <ChatPromptEngine></ChatPromptEngine>

View File

@ -23,7 +23,7 @@ interface IProps {
export function ChatBox({ controller }: IProps) { export function ChatBox({ controller }: IProps) {
const { const {
value, value,
scrollRef, // scrollRef,
messageContainerRef, messageContainerRef,
sendLoading, sendLoading,
derivedMessages, derivedMessages,
@ -43,8 +43,8 @@ export function ChatBox({ controller }: IProps) {
const sendDisabled = useSendButtonDisabled(value); const sendDisabled = useSendButtonDisabled(value);
return ( return (
<section className="border-x flex flex-col p-5 w-full"> <section className="border-x flex flex-col p-5 flex-1 min-w-0">
<div ref={messageContainerRef} className="flex-1 overflow-auto"> <div ref={messageContainerRef} className="flex-1 overflow-auto min-h-0">
<div className="w-full"> <div className="w-full">
{derivedMessages?.map((message, i) => { {derivedMessages?.map((message, i) => {
return ( return (
@ -75,7 +75,7 @@ export function ChatBox({ controller }: IProps) {
); );
})} })}
</div> </div>
<div ref={scrollRef} /> {/* <div ref={scrollRef} /> */}
</div> </div>
<NextMessageInput <NextMessageInput
disabled={disabled} disabled={disabled}

View File

@ -7,10 +7,12 @@ import {
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'; } from '@/components/ui/breadcrumb';
import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchDialog } from '@/hooks/use-chat-request'; import { useFetchDialog } from '@/hooks/use-chat-request';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHandleClickConversationCard } from '../hooks/use-click-card'; import { useHandleClickConversationCard } from '../hooks/use-click-card';
import { ChatSettings } from './app-settings/chat-settings';
import { ChatBox } from './chat-box'; import { ChatBox } from './chat-box';
import { Sessions } from './sessions'; import { Sessions } from './sessions';
@ -20,6 +22,8 @@ export default function Chat() {
const { t } = useTranslation(); const { t } = useTranslation();
const { handleConversationCardClick, controller } = const { handleConversationCardClick, controller } =
useHandleClickConversationCard(); useHandleClickConversationCard();
const { visible: settingVisible, switchVisible: switchSettingVisible } =
useSetModalState(true);
return ( return (
<section className="h-full flex flex-col"> <section className="h-full flex flex-col">
@ -39,10 +43,18 @@ export default function Chat() {
</Breadcrumb> </Breadcrumb>
</PageHeader> </PageHeader>
<div className="flex flex-1 min-h-0"> <div className="flex flex-1 min-h-0">
<Sessions <div className="flex flex-1 min-w-0">
handleConversationCardClick={handleConversationCardClick} <Sessions
></Sessions> handleConversationCardClick={handleConversationCardClick}
<ChatBox controller={controller}></ChatBox> switchSettingVisible={switchSettingVisible}
></Sessions>
<ChatBox controller={controller}></ChatBox>
</div>
{settingVisible && (
<ChatSettings
switchSettingVisible={switchSettingVisible}
></ChatSettings>
)}
</div> </div>
</section> </section>
); );

View File

@ -1,21 +1,30 @@
import { MoreButton } from '@/components/more-button'; import { MoreButton } from '@/components/more-button';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { useGetChatSearchParams } from '@/hooks/use-chat-request'; import { useSetModalState } from '@/hooks/common-hooks';
import {
useFetchDialog,
useGetChatSearchParams,
} from '@/hooks/use-chat-request';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Plus } from 'lucide-react'; import { PanelLeftClose, PanelRightClose, Plus } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useHandleClickConversationCard } from '../hooks/use-click-card'; import { useHandleClickConversationCard } from '../hooks/use-click-card';
import { useSelectDerivedConversationList } from '../hooks/use-select-conversation-list'; import { useSelectDerivedConversationList } from '../hooks/use-select-conversation-list';
import { ChatSettingSheet } from './app-settings/chat-settings-sheet';
type SessionProps = Pick< type SessionProps = Pick<
ReturnType<typeof useHandleClickConversationCard>, ReturnType<typeof useHandleClickConversationCard>,
'handleConversationCardClick' 'handleConversationCardClick'
>; > & { switchSettingVisible(): void };
export function Sessions({ handleConversationCardClick }: SessionProps) { export function Sessions({
handleConversationCardClick,
switchSettingVisible,
}: SessionProps) {
const { list: conversationList, addTemporaryConversation } = const { list: conversationList, addTemporaryConversation } =
useSelectDerivedConversationList(); useSelectDerivedConversationList();
const { data } = useFetchDialog();
const { visible, switchVisible } = useSetModalState(true);
const handleCardClick = useCallback( const handleCardClick = useCallback(
(conversationId: string, isNew: boolean) => () => { (conversationId: string, isNew: boolean) => () => {
@ -26,9 +35,32 @@ export function Sessions({ handleConversationCardClick }: SessionProps) {
const { conversationId } = useGetChatSearchParams(); const { conversationId } = useGetChatSearchParams();
if (!visible) {
return (
<PanelRightClose
className="cursor-pointer size-4 mt-8"
onClick={switchVisible}
/>
);
}
return ( return (
<section className="p-6 w-[400px] max-w-[20%] flex flex-col"> <section className="p-6 w-[400px] max-w-[20%] flex flex-col">
<div className="flex justify-between items-center mb-4"> <section className="flex items-center text-base justify-between gap-2">
<div className="flex gap-3 items-center min-w-0">
<RAGFlowAvatar
avatar={data.icon}
name={data.name}
className="size-8"
></RAGFlowAvatar>
<span className="flex-1 truncate">{data.name}</span>
</div>
<PanelLeftClose
className="cursor-pointer size-4"
onClick={switchVisible}
/>
</section>
<div className="flex justify-between items-center mb-4 pt-10">
<span className="text-xl font-bold">Conversations</span> <span className="text-xl font-bold">Conversations</span>
<Button variant={'ghost'} onClick={addTemporaryConversation}> <Button variant={'ghost'} onClick={addTemporaryConversation}>
<Plus></Plus> <Plus></Plus>
@ -51,9 +83,9 @@ export function Sessions({ handleConversationCardClick }: SessionProps) {
))} ))}
</div> </div>
<div className="py-2"> <div className="py-2">
<ChatSettingSheet> <Button className="w-full" onClick={switchSettingVisible}>
<Button className="w-full">Chat Settings</Button> Chat Settings
</ChatSettingSheet> </Button>
</div> </div>
</section> </section>
); );

View File

@ -37,6 +37,7 @@ const {
upload_and_parse, upload_and_parse,
listTagByKnowledgeIds, listTagByKnowledgeIds,
setMeta, setMeta,
getMeta,
} = api; } = api;
const methods = { const methods = {
@ -159,6 +160,10 @@ const methods = {
url: api.get_dataset_filter, url: api.get_dataset_filter,
method: 'post', method: 'post',
}, },
getMeta: {
url: getMeta,
method: 'get',
},
}; };
const kbService = registerServer<keyof typeof methods>(methods, request); const kbService = registerServer<keyof typeof methods>(methods, request);

View File

@ -44,6 +44,7 @@ export default {
get_kb_detail: `${api_host}/kb/detail`, get_kb_detail: `${api_host}/kb/detail`,
getKnowledgeGraph: (knowledgeId: string) => getKnowledgeGraph: (knowledgeId: string) =>
`${api_host}/kb/${knowledgeId}/knowledge_graph`, `${api_host}/kb/${knowledgeId}/knowledge_graph`,
getMeta: `${api_host}/kb/get_meta`,
// tags // tags
listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`, listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`,