chat-with-gpt/app/src/chat-manager.ts

365 lines
11 KiB
TypeScript
Raw Normal View History

2023-03-06 13:30:58 +00:00
import { BroadcastChannel } from 'broadcast-channel';
import EventEmitter from 'events';
import MiniSearch, { SearchResult } from 'minisearch'
import { v4 as uuidv4 } from 'uuid';
2023-03-14 11:00:40 +00:00
import { Chat, deserializeChat, getOpenAIMessageFromMessage, Message, Parameters, serializeChat, UserSubmittedMessage } from './types';
2023-03-06 13:30:58 +00:00
import { MessageTree } from './message-tree';
import { createStreamingChatCompletion } from './openai';
import { createTitle } from './titles';
import { ellipsize, sleep } from './utils';
import * as idb from './idb';
2023-03-09 19:50:57 +00:00
import { selectMessagesToSendSafely } from './tokenizer';
2023-03-06 13:30:58 +00:00
export const channel = new BroadcastChannel('chats');
export class ChatManager extends EventEmitter {
public chats = new Map<string, Chat>();
public search = new Search(this.chats);
private loaded = false;
private changed = false;
2023-03-14 11:00:40 +00:00
private activeReplies = new Map<string, Message>();
2023-03-06 13:30:58 +00:00
constructor() {
super();
this.load();
this.on('update', () => {
this.changed = true;
});
channel.onmessage = (message: {
type: 'chat-update',
2023-03-14 11:00:40 +00:00
data: string,
2023-03-06 13:30:58 +00:00
}) => {
2023-03-14 11:00:40 +00:00
const chat = deserializeChat(message.data);
const id = chat.id;
this.chats.set(id, chat);
2023-03-06 13:30:58 +00:00
this.emit(id);
};
(async () => {
while (true) {
await sleep(100);
if (this.loaded && this.changed) {
this.changed = false;
await this.save();
}
}
})();
}
public async createChat(): Promise<string> {
const id = uuidv4();
const chat: Chat = {
id,
messages: new MessageTree(),
created: Date.now(),
updated: Date.now(),
};
this.chats.set(id, chat);
this.search.update(chat);
2023-03-14 11:00:40 +00:00
channel.postMessage({ type: 'chat-update', data: serializeChat(chat) });
2023-03-06 13:30:58 +00:00
return id;
}
public async sendMessage(message: UserSubmittedMessage) {
const chat = this.chats.get(message.chatID);
if (!chat) {
throw new Error('Chat not found');
}
const newMessage: Message = {
id: uuidv4(),
parentID: message.parentID,
chatID: chat.id,
timestamp: Date.now(),
role: 'user',
content: message.content,
done: true,
};
2023-03-09 09:38:59 +00:00
chat.messages.addMessage(newMessage);
chat.updated = Date.now();
this.emit(chat.id);
this.emit('messages', [newMessage]);
2023-03-14 11:00:40 +00:00
channel.postMessage({ type: 'chat-update', data: serializeChat(chat) });
2023-03-09 09:38:59 +00:00
const messages: Message[] = message.parentID
? chat.messages.getMessageChainTo(message.parentID)
: [];
messages.push(newMessage);
await this.getReply(messages, message.requestedParameters);
}
public async regenerate(message: Message, requestedParameters: Parameters) {
const chat = this.chats.get(message.chatID);
if (!chat) {
throw new Error('Chat not found');
}
const messages: Message[] = message.parentID
? chat.messages.getMessageChainTo(message.parentID)
: [];
await this.getReply(messages, requestedParameters);
}
private async getReply(messages: Message[], requestedParameters: Parameters) {
const latestMessage = messages[messages.length - 1];
const chat = this.chats.get(latestMessage.chatID);
if (!chat) {
throw new Error('Chat not found');
}
2023-03-06 13:30:58 +00:00
const reply: Message = {
id: uuidv4(),
2023-03-09 09:38:59 +00:00
parentID: latestMessage.id,
chatID: latestMessage.chatID,
2023-03-06 13:30:58 +00:00
timestamp: Date.now(),
role: 'assistant',
content: '',
done: false,
};
2023-03-14 11:00:40 +00:00
this.activeReplies.set(reply.id, reply);
2023-03-06 13:30:58 +00:00
chat.messages.addMessage(reply);
chat.updated = Date.now();
this.emit(chat.id);
2023-03-14 11:00:40 +00:00
channel.postMessage({ type: 'chat-update', data: serializeChat(chat) });
2023-03-06 13:30:58 +00:00
2023-03-08 21:30:11 +00:00
const messagesToSend = selectMessagesToSendSafely(messages.map(getOpenAIMessageFromMessage));
2023-03-06 13:30:58 +00:00
2023-03-14 11:00:40 +00:00
const { emitter, cancel } = await createStreamingChatCompletion(messagesToSend, requestedParameters);
2023-03-06 13:30:58 +00:00
2023-03-14 11:00:40 +00:00
let lastChunkReceivedAt = Date.now();
const onError = () => {
if (reply.done) {
return;
2023-03-06 13:30:58 +00:00
}
2023-03-14 11:00:40 +00:00
clearInterval(timer);
cancel();
reply.content += "\n\nI'm sorry, I'm having trouble connecting to OpenAI. Please make sure you've entered your OpenAI API key correctly and try again.";
reply.content = reply.content.trim();
reply.done = true;
this.activeReplies.delete(reply.id);
chat.messages.updateMessage(reply);
chat.updated = Date.now();
this.emit(chat.id);
this.emit('messages', [reply]);
channel.postMessage({ type: 'chat-update', data: serializeChat(chat) });
};
let timer = setInterval(() => {
const sinceLastChunk = Date.now() - lastChunkReceivedAt;
if (sinceLastChunk > 10000 && !reply.done) {
onError();
}
}, 2000);
emitter.on('error', () => {
if (!reply.content && !reply.done) {
lastChunkReceivedAt = Date.now();
onError();
}
});
2023-03-06 13:30:58 +00:00
2023-03-14 11:00:40 +00:00
emitter.on('data', (data: string) => {
if (reply.done) {
cancel();
return;
}
lastChunkReceivedAt = Date.now();
2023-03-06 13:30:58 +00:00
reply.content = data;
chat.messages.updateMessage(reply);
this.emit(chat.id);
2023-03-14 11:00:40 +00:00
channel.postMessage({ type: 'chat-update', data: serializeChat(chat) });
2023-03-06 13:30:58 +00:00
});
2023-03-14 11:00:40 +00:00
emitter.on('done', async () => {
if (reply.done) {
return;
}
clearInterval(timer);
lastChunkReceivedAt = Date.now();
2023-03-06 13:30:58 +00:00
reply.done = true;
2023-03-14 11:00:40 +00:00
this.activeReplies.delete(reply.id);
2023-03-06 13:30:58 +00:00
chat.messages.updateMessage(reply);
chat.updated = Date.now();
this.emit(chat.id);
this.emit('messages', [reply]);
this.emit('update');
2023-03-14 11:00:40 +00:00
channel.postMessage({ type: 'chat-update', data: serializeChat(chat) });
2023-03-06 13:30:58 +00:00
setTimeout(() => this.search.update(chat), 500);
if (!chat.title) {
2023-03-09 09:38:59 +00:00
chat.title = await createTitle(chat, requestedParameters.apiKey);
2023-03-06 13:30:58 +00:00
if (chat.title) {
this.emit(chat.id);
this.emit('title', chat.id, chat.title);
this.emit('update');
2023-03-14 11:00:40 +00:00
channel.postMessage({ type: 'chat-update', data: serializeChat(chat) });
2023-03-06 13:30:58 +00:00
setTimeout(() => this.search.update(chat), 500);
}
}
});
}
private async save() {
const serialized = Array.from(this.chats.values())
.map((c) => {
const serialized = { ...c } as any;
serialized.messages = c.messages.serialize();
return serialized;
});
await idb.set('chats', serialized);
}
2023-03-14 11:00:40 +00:00
public cancelReply(id: string) {
const reply = this.activeReplies.get(id);
if (reply) {
reply.done = true;
this.activeReplies.delete(reply.id);
const chat = this.chats.get(reply.chatID);
const message = chat?.messages.get(id);
if (message) {
message.done = true;
this.emit(reply.chatID);
this.emit('messages', [reply]);
this.emit('update');
channel.postMessage({ type: 'chat-update', data: serializeChat(chat!) });
}
} else {
console.log('failed to find reply');
}
}
2023-03-06 13:30:58 +00:00
private async load() {
const serialized = await idb.get('chats');
if (serialized) {
for (const chat of serialized) {
const messages = new MessageTree();
for (const m of chat.messages) {
messages.addMessage(m);
}
chat.messages = messages;
this.loadChat(chat);
}
this.emit('update');
}
this.loaded = true;
}
public loadChat(chat: Chat) {
if (!chat?.id) {
return;
}
2023-03-14 11:00:40 +00:00
const existing = this.chats.get(chat.id);
if (existing && existing.title && !chat.title) {
chat.title = existing.title;
}
chat.created = chat.messages.first?.timestamp || 0;
chat.updated = chat.messages.mostRecentLeaf().timestamp;
2023-03-06 13:30:58 +00:00
this.chats.set(chat.id, chat);
this.search.update(chat);
this.emit(chat.id);
}
public get(id: string): Chat | undefined {
return this.chats.get(id);
}
}
export class Search {
private index = new MiniSearch({
fields: ['value'],
storeFields: ['id', 'value'],
});
constructor(private chats: Map<string, Chat>) {
}
public update(chat: Chat) {
const messages = chat.messages.serialize()
.map((m: Message) => m.content)
.join('\n\n');
const doc = {
id: chat.id,
value: chat.title + '\n\n' + messages,
};
if (!this.index.has(chat.id)) {
this.index.add(doc);
} else {
this.index.replace(doc);
}
}
public query(query: string) {
if (!query?.trim().length) {
const searchResults = Array.from(this.chats.values())
.sort((a, b) => b.updated - a.updated)
.slice(0, 10);
const results = this.processSearchResults(searchResults);
return results;
}
let searchResults = this.index.search(query, { fuzzy: 0.2 });
let output = this.processSearchResults(searchResults);
if (!output.length) {
searchResults = this.index.search(query, { prefix: true });
output = this.processSearchResults(searchResults);
}
return output;
}
private processSearchResults(searchResults: SearchResult[] | Chat[]) {
const output: any[] = [];
for (const item of searchResults) {
const chatID = item.id;
let chat = this.chats.get(chatID);
2023-03-06 13:30:58 +00:00
if (!chat) {
continue;
}
chat = { ...chat };
2023-03-06 13:30:58 +00:00
let description = chat.messages?.first?.content || '';
description = ellipsize(description, 400);
if (!chat.title) {
chat.title = ellipsize(description, 100);
}
2023-03-08 21:30:11 +00:00
2023-03-06 13:30:58 +00:00
if (!chat.title || !description) {
continue;
}
output.push({
chatID,
title: chat.title,
description,
});
}
return output;
}
}
2023-03-09 20:18:52 +00:00
const chatManager = new ChatManager();
export default chatManager;