fix: chat history with memory

This commit is contained in:
Yifei Zhang 2023-03-19 16:09:30 +00:00
parent c133cae04b
commit 4d97c269ff
4 changed files with 59 additions and 50 deletions

View File

@ -26,7 +26,7 @@ import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg"; import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg"; import DownloadIcon from "../icons/download.svg";
import { Message, SubmitKey, useChatStore, Theme } from "../store"; import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { showModal } from "./ui-lib"; import { showModal } from "./ui-lib";
import { copyToClipboard, downloadAs, isIOS } from "../utils"; import { copyToClipboard, downloadAs, isIOS } from "../utils";
@ -189,8 +189,8 @@ export function Chat(props: { showSideBar?: () => void }) {
return ( return (
<div className={styles.chat} key={session.id}> <div className={styles.chat} key={session.id}>
<div className={styles["window-header"]}> <div className={styles["window-header"]}>
<div> <div className={styles["window-header-title"]}>
<div className={styles["window-header-title"]}>{session.topic}</div> <div className={styles["window-header-main-title"]}>{session.topic}</div>
<div className={styles["window-header-sub-title"]}> <div className={styles["window-header-sub-title"]}>
ChatGPT {session.messages.length} ChatGPT {session.messages.length}
</div> </div>
@ -210,7 +210,7 @@ export function Chat(props: { showSideBar?: () => void }) {
bordered bordered
title="查看压缩后的历史 Prompt" title="查看压缩后的历史 Prompt"
onClick={() => { onClick={() => {
showMemoryPrompt(session.memoryPrompt) showMemoryPrompt(session)
}} }}
/> />
</div> </div>
@ -323,12 +323,12 @@ function exportMessages(messages: Message[], topic: string) {
}) })
} }
function showMemoryPrompt(prompt: string) { function showMemoryPrompt(session: ChatSession) {
showModal({ showModal({
title: "上下文记忆 Prompt", children: <div className="markdown-body"> title: `上下文记忆 Prompt (${session.lastSummarizeIndex} of ${session.messages.length})`, children: <div className="markdown-body">
<pre className={styles['export-content']}>{prompt}</pre> <pre className={styles['export-content']}>{session.memoryPrompt || '无'}</pre>
</div>, actions: [ </div>, actions: [
<IconButton key="copy" icon={<CopyIcon />} bordered text="全部复制" onClick={() => copyToClipboard(prompt)} />, <IconButton key="copy" icon={<CopyIcon />} bordered text="全部复制" onClick={() => copyToClipboard(session.memoryPrompt)} />,
] ]
}) })
} }

View File

