feat: create a chat assistant and extract SimilaritySlider (#67)

* feat: extract SimilaritySlider

* feat: create a chat assistant
This commit is contained in:
balibabu 2024-02-20 18:10:20 +08:00 committed by GitHub
parent a8294f2168
commit 8c4ec9955e
26 changed files with 716 additions and 196 deletions

View File

@ -0,0 +1,5 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.1875 12.1045L7.04673 12.5819C7.21217 12.6738 7.29489 12.7197 7.38249 12.7377C7.46002 12.7537 7.53998 12.7537 7.61751 12.7377C7.70511 12.7197 7.78783 12.6738 7.95327 12.5819L8.8125 12.1045M3.5625 10.6462L2.73007 10.1837C2.55535 10.0866 2.46798 10.0381 2.40437 9.96907C2.34809 9.908 2.3055 9.83562 2.27945 9.75677C2.25 9.66764 2.25 9.5677 2.25 9.36782V8.45867M2.25 5.542V4.63284C2.25 4.43297 2.25 4.33303 2.27945 4.2439C2.3055 4.16505 2.34809 4.09267 2.40437 4.0316C2.46798 3.96257 2.55535 3.91403 2.73007 3.81696L3.5625 3.3545M6.1875 1.89617L7.04673 1.41882C7.21217 1.32691 7.29489 1.28095 7.38249 1.26294C7.46002 1.24699 7.53998 1.24699 7.61751 1.26294C7.70511 1.28095 7.78783 1.32691 7.95327 1.41882L8.8125 1.89617M11.4375 3.3545L12.2699 3.81696C12.4447 3.91403 12.532 3.96257 12.5956 4.0316C12.6519 4.09266 12.6945 4.16505 12.7206 4.2439C12.75 4.33303 12.75 4.43297 12.75 4.63284V5.542M12.75 8.45867V9.36782C12.75 9.5677 12.75 9.66764 12.7206 9.75677C12.6945 9.83562 12.6519 9.908 12.5956 9.96907C12.532 10.0381 12.4447 10.0866 12.2699 10.1837L11.4375 10.6462M6.1875 6.27117L7.5 7.00033M7.5 7.00033L8.8125 6.27117M7.5 7.00033V8.45867M2.25 4.08367L3.5625 4.81283M11.4375 4.81283L12.75 4.08367M7.5 11.3753V12.8337"
stroke="#A5A3A9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,29 @@
import { Form, Slider } from 'antd';
type FieldType = {
similarity_threshold?: number;
vector_similarity_weight?: number;
};
const SimilaritySlider = () => {
return (
<>
<Form.Item<FieldType>
label="Similarity threshold"
name={'similarity_threshold'}
initialValue={0}
>
<Slider max={1} step={0.01} />
</Form.Item>
<Form.Item<FieldType>
label="Vector similarity weight"
name={'vector_similarity_weight'}
initialValue={0}
>
<Slider max={1} step={0.01} />
</Form.Item>
</>
);
};
export default SimilaritySlider;

View File

@ -11,3 +11,40 @@ export enum RunningStatus {
DONE = '3', // need to refresh DONE = '3', // need to refresh
FAIL = '4', // need to refresh FAIL = '4', // need to refresh
} }
export enum ModelVariableType {
Improvise = 'Improvise',
Precise = 'Precise',
Balance = 'Balance',
}
export const settledModelVariableMap = {
[ModelVariableType.Improvise]: {
temperature: 0.9,
top_p: 0.9,
frequency_penalty: 0.2,
presence_penalty: 0.4,
max_tokens: 512,
},
[ModelVariableType.Precise]: {
temperature: 0.1,
top_p: 0.3,
frequency_penalty: 0.7,
presence_penalty: 0.4,
max_tokens: 215,
},
[ModelVariableType.Balance]: {
temperature: 0.5,
top_p: 0.5,
frequency_penalty: 0.7,
presence_penalty: 0.4,
max_tokens: 215,
},
};
export enum LlmModelType {
Embedding = 'embedding',
Chat = 'chat',
Image2text = 'image2text',
Speech2text = 'speech2text',
}

View File

@ -124,3 +124,22 @@ export const useFetchKnowledgeBaseConfiguration = () => {
fetchKnowledgeBaseConfiguration(); fetchKnowledgeBaseConfiguration();
}, [fetchKnowledgeBaseConfiguration]); }, [fetchKnowledgeBaseConfiguration]);
}; };
export const useFetchKnowledgeList = (): IKnowledge[] => {
const dispatch = useDispatch();
const knowledgeModel = useSelector((state: any) => state.knowledgeModel);
const { data = [] } = knowledgeModel;
const fetchList = useCallback(() => {
dispatch({
type: 'knowledgeModel/getList',
});
}, [dispatch]);
useEffect(() => {
fetchList();
}, [fetchList]);
return data;
};

39
web/src/hooks/llmHooks.ts Normal file
View File

@ -0,0 +1,39 @@
import { LlmModelType } from '@/constants/knowledge';
import { IThirdOAIModelCollection } from '@/interfaces/database/llm';
import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'umi';
export const useFetchLlmList = (modelType: LlmModelType) => {
const dispatch = useDispatch();
const fetchLlmList = useCallback(() => {
dispatch({
type: 'settingModel/llm_list',
payload: { model_type: modelType },
});
}, [dispatch, modelType]);
useEffect(() => {
fetchLlmList();
}, [fetchLlmList]);
};
export const useSelectLlmOptions = () => {
const llmInfo: IThirdOAIModelCollection = useSelector(
(state: any) => state.settingModel.llmInfo,
);
const embeddingModelOptions = useMemo(() => {
return Object.entries(llmInfo).map(([key, value]) => {
return {
label: key,
options: value.map((x) => ({
label: x.llm_name,
value: x.llm_name,
})),
};
});
}, [llmInfo]);
return embeddingModelOptions;
};

View File

@ -0,0 +1,47 @@
export interface PromptConfig {
empty_response: string;
parameters: Parameter[];
prologue: string;
system: string;
}
export interface Parameter {
key: string;
optional: boolean;
}
export interface LlmSetting {
Creative: Variable;
Custom: Variable;
Evenly: Variable;
Precise: Variable;
}
export interface Variable {
frequency_penalty: number;
max_tokens: number;
presence_penalty: number;
temperature: number;
top_p: number;
}
export interface IDialog {
create_date: string;
create_time: number;
description: string;
icon: string;
id: string;
kb_ids: string[];
kb_names: string[];
language: string;
llm_id: string;
llm_setting: LlmSetting;
llm_setting_type: string;
name: string;
prompt_config: PromptConfig;
prompt_type: string;
status: string;
tenant_id: string;
update_date: string;
update_time: number;
}

View File

