Add function handling for OpenAI model
for now it's querying only time, but in the future there will be more commands
This commit is contained in:
parent
bebef021fb
commit
0df05e2f06
4 changed files with 169 additions and 33 deletions
|
@ -8,12 +8,15 @@ datasource db {
|
|||
}
|
||||
|
||||
model Usage {
|
||||
timestamp DateTime @id
|
||||
@@id([timestamp, functionRan])
|
||||
timestamp DateTime
|
||||
user BigInt
|
||||
channel BigInt
|
||||
guild BigInt?
|
||||
usageReguest Int
|
||||
usageResponse Int
|
||||
functionName String?
|
||||
functionRan Int @default(0)
|
||||
}
|
||||
|
||||
model Limits {
|
||||
|
|
|
@ -2,17 +2,17 @@ import { ChatCompletionRequestMessage as OpenAIMessage , CreateChatCompletionReq
|
|||
|
||||
// Don't forget to rename the file to config.ts
|
||||
|
||||
const calendarConfig: Intl.DateTimeFormatOptions = {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
export default class config {
|
||||
static readonly calendarConfig: Intl.DateTimeFormatOptions = {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
/** Tokens to authenticate with */
|
||||
static readonly tokens = {
|
||||
Discord: "Discord token here",
|
||||
|
@ -22,7 +22,7 @@ export default class config {
|
|||
/** Messages to append at the start of every chat when sending to API */
|
||||
static systemPrompt(): OpenAIMessage[] {
|
||||
return [
|
||||
{ role: "system", content: `You are GPTcord, an AI built on top of ChatGPT (a large language model trained by OpenAI) for Discord. Answer as concisely as possible. Current time (${Intl.DateTimeFormat().resolvedOptions().timeZone}): ${new Date().toLocaleString("en-US", calendarConfig)}` }
|
||||
{ role: "system", content: `You are GPTcord, an AI built on top of ChatGPT (a large language model trained by OpenAI) for Discord. Answer as concisely as possible.` }
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { database, openai } from "./index";
|
|||
import Moderation from "./moderation";
|
||||
import config from "./config";
|
||||
import toOpenAIMessages from "./toOpenAIMessages";
|
||||
import FunctionManager from "./funcitonManager";
|
||||
|
||||
type NonNullableInObject<T, V> = { [k in keyof T]: k extends V ? NonNullable<T[k]> : T[k] };
|
||||
type apiRequest = DiscordApi.Message | DiscordApi.RepliableInteraction;
|
||||
|
@ -12,6 +13,9 @@ export type request = apiRequest & NonNullableInObject<apiRequest, "channel" | "
|
|||
/** Stores the queue requests on the channels. */
|
||||
const channelsRunning: DiscordApi.Collection<string, request[]> = new DiscordApi.Collection();
|
||||
|
||||
type ChannelQueue = NonNullable<ReturnType<typeof channelsRunning.get>>;
|
||||
type RequestMessage = NonNullable<ReturnType<ChannelQueue["at"]>>;
|
||||
|
||||
/**
|
||||
* Gets the user that requested the execution
|
||||
* @param request The request to get the user from
|
||||
|
@ -158,13 +162,49 @@ export async function queueRequest(request: apiRequest) {
|
|||
executeFromQueue(request.channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs used tokens to the terminal and to the database
|
||||
* @param answer the response that OpenAI returned
|
||||
* @param message the message that initiated the execution
|
||||
* @param functionRan counter of how many function have been ran
|
||||
*/
|
||||
function logUsedTokens(
|
||||
answer: Awaited<ReturnType<typeof openai.createChatCompletion>>,
|
||||
message: RequestMessage,
|
||||
functionRan: number,
|
||||
) {
|
||||
const usage = answer.data.usage;
|
||||
const functionName = answer.data.choices[0].message?.function_call?.name;
|
||||
if (usage != undefined) {
|
||||
const channelName: string = !message.channel.isDMBased() ? `${message.channel.name} (${message.guild?.name})` : `@${getAuthor(message).tag}`;
|
||||
console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in #${channelName}${functionName ? " [Function: " + functionName + "]" : ""}`);
|
||||
|
||||
database.usage.create({
|
||||
data: {
|
||||
timestamp: message.createdAt,
|
||||
user: BigInt(getAuthor(message).id),
|
||||
channel: BigInt(message.channelId),
|
||||
guild: message.guildId ? BigInt(message.guildId) : null,
|
||||
usageReguest: usage.prompt_tokens,
|
||||
usageResponse: usage.completion_tokens,
|
||||
functionName: functionName ?? null,
|
||||
functionRan: functionName ? functionRan : 0,
|
||||
}
|
||||
}).catch((e => {
|
||||
console.error("Failed to push to a database");
|
||||
console.error(e);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the queue for the channel
|
||||
* @param channel the channel to run the queue for
|
||||
*/
|
||||
async function executeFromQueue(channel: string) {
|
||||
const channelQueue = channelsRunning.get(channel) as NonNullable<ReturnType<typeof channelsRunning.get>>;
|
||||
const message = channelQueue.at(0) as NonNullable<ReturnType<typeof channelQueue.at>>;
|
||||
const channelQueue = channelsRunning.get(channel) as ChannelQueue;
|
||||
const message = channelQueue.at(0) as RequestMessage;
|
||||
let functionRanCounter = 0;
|
||||
|
||||
try {
|
||||
let messages: DiscordApi.Collection<string, DiscordApi.Message> = await message.channel.messages.fetch({ limit: config.limits.messages, cache: false });
|
||||
|
@ -180,29 +220,39 @@ async function executeFromQueue(channel: string) {
|
|||
message.deferReply();
|
||||
}
|
||||
|
||||
const answer = await openai.createChatCompletion({
|
||||
const OpenAImessages = toOpenAIMessages(messages);
|
||||
let answer = await openai.createChatCompletion({
|
||||
...config.chatCompletionConfig,
|
||||
messages: toOpenAIMessages(messages),
|
||||
messages: OpenAImessages,
|
||||
// FIXME: don't use new instance of FunctionManager
|
||||
functions: new FunctionManager().getFunctions(),
|
||||
});
|
||||
|
||||
const usage = answer.data.usage;
|
||||
if (usage != undefined) {
|
||||
const channelName: string = !message.channel.isDMBased() ? `${message.channel.name} (${message.guild?.name})` : `@${getAuthor(message).tag}`;
|
||||
console.log(`Used ${usage.total_tokens} (${usage.prompt_tokens} + ${usage.completion_tokens}) tokens for ${getAuthor(message).tag} (${getAuthor(message).id}) in #${channelName}`);
|
||||
logUsedTokens(answer, message, ++functionRanCounter);
|
||||
|
||||
database.usage.create({
|
||||
data: {
|
||||
timestamp: message.createdAt,
|
||||
user: BigInt(getAuthor(message).id),
|
||||
channel: BigInt(message.channelId),
|
||||
guild: message.guildId ? BigInt(message.guildId) : null,
|
||||
usageReguest: usage.prompt_tokens,
|
||||
usageResponse: usage.completion_tokens
|
||||
}
|
||||
}).catch((e => {
|
||||
console.error("Failed to push to a database");
|
||||
console.error(e);
|
||||
}));
|
||||
let generatedMessage = answer.data.choices[0].message;
|
||||
if (!generatedMessage) throw new Error("empty message received");
|
||||
|
||||
// handle function calls
|
||||
while (generatedMessage.function_call) {
|
||||
OpenAImessages.push(generatedMessage);
|
||||
OpenAImessages.push({
|
||||
role: "function",
|
||||
name: generatedMessage.function_call.name,
|
||||
// FIXME: don't use new instance of FunctionManager
|
||||
content: new FunctionManager().handleFunction(generatedMessage.function_call),
|
||||
});
|
||||
answer = await openai.createChatCompletion({
|
||||
...config.chatCompletionConfig,
|
||||
messages: OpenAImessages,
|
||||
// FIXME: don't use new instance of FunctionManager
|
||||
functions: new FunctionManager().getFunctions(),
|
||||
});
|
||||
|
||||
logUsedTokens(answer, message, ++functionRanCounter);
|
||||
|
||||
generatedMessage = answer.data.choices[0].message;
|
||||
if (!generatedMessage) throw new Error("empty message received");
|
||||
}
|
||||
|
||||
const answerContent = answer.data.choices[0].message?.content;
|
||||
|
|
83
src/funcitonManager.ts
Normal file
83
src/funcitonManager.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { ChatCompletionFunctions, ChatCompletionRequestMessageFunctionCall } from "openai";
|
||||
import config from "./config";
|
||||
|
||||
type parameterMap = {
|
||||
string: string,
|
||||
number: number,
|
||||
};
|
||||
|
||||
type nameTypeMap = {[name: string]: keyof parameterMap} | Record<string, never>;
|
||||
|
||||
type OpenAIFunctionRequestData<T extends nameTypeMap> = {
|
||||
[name in keyof T]: T[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the function that can be ran by the OpenAI model
|
||||
*/
|
||||
export interface OpenAIFunction<T extends nameTypeMap = nameTypeMap> {
|
||||
name: string,
|
||||
description?: string,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: T extends Record<string, never> ? Record<string, never> : {
|
||||
[name in T[string]]: {
|
||||
type: T[name],
|
||||
description?: string,
|
||||
}
|
||||
},
|
||||
required?: Array<keyof T>,
|
||||
},
|
||||
}
|
||||
|
||||
export abstract class OpenAIFunction<T extends nameTypeMap = nameTypeMap> {
|
||||
getSettings(): ChatCompletionFunctions {
|
||||
return {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
parameters: this.parameters,
|
||||
};
|
||||
}
|
||||
|
||||
abstract execute(data: OpenAIFunctionRequestData<T>): string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Manages functions for the OpenAI
|
||||
**/
|
||||
export default class FunctionManager {
|
||||
store = new Map<string, OpenAIFunction>();
|
||||
|
||||
constructor() {
|
||||
this.store.set("getTime", new GetTime());
|
||||
}
|
||||
|
||||
public getFunctions(): ChatCompletionFunctions[] {
|
||||
const rvalue: ChatCompletionFunctions[] = [];
|
||||
for (const [, value] of this.store) {
|
||||
rvalue.push(value.getSettings());
|
||||
}
|
||||
return rvalue;
|
||||
}
|
||||
|
||||
public handleFunction(request: ChatCompletionRequestMessageFunctionCall) {
|
||||
|
||||
const parsedArguments = JSON.parse(request.arguments ?? "");
|
||||
return this.store.get(request.name ?? "")?.execute(parsedArguments);
|
||||
}
|
||||
}
|
||||
|
||||
// buildins
|
||||
|
||||
class GetTime extends OpenAIFunction<Record<string, never>> {
|
||||
name = "getTime";
|
||||
description = "Gets current date and time with a timezone attached";
|
||||
parameters = {
|
||||
type: "object" as const,
|
||||
properties: {} as Record<string, never>,
|
||||
};
|
||||
|
||||
execute(): string {
|
||||
return `${Intl.DateTimeFormat().resolvedOptions().timeZone}): ${new Date().toLocaleString("en-US", config.calendarConfig)}`;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue