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

@@ -1,11 +1,20 @@
import { Form, Input } from 'antd';
import { Form, Input, Select } from 'antd';
import classNames from 'classnames';
import { ISegmentedContentProps } from './interface';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import styles from './index.less';
const { Option } = Select;
const AssistantSetting = ({ show }: ISegmentedContentProps) => {
const knowledgeList = useFetchKnowledgeList();
const knowledgeOptions = knowledgeList.map((x) => ({
label: x.name,
value: x.id,
}));
return (
<section
className={classNames({
@@ -19,15 +28,43 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
>
<Input placeholder="e.g. Resume Jarvis" />
</Form.Item>
<Form.Item name={'avatar'} label="Assistant avatar">
<Form.Item name={'icon'} label="Assistant avatar">
<Input />
</Form.Item>
<Form.Item name={'keywords'} label="Keywords">
<Input.TextArea autoSize={{ minRows: 3 }} />
<Form.Item name={'language'} label="Language" initialValue={'Chinese'}>
<Select
options={[
{ value: 'Chinese', label: 'Chinese' },
{ value: 'English', label: 'English' },
]}
/>
</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 }} />
</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>
);
};

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;
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 { Divider, Flex, Form, Modal, Segmented } from 'antd';
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 ModelSetting from './model-setting';
import PromptEngine from './prompt-engine';
import { useSetDialog } from '../hooks';
import { variableEnabledFieldMap } from './constants';
import styles from './index.less';
enum ConfigurationSegmented {
AssistantSetting = 'Assistant Setting',
ModelSetting = 'Model Setting',
PromptEngine = 'Prompt Engine',
ModelSetting = 'Model Setting',
}
const segmentedMap = {
@@ -45,10 +48,24 @@ const ChatConfigurationModal = ({
const [value, setValue] = useState<ConfigurationSegmented>(
ConfigurationSegmented.AssistantSetting,
);
const promptEngineRef = useRef(null);
const setDialog = useSetDialog();
const handleOk = async () => {
const x = await form.validateFields();
console.info(x);
const values = await form.validateFields();
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 = () => {
@@ -97,7 +114,14 @@ const ChatConfigurationModal = ({
colon={false}
>
{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>
</Modal>

View File

@@ -1,3 +1,14 @@
import { FormInstance } from 'antd';
export interface ISegmentedContentProps {
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 { useEffect } from 'react';
import { ISegmentedContentProps } from './interface';
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
import { variableEnabledFieldMap } from './constants';
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 (
<section
className={classNames({
@@ -15,135 +51,170 @@ const ModelSetting = ({ show }: ISegmentedContentProps) => {
>
<Form.Item
label="Model"
name="model"
// rules={[{ required: true, message: 'Please input!' }]}
name="llm_id"
rules={[{ required: true, message: 'Please select!' }]}
>
<Select />
<Select options={modelOptions} />
</Form.Item>
<Divider></Divider>
<Form.Item
label="Parameters"
name="parameters"
initialValue={ModelVariableType.Precise}
// rules={[{ required: true, message: 'Please input!' }]}
>
<Select />
<Select<ModelVariableType>
options={parameterOptions}
onChange={handleParametersChange}
/>
</Form.Item>
<Form.Item label="Temperature">
<Flex gap={20}>
<Form.Item label="Temperature" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item
name={'temperatureEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'temperature']}
noStyle
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>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'temperature']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
/>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label="Top P">
<Flex gap={20}>
<Form.Item label="Top P" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item name={'topPEnabled'} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'top_p']}
noStyle
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>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'top_p']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
/>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label="Presence Penalty">
<Flex gap={20}>
<Form.Item label="Presence Penalty" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item
name={'presencePenaltyEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'presence_penalty']}
noStyle
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>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'presence_penalty']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
/>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label="Frequency Penalty">
<Flex gap={20}>
<Form.Item label="Frequency Penalty" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item
name={'frequencyPenaltyEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'frequency_penalty']}
noStyle
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>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'frequency_penalty']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
/>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label="Max Tokens">
<Flex gap={20}>
<Form.Item label="Max Tokens" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item name={'maxTokensEnabled'} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'max_tokens']}
noStyle
rules={[{ required: true, message: 'Province is required' }]}
>
<Slider style={{ display: 'inline-block', width: '100%' }} />
<Slider className={styles.variableSlider} max={2048} />
</Form.Item>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'max_tokens']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={2048}
min={0}
/>
</Form.Item>
</Flex>

View File

@@ -1,3 +1,4 @@
import SimilaritySlider from '@/components/similarity-slider';
import { DeleteOutlined } from '@ant-design/icons';
import {
Button,
@@ -6,13 +7,19 @@ import {
Form,
Input,
Row,
Select,
Slider,
Switch,
Table,
TableProps,
} from 'antd';
import classNames from 'classnames';
import { useState } from 'react';
import {
ForwardedRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { v4 as uuid } from 'uuid';
import { EditableCell, EditableRow } from './editable-cell';
import { ISegmentedContentProps } from './interface';
@@ -21,12 +28,20 @@ import styles from './index.less';
interface DataType {
key: string;
variable: string;
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 components = {
@@ -52,6 +67,44 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
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'] = [
{
title: 'key',
@@ -71,8 +124,14 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
key: 'optional',
width: 40,
align: 'center',
render() {
return <Switch size="small" />;
render(text, record) {
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 (
<section
className={classNames({
@@ -106,12 +154,20 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
>
<Form.Item
label="Orchestrate"
name="orchestrate"
rules={[{ required: true, message: 'Please input!' }]}
name={['prompt_config', 'system']}
initialValue={`你是一个智能助手,请总结知识库的内容来回答问题,请列举知识库中的数据详细回答。当所有知识库内容都与问题无关时,你的回答必须包括“知识库中未找到您要的答案!”这句话。回答需要考虑聊天历史。
以下是知识库:
{knowledge}
以上是知识库。`}
>
<Input.TextArea autoSize={{ maxRows: 5, minRows: 5 }} />
</Form.Item>
<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)}>
<Row align={'middle'} justify="end">
<Col span={6} className={styles.variableAlign}>
@@ -139,25 +195,8 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
</Row>
)}
</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>
);
};
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 {
width: 288px;
padding: 26px;
.chatAppCard {
:global(.ant-card-body) {
padding: 10px;
}
.cubeIcon {
&:hover {
cursor: pointer;
}
}
}
}
.chatTitleWrapper {
width: 220px;

View File

@@ -1,14 +1,73 @@
import { FormOutlined } from '@ant-design/icons';
import { Button, Card, Divider, Flex, Space, Tag } from 'antd';
import { useSelector } from 'umi';
import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons';
import {
Button,
Card,
Divider,
Dropdown,
Flex,
MenuProps,
Space,
Tag,
} from 'antd';
import ChatContainer from './chat-container';
import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg';
import ModalManager from '@/components/modal-manager';
import classNames from 'classnames';
import ChatConfigurationModal from './chat-configuration-modal';
import { useFetchDialogList } from './hooks';
import { useState } from 'react';
import styles from './index.less';
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 (
<Flex className={styles.chatWrapper}>
@@ -32,9 +91,33 @@ const Chat = () => {
</ModalManager>
<Divider></Divider>
<Card>
<p>Card content</p>
</Card>
<Space direction={'vertical'} size={'middle'}>
{dialogList.map((x) => (
<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>
<Divider type={'vertical'} className={styles.divider}></Divider>
@@ -49,7 +132,9 @@ const Chat = () => {
<b>Chat</b>
<Tag>25</Tag>
</Space>
<FormOutlined />
<Dropdown menu={{ items }}>
<FormOutlined />
</Dropdown>
</Flex>
<Divider></Divider>
<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';
export interface ChatModelState {
name: string;
dialogList: IDialog[];
}
const model: DvaModel<ChatModelState> = {
namespace: 'chatModel',
state: {
name: 'kate',
dialogList: [],
},
reducers: {
save(state, action) {
@@ -16,16 +21,41 @@ const model: DvaModel<ChatModelState> = {
...action.payload,
};
},
},
subscriptions: {
setup({ dispatch, history }) {
return history.listen((query) => {
console.log(query);
});
setDialogList(state, { payload }) {
return {
...state,
dialogList: payload,
};
},
},
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);
},
},
};