205 lines
5.2 KiB
TypeScript
205 lines
5.2 KiB
TypeScript
export interface Message {
|
|
role: "system" | "user" | "assistant" | "function";
|
|
content: string;
|
|
name?: "example_user" | "example_assistant";
|
|
}
|
|
|
|
export interface ChunkMessage {
|
|
model: string;
|
|
choices: {
|
|
delta: { role: "assitant" | undefined; content: string | undefined };
|
|
}[];
|
|
}
|
|
|
|
export interface FetchResponse {
|
|
error?: any;
|
|
id: string;
|
|
object: string;
|
|
created: number;
|
|
model: string;
|
|
usage: {
|
|
prompt_tokens: number | undefined;
|
|
completion_tokens: number | undefined;
|
|
total_tokens: number | undefined;
|
|
};
|
|
choices: {
|
|
message: Message | undefined;
|
|
finish_reason: "stop" | "length";
|
|
index: number | undefined;
|
|
}[];
|
|
}
|
|
// https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
|
|
export function calculate_token_length(content: string): number {
|
|
const totalCount = content.length;
|
|
const chineseCount = content.match(/[\u00ff-\uffff]|\S+/g)?.length ?? 0;
|
|
const englishCount = totalCount - chineseCount;
|
|
const tokenLength = englishCount / 4 + (chineseCount * 4) / 3;
|
|
return ~~tokenLength;
|
|
}
|
|
|
|
class Chat {
|
|
OPENAI_API_KEY: string;
|
|
messages: Message[];
|
|
sysMessageContent: string;
|
|
total_tokens: number;
|
|
max_tokens: number;
|
|
tokens_margin: number;
|
|
apiEndpoint: string;
|
|
model: string;
|
|
temperature: number;
|
|
top_p: number;
|
|
presence_penalty: number;
|
|
frequency_penalty: number;
|
|
|
|
constructor(
|
|
OPENAI_API_KEY: string | undefined,
|
|
{
|
|
systemMessage = "你是一个有用的人工智能助理",
|
|
max_tokens = 4096,
|
|
tokens_margin = 1024,
|
|
apiEndPoint = "https://api.openai.com/v1/chat/completions",
|
|
model = "gpt-3.5-turbo",
|
|
temperature = 1.0,
|
|
top_p = 1,
|
|
presence_penalty = 0,
|
|
frequency_penalty = 0,
|
|
} = {}
|
|
) {
|
|
if (OPENAI_API_KEY === undefined) {
|
|
throw "OPENAI_API_KEY is undefined";
|
|
}
|
|
this.OPENAI_API_KEY = OPENAI_API_KEY;
|
|
this.messages = [];
|
|
this.total_tokens = 0;
|
|
this.max_tokens = max_tokens;
|
|
this.tokens_margin = tokens_margin;
|
|
this.sysMessageContent = systemMessage;
|
|
this.apiEndpoint = apiEndPoint;
|
|
this.model = model;
|
|
this.temperature = temperature;
|
|
this.top_p = top_p;
|
|
this.presence_penalty = presence_penalty;
|
|
this.frequency_penalty = frequency_penalty;
|
|
}
|
|
|
|
_fetch(stream = false) {
|
|
return fetch(this.apiEndpoint, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${this.OPENAI_API_KEY}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
model: this.model,
|
|
messages: [
|
|
{ role: "system", content: this.sysMessageContent },
|
|
...this.messages,
|
|
],
|
|
stream,
|
|
temperature: this.temperature,
|
|
top_p: this.top_p,
|
|
presence_penalty: this.presence_penalty,
|
|
frequency_penalty: this.frequency_penalty,
|
|
}),
|
|
});
|
|
}
|
|
|
|
async fetch(): Promise<FetchResponse> {
|
|
const resp = await this._fetch();
|
|
const j = await resp.json();
|
|
if (j.error !== undefined) {
|
|
throw JSON.stringify(j.error);
|
|
}
|
|
return j;
|
|
}
|
|
|
|
async say(content: string): Promise<string> {
|
|
this.messages.push({ role: "user", content });
|
|
await this.complete();
|
|
return this.messages.slice(-1)[0].content;
|
|
}
|
|
|
|
processFetchResponse(resp: FetchResponse): string {
|
|
if (resp.error !== undefined) {
|
|
throw JSON.stringify(resp.error);
|
|
}
|
|
this.total_tokens = resp?.usage?.total_tokens ?? 0;
|
|
if (resp?.choices[0]?.message) {
|
|
this.messages.push(resp?.choices[0]?.message);
|
|
}
|
|
|
|
if (resp.choices[0]?.finish_reason === "length") {
|
|
this.forceForgetSomeMessages();
|
|
} else {
|
|
this.forgetSomeMessages();
|
|
}
|
|
|
|
return (
|
|
resp?.choices[0]?.message?.content ?? `Error: ${JSON.stringify(resp)}`
|
|
);
|
|
}
|
|
|
|
async complete(): Promise<string> {
|
|
const resp = await this.fetch();
|
|
return this.processFetchResponse(resp);
|
|
}
|
|
|
|
completeWithSteam() {
|
|
this.total_tokens = this.messages
|
|
.map((msg) => this.calculate_token_length(msg.content) + 20)
|
|
.reduce((a, v) => a + v);
|
|
return this._fetch(true);
|
|
}
|
|
|
|
calculate_token_length(content: string): number {
|
|
return calculate_token_length(content);
|
|
}
|
|
|
|
user(...messages: string[]) {
|
|
for (const msg of messages) {
|
|
this.messages.push({ role: "user", content: msg });
|
|
this.total_tokens += this.calculate_token_length(msg);
|
|
this.forgetSomeMessages();
|
|
}
|
|
}
|
|
|
|
assistant(...messages: string[]) {
|
|
for (const msg of messages) {
|
|
this.messages.push({ role: "assistant", content: msg });
|
|
this.total_tokens += this.calculate_token_length(msg);
|
|
this.forgetSomeMessages();
|
|
}
|
|
}
|
|
|
|
forgetSomeMessages() {
|
|
// forget occur condition
|
|
if (this.total_tokens + this.tokens_margin >= this.max_tokens) {
|
|
this.forceForgetSomeMessages();
|
|
}
|
|
}
|
|
|
|
forceForgetSomeMessages() {
|
|
this.messages = [
|
|
...this.messages.slice(Math.max(~~(this.messages.length / 4), 2)),
|
|
];
|
|
}
|
|
|
|
forgetAllMessage() {
|
|
this.messages = [];
|
|
}
|
|
|
|
stats(): string {
|
|
return (
|
|
`total_tokens: ${this.total_tokens}` +
|
|
"\n" +
|
|
`max_tokens: ${this.max_tokens}` +
|
|
"\n" +
|
|
`tokens_margin: ${this.tokens_margin}` +
|
|
"\n" +
|
|
`messages.length: ${this.messages.length}`
|
|
);
|
|
}
|
|
}
|
|
|
|
export default Chat;
|