apphouse
Version:
Component library for React that uses observable state management and theme-able components.
208 lines (185 loc) • 5.86 kB
text/typescript
import { makeAutoObservable } from 'mobx';
import {} from 'openai';
import { Chat, List, getErrorMessage } from '..';
import { getUniqueId } from '../utils/string/getUniqueId';
import { ChatHistory } from './Chat';
import { ChatCompletionMessageParam } from 'openai/resources/index.mjs';
enum OpenAIErrorCodes {
invalidRequest = 'invalid-request',
contextLengthExceeded = 'context_length_exceeded'
}
export interface OpenAiChatApi {
chatCompletion: (history: ChatHistory) => Promise<any>;
createAudioTranscription: (file: File) => Promise<any>;
uploadFile: (file: File) => Promise<any>;
}
export type ChatCompletionMessage = ChatCompletionMessageParam & {
id: string;
};
export type ChatRoomHistory = List<ChatCompletionMessage>;
export class OpenAiChat {
chat: Chat;
loadingAiResponse: boolean;
api: OpenAiChatApi;
constructor(api: OpenAiChatApi) {
this.api = api;
const openAiUsers = [
{ id: 'user', name: 'User', role: 'user' },
{ id: 'assistant', name: 'Assistant', role: 'assistant' },
{ id: 'error', name: 'Error', role: 'system' },
{ id: 'system', name: 'System', role: 'system' }
];
this.chat = new Chat(openAiUsers, []);
this.loadingAiResponse = false;
this.chat.addMessage({
id: getUniqueId(),
userId: 'assistant',
content: 'Hello, how can I help you today?'
});
makeAutoObservable(this);
}
get lastAssistantMessageIndex() {
const lastAssistantMessageIndex = this.history.values.map(
(message, index) => {
if (message.userId === 'assistant') {
return index;
}
return 0;
}
);
return lastAssistantMessageIndex[lastAssistantMessageIndex.length - 1];
}
get history() {
return this.chat.history;
}
/**
* The api does not include an id for the message, so we strip these out
*/
get chatAiChatHistory() {
return this.chat.history.values.map(({ id, ...message }) => message);
}
addMessage = (message: ChatCompletionMessage) => {
this.history.set({
id: message.id,
userId: message.role,
content: message.content || ('' as any)
});
};
requestAudioTranscription = async (file: File) => {
this.addMessage({
id: getUniqueId(),
role: 'user',
content: `request transcription for ${file.name}`
});
this.setAiLoading(true);
const response = await this.api.createAudioTranscription(file);
if (response.type === 'error') {
this.setAiLoading(false);
this.addMessage({
id: getUniqueId(),
role: 'system',
content: response.message || ''
});
} else {
this.setAiLoading(false);
this.addMessage({
id: getUniqueId(),
role: 'assistant',
content: response.message || ''
});
}
};
uploadFile = async (file: File) => {
this.addMessage({
id: getUniqueId(),
role: 'user',
content: `uploading file for fine tunning ${file.name}`
});
this.setAiLoading(true);
try {
const response = await this.api.uploadFile(file);
if (response?.type === 'error') {
this.handleErrorMessage(response);
this.setAiLoading(false);
} else {
this.setAiLoading(false);
this.addMessage({
id: getUniqueId(),
role: 'assistant',
content: String(response?.message) || ''
});
}
} catch (error) {
this.handleErrorMessage(error);
this.setAiLoading(false);
}
};
sendUserMessage = async (message: string) => {
this.addMessage({ id: getUniqueId(), role: 'user', content: message });
this.setAiLoading(true);
try {
const response = await this.api.chatCompletion(this.history);
if (response.type === 'error') {
this.handleErrorMessage(response);
try {
this.setAiLoading(false);
} catch (error) {
console.log(error);
}
} else {
this.addMessage({
id: getUniqueId(),
role: 'assistant',
content: response.message || ''
});
this.setAiLoading(false);
}
} catch (error) {
this.handleErrorMessage(error);
this.setAiLoading(false);
}
};
setAiLoading = (loading: boolean) => {
this.loadingAiResponse = loading;
};
/**
* Handle error when content length is exceeded
* code: "context_length_exceeded"
* message: "This model's maximum context length is 4097 tokens. However, your messages resulted in 4176 tokens. Please reduce the length of the messages."
* param: "messages"
* type: "invalid_request_error"
*/
private handleErrorContentLengthExceeded = (message?: string) => {
// When the user sends a message it takes into account the messages
// in the history.
// Lets try to make the history smaller by clearing old messages and send the message again.
if (this.history.length > 1) {
this.history.shift();
// let's try to send the message again
this.sendUserMessage(message || '');
} else {
this.handleErrorMessage({
code: 'context_length_exceeded',
type: 'invalid_request_error',
message:
"This model's maximum context length is 4097 tokens. However, your messages resulted in 4176 tokens. Please reduce the length of the messages."
});
}
};
private resolveErrorMessage = (error: any) => {
const message = getErrorMessage(error);
this.addMessage({
id: getUniqueId(),
role: 'system',
content: message || ''
});
return message;
};
private handleErrorMessage = async (error: any, text?: string) => {
if (error.code === OpenAIErrorCodes.contextLengthExceeded) {
this.handleErrorContentLengthExceeded(text);
return;
}
return this.resolveErrorMessage(error);
};
}