@ -26,8 +26,8 @@ export function Settings(props: { closeSettings: () => void }) {
return ( return (
<> <>
<div className={styles["window-header"]}> <div className={styles["window-header"]}>
<div> <div className={styles["window-header-title"]}>
<div className={styles["window-header-title"]}></div> <div className={styles["window-header-main-title"]}></div>
<div className={styles["window-header-sub-title"]}></div> <div className={styles["window-header-sub-title"]}></div>
</div> </div>
<div className={styles["window-actions"]}> <div className={styles["window-actions"]}>
@ -140,14 +140,14 @@ export function Settings(props: { closeSettings: () => void }) {
</List> </List>
<List> <List>
<ListItem> <ListItem>
<div className={styles["settings-title"]}></div> <div className={styles["settings-title"]}></div>
<input <input
type="range" type="range"
title={config.historyMessageCount.toString()} title={config.historyMessageCount.toString()}
value={config.historyMessageCount} value={config.historyMessageCount}
min="5" min="2"
max="20" max="25"
step="5" step="2"
onChange={(e) => onChange={(e) =>
updateConfig( updateConfig(
(config) => (config) =>
@ -157,7 +157,6 @@ export function Settings(props: { closeSettings: () => void }) {
></input> ></input>
</ListItem> </ListItem>
<ListItem> <ListItem>
<div className={styles["settings-title"]}> <div className={styles["settings-title"]}>

View File

@ -8,18 +8,22 @@
} }
.window-header-title { .window-header-title {
font-size: 20px; max-width: calc(100% - 100px);
font-weight: bolder;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
max-width: 50vw;
}
.window-header-sub-title { .window-header-main-title {
font-size: 14px; font-size: 20px;
margin-top: 5px; font-weight: bolder;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
max-width: 50vw;
}
.window-header-sub-title {
font-size: 14px;
margin-top: 5px;
}
} }
.window-actions { .window-actions {
@ -28,4 +32,4 @@
.window-action-button { .window-action-button {
margin-left: 10px; margin-left: 10px;
} }

View File

@ -23,7 +23,7 @@ export enum Theme {
Light = "light", Light = "light",
} }
interface ChatConfig { export interface ChatConfig {
maxToken?: number; maxToken?: number;
historyMessageCount: number; // -1 means all historyMessageCount: number; // -1 means all
compressMessageLengthThreshold: number; compressMessageLengthThreshold: number;
@ -35,7 +35,7 @@ interface ChatConfig {
} }
const DEFAULT_CONFIG: ChatConfig = { const DEFAULT_CONFIG: ChatConfig = {
historyMessageCount: 5, historyMessageCount: 4,
compressMessageLengthThreshold: 500, compressMessageLengthThreshold: 500,
sendBotMessages: true as boolean, sendBotMessages: true as boolean,
submitKey: SubmitKey.CtrlEnter as SubmitKey, submitKey: SubmitKey.CtrlEnter as SubmitKey,
@ -44,13 +44,13 @@ const DEFAULT_CONFIG: ChatConfig = {
tightBorder: false, tightBorder: false,
}; };
interface ChatStat { export interface ChatStat {
tokenCount: number; tokenCount: number;
wordCount: number; wordCount: number;
charCount: number; charCount: number;
} }
interface ChatSession { export interface ChatSession {
id: number; id: number;
topic: string; topic: string;
memoryPrompt: string; memoryPrompt: string;
@ -190,24 +190,20 @@ export const useChatStore = create<ChatStore>()(
}, },
onNewMessage(message) { onNewMessage(message) {
get().updateCurrentSession(session => {
session.lastUpdate = new Date().toLocaleString()
})
get().updateStat(message); get().updateStat(message);
get().summarizeSession(); get().summarizeSession();
}, },
async onUserInput(content) { async onUserInput(content) {
const message: Message = { const userMessage: Message = {
role: "user", role: "user",
content, content,
date: new Date().toLocaleString(), date: new Date().toLocaleString(),
}; };
get().updateCurrentSession((session) => {
session.messages.push(message);
});
// get last five messges
const messages = get().currentSession().messages.concat(message);
const botMessage: Message = { const botMessage: Message = {
content: "", content: "",
role: "assistant", role: "assistant",
@ -215,13 +211,18 @@ export const useChatStore = create<ChatStore>()(
streaming: true, streaming: true,
}; };
// get recent messages
const recentMessages = get().getMessagesWithMemory()
const sendMessages = recentMessages.concat(userMessage)
// save user's and bot's message
get().updateCurrentSession((session) => { get().updateCurrentSession((session) => {
session.messages.push(userMessage);
session.messages.push(botMessage); session.messages.push(botMessage);
}); });
const recentMessages = get().getMessagesWithMemory() console.log('[User Input] ', sendMessages)
requestChatStream(sendMessages, {
requestChatStream(recentMessages, {
onMessage(content, done) { onMessage(content, done) {
if (done) { if (done) {
botMessage.streaming = false; botMessage.streaming = false;
@ -243,11 +244,12 @@ export const useChatStore = create<ChatStore>()(
getMessagesWithMemory() { getMessagesWithMemory() {
const session = get().currentSession() const session = get().currentSession()
const config = get().config const config = get().config
const recentMessages = session.messages.slice(-config.historyMessageCount); const n = session.messages.length
const recentMessages = session.messages.slice(n - config.historyMessageCount);
const memoryPrompt: Message = { const memoryPrompt: Message = {
role: 'system', role: 'system',
content: '这是你和用户的历史聊天总结' + session.memoryPrompt, content: '这是 ai 和用户的历史聊天总结作为前情提要' + session.memoryPrompt,
date: '' date: ''
} }
@ -285,22 +287,26 @@ export const useChatStore = create<ChatStore>()(
}); });
} }
const config = get().config
const messages = get().getMessagesWithMemory() const messages = get().getMessagesWithMemory()
const toBeSummarizedMsgs = messages.slice(session.lastSummarizeIndex) const toBeSummarizedMsgs = get().getMessagesWithMemory()
const historyMsgLength = session.memoryPrompt.length + toBeSummarizedMsgs.reduce((pre, cur) => pre + cur.content.length, 0) const historyMsgLength = toBeSummarizedMsgs.reduce((pre, cur) => pre + cur.content.length, 0)
const lastSummarizeIndex = messages.length const lastSummarizeIndex = session.messages.length
if (historyMsgLength > 500) {
console.log('[Chat History] ', messages, historyMsgLength, config.compressMessageLengthThreshold)
if (historyMsgLength > config.compressMessageLengthThreshold) {
requestChatStream(toBeSummarizedMsgs.concat({ requestChatStream(toBeSummarizedMsgs.concat({
role: 'system', role: 'system',
content: '总结一下你和用户的对话,用作后续的上下文提示 prompt控制在 50 字以内', content: '总结一下 ai 和用户的对话,用作后续的上下文提示 prompt控制在 100 字以内,你在回复时用 ai 自称',
date: '' date: ''
}), { }), {
filterBot: false, filterBot: false,
onMessage(message, done) { onMessage(message, done) {
session.memoryPrompt = message session.memoryPrompt = message
session.lastSummarizeIndex = lastSummarizeIndex
if (done) { if (done) {
console.log('[Memory] ', session.memoryPrompt) console.log('[Memory] ', session.memoryPrompt)
session.lastSummarizeIndex = lastSummarizeIndex
} }
}, },
onError(error) { onError(error) {