@ -53,7 +53,7 @@ const RenameModal = () => {
useEffect(() => { useEffect(() => {
form.setFieldValue('name', initialName); form.setFieldValue('name', initialName);
}, [initialName, documentId]); }, [initialName, documentId, form]);
return ( return (
<Modal <Modal

View File

@ -17,13 +17,14 @@ import {
UploadFile, UploadFile,
} from 'antd'; } from 'antd';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { useCallback, useEffect, useMemo } from 'react'; import { useEffect } from 'react';
import { useDispatch, useSelector } from 'umi'; import { useDispatch, useSelector } from 'umi';
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { IKnowledge } from '@/interfaces/database/knowledge'; import { IKnowledge } from '@/interfaces/database/knowledge';
import { IThirdOAIModelCollection } from '@/interfaces/database/llm';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { LlmModelType } from '../../constant';
import styles from './index.less'; import styles from './index.less';
const { Title } = Typography; const { Title } = Typography;
@ -35,9 +36,6 @@ const Configuration = () => {
const knowledgeBaseId = useKnowledgeBaseId(); const knowledgeBaseId = useKnowledgeBaseId();
const loading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']); const loading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']);
const llmInfo: IThirdOAIModelCollection = useSelector(
(state: any) => state.settingModel.llmInfo,
);
const knowledgeDetails: IKnowledge = useSelector( const knowledgeDetails: IKnowledge = useSelector(
(state: any) => state.kSModel.knowledgeDetails, (state: any) => state.kSModel.knowledgeDetails,
); );
@ -51,17 +49,7 @@ const Configuration = () => {
const parserList = useSelectParserList(); const parserList = useSelectParserList();
const embeddingModelOptions = useMemo(() => { const embeddingModelOptions = useSelectLlmOptions();
return Object.entries(llmInfo).map(([key, value]) => {
return {
label: key,
options: value.map((x) => ({
label: x.llm_name,
value: x.llm_name,
})),
};
});
}, [llmInfo]);
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
console.info(values); console.info(values);
@ -86,13 +74,6 @@ const Configuration = () => {
console.log('Failed:', errorInfo); console.log('Failed:', errorInfo);
}; };
const fetchLlmList = useCallback(() => {
dispatch({
type: 'settingModel/llm_list',
payload: { model_type: 'embedding' },
});
}, [dispatch]);
useEffect(() => { useEffect(() => {
const avatar = knowledgeDetails.avatar; const avatar = knowledgeDetails.avatar;
let fileList: UploadFile[] = []; let fileList: UploadFile[] = [];
@ -115,9 +96,7 @@ const Configuration = () => {
useFetchParserList(); useFetchParserList();
useFetchKnowledgeBaseConfiguration(); useFetchKnowledgeBaseConfiguration();
useEffect(() => { useFetchLlmList(LlmModelType.Embedding);
fetchLlmList();
}, [fetchLlmList]);
return ( return (
<div className={styles.configurationWrapper}> <div className={styles.configurationWrapper}>

View File

@ -16,14 +16,10 @@ const KnowledgeTesting = () => {
const handleTesting = async () => { const handleTesting = async () => {
const values = await form.validateFields(); const values = await form.validateFields();
console.info(values); console.info(values);
const similarity_threshold = values.similarity_threshold / 100;
const vector_similarity_weight = values.vector_similarity_weight / 100;
dispatch({ dispatch({
type: 'testingModel/testDocumentChunk', type: 'testingModel/testDocumentChunk',
payload: { payload: {
...values, ...values,
similarity_threshold,
vector_similarity_weight,
kb_id: knowledgeBaseId, kb_id: knowledgeBaseId,
}, },
}); });

View File

@ -1,3 +1,5 @@
import SimilaritySlider from '@/components/similarity-slider';
import { DeleteOutlined, HistoryOutlined } from '@ant-design/icons';
import { import {
Button, Button,
Card, Card,
@ -6,22 +8,15 @@ import {
Form, Form,
Input, Input,
Slider, Slider,
SliderSingleProps,
Space, Space,
Tag, Tag,
} from 'antd'; } from 'antd';
import { DeleteOutlined, HistoryOutlined } from '@ant-design/icons';
import { FormInstance } from 'antd/lib'; import { FormInstance } from 'antd/lib';
import styles from './index.less'; import styles from './index.less';
const list = [1, 2, 3]; const list = [1, 2, 3];
const marks: SliderSingleProps['marks'] = {
0: '0',
100: '1',
};
type FieldType = { type FieldType = {
similarity_threshold?: number; similarity_threshold?: number;
vector_similarity_weight?: number; vector_similarity_weight?: number;
@ -29,12 +24,6 @@ type FieldType = {
question: string; question: string;
}; };
const formatter = (value: number | undefined) => {
return typeof value === 'number' ? value / 100 : 0;
};
const tooltip = { formatter };
interface IProps { interface IProps {
form: FormInstance; form: FormInstance;
handleTesting: () => Promise<any>; handleTesting: () => Promise<any>;
@ -59,23 +48,12 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
layout="vertical" layout="vertical"
form={form} form={form}
initialValues={{ initialValues={{
similarity_threshold: 20, similarity_threshold: 0.2,
vector_similarity_weight: 30, vector_similarity_weight: 0.3,
top_k: 1024, top_k: 1024,
}} }}
> >
<Form.Item<FieldType> <SimilaritySlider></SimilaritySlider>
label="Similarity threshold"
name={'similarity_threshold'}
>
<Slider marks={marks} defaultValue={0} tooltip={tooltip} />
</Form.Item>
<Form.Item<FieldType>
label="Vector similarity weight"
name={'vector_similarity_weight'}
>
<Slider marks={marks} defaultValue={0} tooltip={tooltip} />
</Form.Item>
<Form.Item<FieldType> label="Top k" name={'top_k'}> <Form.Item<FieldType> label="Top k" name={'top_k'}>
<Slider marks={{ 0: 0, 2048: 2048 }} defaultValue={0} max={2048} /> <Slider marks={{ 0: 0, 2048: 2048 }} defaultValue={0} max={2048} />
</Form.Item> </Form.Item>

View File

@ -1,11 +1,20 @@
import { Form, Input } from 'antd'; import { Form, Input, Select } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { ISegmentedContentProps } from './interface'; import { ISegmentedContentProps } from './interface';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import styles from './index.less'; import styles from './index.less';
const { Option } = Select;
const AssistantSetting = ({ show }: ISegmentedContentProps) => { const AssistantSetting = ({ show }: ISegmentedContentProps) => {
const knowledgeList = useFetchKnowledgeList();
const knowledgeOptions = knowledgeList.map((x) => ({
label: x.name,
value: x.id,
}));
return ( return (
<section <section
className={classNames({ className={classNames({
@ -19,15 +28,43 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
> >
<Input placeholder="e.g. Resume Jarvis" /> <Input placeholder="e.g. Resume Jarvis" />
</Form.Item> </Form.Item>
<Form.Item name={'avatar'} label="Assistant avatar"> <Form.Item name={'icon'} label="Assistant avatar">
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item name={'keywords'} label="Keywords"> <Form.Item name={'language'} label="Language" initialValue={'Chinese'}>
<Input.TextArea autoSize={{ minRows: 3 }} /> <Select
options={[
{ value: 'Chinese', label: 'Chinese' },
{ value: 'English', label: 'English' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item name={'opener'} label="Set an opener"> <Form.Item
name={['prompt_config', 'empty_response']}
label="Empty response"
>
<Input placeholder="" />
</Form.Item>
<Form.Item name={['prompt_config', 'prologue']} label="Set an opener">
<Input.TextArea autoSize={{ minRows: 5 }} /> <Input.TextArea autoSize={{ minRows: 5 }} />
</Form.Item> </Form.Item>
<Form.Item
label="Select one context"
name="kb_ids"
rules={[
{
required: true,
message: 'Please select!',
type: 'array',
},
]}
>
<Select
mode="multiple"
options={knowledgeOptions}
placeholder="Please select"
></Select>
</Form.Item>
</section> </section>
); );
}; };

View File

@ -0,0 +1,7 @@
export const variableEnabledFieldMap = {
temperatureEnabled: 'temperature',
topPEnabled: 'top_p',
presencePenaltyEnabled: 'presence_penalty',
frequencyPenaltyEnabled: 'frequency_penalty',
maxTokensEnabled: 'max_tokens',
};

View File

@ -41,3 +41,10 @@
width: 0; width: 0;
margin: 0; margin: 0;
} }
.sliderInputNumber {
width: 80px;
}
.variableSlider {
width: 100%;
}

View File

@ -2,17 +2,20 @@ import { ReactComponent as ChatConfigurationAtom } from '@/assets/svg/chat-confi
import { IModalManagerChildrenProps } from '@/components/modal-manager'; import { IModalManagerChildrenProps } from '@/components/modal-manager';
import { Divider, Flex, Form, Modal, Segmented } from 'antd'; import { Divider, Flex, Form, Modal, Segmented } from 'antd';
import { SegmentedValue } from 'antd/es/segmented'; import { SegmentedValue } from 'antd/es/segmented';
import { useState } from 'react'; import omit from 'lodash/omit';
import { useRef, useState } from 'react';
import AssistantSetting from './assistant-setting'; import AssistantSetting from './assistant-setting';
import ModelSetting from './model-setting'; import ModelSetting from './model-setting';
import PromptEngine from './prompt-engine'; import PromptEngine from './prompt-engine';
import { useSetDialog } from '../hooks';
import { variableEnabledFieldMap } from './constants';
import styles from './index.less'; import styles from './index.less';
enum ConfigurationSegmented { enum ConfigurationSegmented {
AssistantSetting = 'Assistant Setting', AssistantSetting = 'Assistant Setting',
ModelSetting = 'Model Setting',
PromptEngine = 'Prompt Engine', PromptEngine = 'Prompt Engine',
ModelSetting = 'Model Setting',
} }
const segmentedMap = { const segmentedMap = {
@ -45,10 +48,24 @@ const ChatConfigurationModal = ({
const [value, setValue] = useState<ConfigurationSegmented>( const [value, setValue] = useState<ConfigurationSegmented>(
ConfigurationSegmented.AssistantSetting, ConfigurationSegmented.AssistantSetting,
); );
const promptEngineRef = useRef(null);
const setDialog = useSetDialog();
const handleOk = async () => { const handleOk = async () => {
const x = await form.validateFields(); const values = await form.validateFields();
console.info(x); const nextValues: any = omit(values, Object.keys(variableEnabledFieldMap));
const finalValues = {
...nextValues,
prompt_config: {
...nextValues.prompt_config,
parameters: promptEngineRef.current,
},
};
console.info(promptEngineRef.current);
console.info(nextValues);
console.info(finalValues);
setDialog(finalValues);
}; };
const handleCancel = () => { const handleCancel = () => {
@ -97,7 +114,14 @@ const ChatConfigurationModal = ({
colon={false} colon={false}
> >
{Object.entries(segmentedMap).map(([key, Element]) => ( {Object.entries(segmentedMap).map(([key, Element]) => (
<Element key={key} show={key === value}></Element> <Element
key={key}
show={key === value}
form={form}
{...(key === ConfigurationSegmented.PromptEngine
? { ref: promptEngineRef }
: {})}
></Element>
))} ))}
</Form> </Form>
</Modal> </Modal>

View File

@ -1,3 +1,14 @@
import { FormInstance } from 'antd';
export interface ISegmentedContentProps { export interface ISegmentedContentProps {
show: boolean; show: boolean;
form: FormInstance;
}
export interface IVariable {
temperature: number;
top_p: number;
frequency_penalty: number;
presence_penalty: number;
max_tokens: number;
} }

View File

@ -1,12 +1,48 @@
import { Divider, Flex, Form, InputNumber, Select, Slider } from 'antd'; import {
LlmModelType,
ModelVariableType,
settledModelVariableMap,
} from '@/constants/knowledge';
import { Divider, Flex, Form, InputNumber, Select, Slider, Switch } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect } from 'react';
import { ISegmentedContentProps } from './interface'; import { ISegmentedContentProps } from './interface';
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
import { variableEnabledFieldMap } from './constants';
import styles from './index.less'; import styles from './index.less';
const { Option } = Select; const ModelSetting = ({ show, form }: ISegmentedContentProps) => {
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
label: x,
value: x,
}));
const parameters: ModelVariableType = Form.useWatch('parameters', form);
const modelOptions = useSelectLlmOptions();
const handleParametersChange = (value: ModelVariableType) => {
console.info(value);
};
useEffect(() => {
const variable = settledModelVariableMap[parameters];
form.setFieldsValue({ llm_setting: variable });
}, [parameters, form]);
useEffect(() => {
const values = Object.keys(variableEnabledFieldMap).reduce<
Record<string, boolean>
>((pre, field) => {
pre[field] = true;
return pre;
}, {});
form.setFieldsValue(values);
}, [form]);
useFetchLlmList(LlmModelType.Chat);
const ModelSetting = ({ show }: ISegmentedContentProps) => {
return ( return (
<section <section
className={classNames({ className={classNames({
@ -15,135 +51,170 @@ const ModelSetting = ({ show }: ISegmentedContentProps) => {
> >
<Form.Item <Form.Item
label="Model" label="Model"
name="model" name="llm_id"
// rules={[{ required: true, message: 'Please input!' }]} rules={[{ required: true, message: 'Please select!' }]}
> >
<Select /> <Select options={modelOptions} />
</Form.Item> </Form.Item>
<Divider></Divider> <Divider></Divider>
<Form.Item <Form.Item
label="Parameters" label="Parameters"
name="parameters" name="parameters"
initialValue={ModelVariableType.Precise}
// rules={[{ required: true, message: 'Please input!' }]} // rules={[{ required: true, message: 'Please input!' }]}
> >
<Select /> <Select<ModelVariableType>
options={parameterOptions}
onChange={handleParametersChange}
/>
</Form.Item> </Form.Item>
<Form.Item label="Temperature"> <Form.Item label="Temperature" tooltip={'xx'}>
<Flex gap={20}> <Flex gap={20} align="center">
<Form.Item
name={'temperatureEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}> <Flex flex={1}>
<Form.Item <Form.Item
name={['address', 'province']} name={['llm_setting', 'temperature']}
noStyle noStyle
rules={[{ required: true, message: 'Province is required' }]} rules={[{ required: true, message: 'Province is required' }]}
> >
<Slider style={{ display: 'inline-block', width: '100%' }} /> <Slider className={styles.variableSlider} max={1} step={0.01} />
</Form.Item> </Form.Item>
</Flex> </Flex>
<Form.Item <Form.Item
name={['address', 'street']} name={['llm_setting', 'temperature']}
noStyle noStyle
rules={[{ required: true, message: 'Street is required' }]} rules={[{ required: true, message: 'Street is required' }]}
> >
<InputNumber <InputNumber
style={{ className={styles.sliderInputNumber}
width: 50, max={1}
}} min={0}
step={0.01}
/> />
</Form.Item> </Form.Item>
</Flex> </Flex>
</Form.Item> </Form.Item>
<Form.Item label="Top P"> <Form.Item label="Top P" tooltip={'xx'}>
<Flex gap={20}> <Flex gap={20} align="center">
<Form.Item name={'topPEnabled'} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
<Flex flex={1}> <Flex flex={1}>
<Form.Item <Form.Item
name={['address', 'province']} name={['llm_setting', 'top_p']}
noStyle noStyle
rules={[{ required: true, message: 'Province is required' }]} rules={[{ required: true, message: 'Province is required' }]}
> >
<Slider style={{ display: 'inline-block', width: '100%' }} /> <Slider className={styles.variableSlider} max={1} step={0.01} />
</Form.Item> </Form.Item>
</Flex> </Flex>
<Form.Item <Form.Item
name={['address', 'street']} name={['llm_setting', 'top_p']}
noStyle noStyle
rules={[{ required: true, message: 'Street is required' }]} rules={[{ required: true, message: 'Street is required' }]}
> >
<InputNumber <InputNumber
style={{ className={styles.sliderInputNumber}
width: 50, max={1}
}} min={0}
step={0.01}
/> />
</Form.Item> </Form.Item>
</Flex> </Flex>
</Form.Item> </Form.Item>
<Form.Item label="Presence Penalty"> <Form.Item label="Presence Penalty" tooltip={'xx'}>
<Flex gap={20}> <Flex gap={20} align="center">
<Form.Item
name={'presencePenaltyEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}> <Flex flex={1}>
<Form.Item <Form.Item
name={['address', 'province']} name={['llm_setting', 'presence_penalty']}
noStyle noStyle
rules={[{ required: true, message: 'Province is required' }]} rules={[{ required: true, message: 'Province is required' }]}
> >
<Slider style={{ display: 'inline-block', width: '100%' }} /> <Slider className={styles.variableSlider} max={1} step={0.01} />
</Form.Item> </Form.Item>
</Flex> </Flex>
<Form.Item <Form.Item
name={['address', 'street']} name={['llm_setting', 'presence_penalty']}
noStyle noStyle
rules={[{ required: true, message: 'Street is required' }]} rules={[{ required: true, message: 'Street is required' }]}
> >
<InputNumber <InputNumber
style={{ className={styles.sliderInputNumber}
width: 50, max={1}
}} min={0}
step={0.01}
/> />
</Form.Item> </Form.Item>
</Flex> </Flex>
</Form.Item> </Form.Item>
<Form.Item label="Frequency Penalty"> <Form.Item label="Frequency Penalty" tooltip={'xx'}>
<Flex gap={20}> <Flex gap={20} align="center">
<Form.Item
name={'frequencyPenaltyEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}> <Flex flex={1}>
<Form.Item <Form.Item
name={['address', 'province']} name={['llm_setting', 'frequency_penalty']}
noStyle noStyle
rules={[{ required: true, message: 'Province is required' }]} rules={[{ required: true, message: 'Province is required' }]}
> >
<Slider style={{ display: 'inline-block', width: '100%' }} /> <Slider className={styles.variableSlider} max={1} step={0.01} />
</Form.Item> </Form.Item>
</Flex> </Flex>
<Form.Item <Form.Item
name={['address', 'street']} name={['llm_setting', 'frequency_penalty']}
noStyle noStyle
rules={[{ required: true, message: 'Street is required' }]} rules={[{ required: true, message: 'Street is required' }]}
> >
<InputNumber <InputNumber
style={{ className={styles.sliderInputNumber}
width: 50, max={1}
}} min={0}
step={0.01}
/> />
</Form.Item> </Form.Item>
</Flex> </Flex>
</Form.Item> </Form.Item>
<Form.Item label="Max Tokens"> <Form.Item label="Max Tokens" tooltip={'xx'}>
<Flex gap={20}> <Flex gap={20} align="center">
<Form.Item name={'maxTokensEnabled'} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
<Flex flex={1}> <Flex flex={1}>
<Form.Item <Form.Item
name={['address', 'province']} name={['llm_setting', 'max_tokens']}
noStyle noStyle
rules={[{ required: true, message: 'Province is required' }]} rules={[{ required: true, message: 'Province is required' }]}
> >
<Slider style={{ display: 'inline-block', width: '100%' }} /> <Slider className={styles.variableSlider} max={2048} />
</Form.Item> </Form.Item>
</Flex> </Flex>
<Form.Item <Form.Item
name={['address', 'street']} name={['llm_setting', 'max_tokens']}
noStyle noStyle
rules={[{ required: true, message: 'Street is required' }]} rules={[{ required: true, message: 'Street is required' }]}
> >
<InputNumber <InputNumber
style={{ className={styles.sliderInputNumber}
width: 50, max={2048}
}} min={0}
/> />
</Form.Item> </Form.Item>
</Flex> </Flex>

View File

@ -1,3 +1,4 @@
import SimilaritySlider from '@/components/similarity-slider';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { import {
Button, Button,
@ -6,13 +7,19 @@ import {
Form, Form,
Input, Input,
Row, Row,
Select, Slider,
Switch, Switch,
Table, Table,
TableProps, TableProps,
} from 'antd'; } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useState } from 'react'; import {
ForwardedRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { EditableCell, EditableRow } from './editable-cell'; import { EditableCell, EditableRow } from './editable-cell';
import { ISegmentedContentProps } from './interface'; import { ISegmentedContentProps } from './interface';
@ -21,12 +28,20 @@ import styles from './index.less';
interface DataType { interface DataType {
key: string; key: string;
variable: string;
optional: boolean; optional: boolean;
} }
const { Option } = Select; type FieldType = {
similarity_threshold?: number;
vector_similarity_weight?: number;
top_n?: number;
};
const PromptEngine = ({ show }: ISegmentedContentProps) => { const PromptEngine = (
{ show, form }: ISegmentedContentProps,
ref: ForwardedRef<Array<Omit<DataType, 'variable'>>>,
) => {
const [dataSource, setDataSource] = useState<DataType[]>([]); const [dataSource, setDataSource] = useState<DataType[]>([]);
const components = { const components = {
@ -52,6 +67,44 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
setDataSource(newData); setDataSource(newData);
}; };
const handleAdd = () => {
setDataSource((state) => [
...state,
{
key: uuid(),
variable: '',
optional: true,
},
]);
};
const handleOptionalChange = (row: DataType) => (checked: boolean) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.key === item.key);
const item = newData[index];
newData.splice(index, 1, {
...item,
optional: checked,
});
setDataSource(newData);
};
useImperativeHandle(
ref,
() => {
return dataSource
.filter((x) => x.variable.trim() !== '')
.map((x) => ({ key: x.variable, optional: x.optional }));
},
[dataSource],
);
useEffect(() => {
form.setFieldValue(['prompt_config', 'parameters'], dataSource);
const x = form.getFieldValue(['prompt_config', 'parameters']);
console.info(x);
}, [dataSource, form]);
const columns: TableProps<DataType>['columns'] = [ const columns: TableProps<DataType>['columns'] = [
{ {
title: 'key', title: 'key',
@ -71,8 +124,14 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
key: 'optional', key: 'optional',
width: 40, width: 40,
align: 'center', align: 'center',
render() { render(text, record) {
return <Switch size="small" />; return (
<Switch
size="small"
checked={text}
onChange={handleOptionalChange(record)}
/>
);
}, },
}, },
{ {
@ -87,17 +146,6 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
}, },
]; ];
const handleAdd = () => {
setDataSource((state) => [
...state,
{
key: uuid(),
variable: '',
optional: true,
},
]);
};
return ( return (
<section <section
className={classNames({ className={classNames({
@ -106,12 +154,20 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
> >
<Form.Item <Form.Item
label="Orchestrate" label="Orchestrate"
name="orchestrate"
rules={[{ required: true, message: 'Please input!' }]} rules={[{ required: true, message: 'Please input!' }]}
name={['prompt_config', 'system']}
initialValue={`你是一个智能助手,请总结知识库的内容来回答问题,请列举知识库中的数据详细回答。当所有知识库内容都与问题无关时,你的回答必须包括“知识库中未找到您要的答案!”这句话。回答需要考虑聊天历史。
{knowledge}
`}
> >
<Input.TextArea autoSize={{ maxRows: 5, minRows: 5 }} /> <Input.TextArea autoSize={{ maxRows: 5, minRows: 5 }} />
</Form.Item> </Form.Item>
<Divider></Divider> <Divider></Divider>
<SimilaritySlider></SimilaritySlider>
<Form.Item<FieldType> label="Top n" name={'top_n'} initialValue={0}>
<Slider max={30} />
</Form.Item>
<section className={classNames(styles.variableContainer)}> <section className={classNames(styles.variableContainer)}>
<Row align={'middle'} justify="end"> <Row align={'middle'} justify="end">
<Col span={6} className={styles.variableAlign}> <Col span={6} className={styles.variableAlign}>
@ -139,25 +195,8 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
</Row> </Row>
)} )}
</section> </section>
<Form.Item
label="Select one context"
name="context"
rules={[
{
required: true,
message: 'Please select your favourite colors!',
type: 'array',
},
]}
>
<Select mode="multiple" placeholder="Please select favourite colors">
<Option value="red">Red</Option>
<Option value="green">Green</Option>
<Option value="blue">Blue</Option>
</Select>
</Form.Item>
</section> </section>
); );
}; };
export default PromptEngine; export default forwardRef(PromptEngine);

View File

@ -0,0 +1,29 @@
import { IDialog } from '@/interfaces/database/chat';
import { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'umi';
export const useFetchDialogList = () => {
const dispatch = useDispatch();
const dialogList: IDialog[] = useSelector(
(state: any) => state.chatModel.dialogList,
);
useEffect(() => {
dispatch({ type: 'chatModel/listDialog' });
}, [dispatch]);
return dialogList;
};
export const useSetDialog = () => {
const dispatch = useDispatch();
const setDialog = useCallback(
(payload: IDialog) => {
dispatch({ type: 'chatModel/setDialog', payload });
},
[dispatch],
);
return setDialog;
};

View File

@ -4,6 +4,17 @@
.chatAppWrapper { .chatAppWrapper {
width: 288px; width: 288px;
padding: 26px; padding: 26px;
.chatAppCard {
:global(.ant-card-body) {
padding: 10px;
}
.cubeIcon {
&:hover {
cursor: pointer;
}
}
}
} }
.chatTitleWrapper { .chatTitleWrapper {
width: 220px; width: 220px;

View File

@ -1,14 +1,73 @@
import { FormOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons';
import { Button, Card, Divider, Flex, Space, Tag } from 'antd'; import {
import { useSelector } from 'umi'; Button,
Card,
Divider,
Dropdown,
Flex,
MenuProps,
Space,
Tag,
} from 'antd';
import ChatContainer from './chat-container'; import ChatContainer from './chat-container';
import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg';
import ModalManager from '@/components/modal-manager'; import ModalManager from '@/components/modal-manager';
import classNames from 'classnames';
import ChatConfigurationModal from './chat-configuration-modal'; import ChatConfigurationModal from './chat-configuration-modal';
import { useFetchDialogList } from './hooks';
import { useState } from 'react';
import styles from './index.less'; import styles from './index.less';
const Chat = () => { const Chat = () => {
const { name } = useSelector((state: any) => state.chatModel); const dialogList = useFetchDialogList();
const [activated, setActivated] = useState<string>('');
const handleAppCardEnter = (id: string) => () => {
setActivated(id);
};
const handleAppCardLeave = () => {
setActivated('');
};
const items: MenuProps['items'] = [
{
key: '1',
label: (
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.antgroup.com"
>
1st menu item
</a>
),
},
];
const appItems: MenuProps['items'] = [
{
key: '1',
label: (
<Space>
<EditOutlined />
Edit
</Space>
),
},
{ type: 'divider' },
{
key: '2',
label: (
<Space>
<DeleteOutlined />
Delete chat
</Space>
),
},
];
return ( return (
<Flex className={styles.chatWrapper}> <Flex className={styles.chatWrapper}>
@ -32,9 +91,33 @@ const Chat = () => {
</ModalManager> </ModalManager>
<Divider></Divider> <Divider></Divider>
<Card> <Space direction={'vertical'} size={'middle'}>
<p>Card content</p> {dialogList.map((x) => (
</Card> <Card
key={x.id}
className={classNames(styles.chatAppCard)}
onMouseEnter={handleAppCardEnter(x.id)}
onMouseLeave={handleAppCardLeave}
>
<Flex justify="space-between" align="center">
<Space>
{x.icon}
<section>
<b>{x.name}</b>
<div>{x.description}</div>
</section>
</Space>
{activated === x.id && (
<section>
<Dropdown menu={{ items: appItems }}>
<ChatAppCube className={styles.cubeIcon}></ChatAppCube>
</Dropdown>
</section>
)}
</Flex>
</Card>
))}
</Space>
</Flex> </Flex>
</Flex> </Flex>
<Divider type={'vertical'} className={styles.divider}></Divider> <Divider type={'vertical'} className={styles.divider}></Divider>
@ -49,7 +132,9 @@ const Chat = () => {
<b>Chat</b> <b>Chat</b>
<Tag>25</Tag> <Tag>25</Tag>
</Space> </Space>
<FormOutlined /> <Dropdown menu={{ items }}>
<FormOutlined />
</Dropdown>
</Flex> </Flex>
<Divider></Divider> <Divider></Divider>
<section className={styles.chatTitleContent}>today</section> <section className={styles.chatTitleContent}>today</section>

View File

@ -1,13 +1,18 @@
import { IDialog } from '@/interfaces/database/chat';
import chatService from '@/services/chatService';
import { message } from 'antd';
import { DvaModel } from 'umi'; import { DvaModel } from 'umi';
export interface ChatModelState { export interface ChatModelState {
name: string; name: string;
dialogList: IDialog[];
} }
const model: DvaModel<ChatModelState> = { const model: DvaModel<ChatModelState> = {
namespace: 'chatModel', namespace: 'chatModel',
state: { state: {
name: 'kate', name: 'kate',
dialogList: [],
}, },
reducers: { reducers: {
save(state, action) { save(state, action) {
@ -16,16 +21,41 @@ const model: DvaModel<ChatModelState> = {
...action.payload, ...action.payload,
}; };
}, },
}, setDialogList(state, { payload }) {
subscriptions: { return {
setup({ dispatch, history }) { ...state,
return history.listen((query) => { dialogList: payload,
console.log(query); };
});
}, },
}, },
effects: { effects: {
*query({ payload }, { call, put }) {}, *getDialog({ payload }, { call, put }) {
const { data } = yield call(chatService.getDialog, payload);
},
*setDialog({ payload }, { call, put }) {
const { data } = yield call(chatService.setDialog, payload);
if (data.retcode === 0) {
yield put({ type: 'listDialog' });
message.success('Created successfully !');
}
},
*listDialog({ payload }, { call, put }) {
const { data } = yield call(chatService.listDialog, payload);
yield put({ type: 'setDialogList', payload: data.data });
},
*listConversation({ payload }, { call, put }) {
const { data } = yield call(chatService.listConversation, payload);
},
*getConversation({ payload }, { call, put }) {
const { data } = yield call(chatService.getConversation, payload);
},
*setConversation({ payload }, { call, put }) {
const { data } = yield call(chatService.setConversation, payload);
},
*completeConversation({ payload }, { call, put }) {
const { data } = yield call(chatService.completeConversation, payload);
},
}, },
}; };

View File

@ -2,33 +2,14 @@ import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
import ModalManager from '@/components/modal-manager'; import ModalManager from '@/components/modal-manager';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Button, Flex, Space } from 'antd'; import { Button, Flex, Space } from 'antd';
import { useCallback, useEffect } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi';
import KnowledgeCard from './knowledge-card'; import KnowledgeCard from './knowledge-card';
import KnowledgeCreatingModal from './knowledge-creating-modal'; import KnowledgeCreatingModal from './knowledge-creating-modal';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import styles from './index.less'; import styles from './index.less';
const Knowledge = () => { const Knowledge = () => {
const dispatch = useDispatch(); const data = useFetchKnowledgeList();
const knowledgeModel = useSelector((state: any) => state.knowledgeModel);
const navigate = useNavigate();
const { data = [] } = knowledgeModel;
const fetchList = useCallback(() => {
dispatch({
type: 'knowledgeModel/getList',
payload: {},
});
}, []);
// const handleAddKnowledge = () => {
// navigate(`/knowledge/${KnowledgeRouteKey.Configuration}`);
// };
useEffect(() => {
fetchList();
}, [fetchList]);
return ( return (
<div className={styles.knowledge}> <div className={styles.knowledge}>

View File

@ -37,7 +37,7 @@ const SettingList = () => {
type: 'settingModel/my_llm', type: 'settingModel/my_llm',
payload: {}, payload: {},
}); });
}, []); }, [dispatch]);
return ( return (
<div <div

View File

@ -0,0 +1,48 @@
import api from '@/utils/api';
import registerServer from '@/utils/registerServer';
import request from '@/utils/request';
const {
getDialog,
setDialog,
listDialog,
getConversation,
setConversation,
completeConversation,
listConversation,
} = api;
const methods = {
getDialog: {
url: getDialog,
method: 'get',
},
setDialog: {
url: setDialog,
method: 'post',
},
listDialog: {
url: listDialog,
method: 'get',
},
listConversation: {
url: listConversation,
method: 'get',
},
getConversation: {
url: getConversation,
method: 'get',
},
setConversation: {
url: setConversation,
method: 'post',
},
completeConversation: {
url: completeConversation,
method: 'post',
},
} as const;
const chatService = registerServer<keyof typeof methods>(methods, request);
export default chatService;

View File

@ -42,4 +42,14 @@ export default {
document_create: `${api_host}/document/create`, document_create: `${api_host}/document/create`,
document_run: `${api_host}/document/run`, document_run: `${api_host}/document/run`,
document_change_parser: `${api_host}/document/change_parser`, document_change_parser: `${api_host}/document/change_parser`,
setDialog: `${api_host}/dialog/set`,
getDialog: `${api_host}/dialog/get`,
listDialog: `${api_host}/dialog/list`,
setConversation: `${api_host}/conversation/set`,
getConversation: `${api_host}/conversation/get`,
listConversation: `${api_host}/conversation/list`,
removeConversation: `${api_host}/conversation/rm`,
completeConversation: `${api_host}/conversation/completion`,
}; };

View File

@ -5,22 +5,23 @@
*/ */
// import numeral from 'numeral'; // import numeral from 'numeral';
import JSEncrypt from 'jsencrypt';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import JSEncrypt from 'jsencrypt';
export const getWidth = () => { export const getWidth = () => {
return { width: window.innerWidth }; return { width: window.innerWidth };
}; };
export const rsaPsw = (password: string) => { export const rsaPsw = (password: string) => {
const pub = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB-----END PUBLIC KEY-----" const pub =
const encryptor = new JSEncrypt() '-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB-----END PUBLIC KEY-----';
const encryptor = new JSEncrypt();
encryptor.setPublicKey(pub) encryptor.setPublicKey(pub);
return encryptor.encrypt(Base64.encode(password)) return encryptor.encrypt(Base64.encode(password));
} };
export default { export default {
getWidth, getWidth,
rsaPsw rsaPsw,
}; };