diff --git a/.vscode/settings.json b/.vscode/settings.json index 70cdadcfd..868e31f39 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,6 +19,7 @@ "Benzos", "BESTOF", "Blurple", + "Blurryturtle", "BOTERRORS", "BOTLOG", "Botspam", @@ -27,6 +28,7 @@ "CLEARMIND", "CLOSEDTRIPSIT", "COLLABVC", + "combochart", "COMPSCI", "DALL", "DBAPI", @@ -39,15 +41,21 @@ "DISSONAUT", "DMMEFORHELP", "DONATIONTRIGGER", + "downvotes", "DRUGQUESTIONS", "EROWID", + "Factsheets", "GLITCHTIP", "GOLDLOUNGE", "GROUPCOLLAB", "HARMREDUCTIONCENTRE", "HELPDESK", + "Hipperooni", + "Hisui", + "Hobbiton", "HOMESERVER", "HRRESOURCES", + "imagen", "IMDB", "IMGUR", "INTANNOUNCE", @@ -65,6 +73,7 @@ "MATRIXADMIN", "MDMA", "MINECRAFTADMIN", + "moderations", "MODHAVEN", "modlog", "moodle", @@ -74,13 +83,16 @@ "NOSONAR", "openai", "OPENTRIPSIT", + "peronsas", "PSYCHONAUT", "REALTALK", "ROLLBAR", + "Rooni", "RTRIPSIT", "Sheesh", "SLEEPYRADIO", "SOCIALMEDIA", + "sonarjs", "spreadshirt", "spreadshop", "STIMMING", @@ -90,6 +102,9 @@ "TEAMTRIPSIT", "Techhelp", "TEMPVOICE", + "Thesarahyouknow", + "thumbdown", + "thumbup", "TICKETBOOTH", "tripbot", "TRIPBOTDEV", @@ -107,6 +122,8 @@ "TRIVIABIGBRAIN", "tryptamines", "UNDERBAN", + "upvotes", + "Utaninja", "VIPLOUNGE", "VIPWELCOME", "VOICECHATTY", diff --git a/src/discord/commands/global/d.ai.ts b/src/discord/commands/global/d.ai.ts index 2b6896959..0c21d9070 100644 --- a/src/discord/commands/global/d.ai.ts +++ b/src/discord/commands/global/d.ai.ts @@ -225,8 +225,8 @@ function getComponentById( // If no component is found, it will return null // This is useful for finding the button that was clicked, or select menu that was used - log.debug(F, `getComponentById started with id: ${id}`); - log.debug(F, `Components: ${JSON.stringify(interaction.message.components, null, 2)}`); + // log.debug(F, `getComponentById started with id: ${id}`); + // log.debug(F, `Components: ${JSON.stringify(interaction.message.components, null, 2)}`); if (interaction.message?.components) { // eslint-disable-next-line no-restricted-syntax @@ -630,7 +630,7 @@ async function aiAudit( async function deletedPage( interaction: ButtonInteraction, ):Promise { - log.debug(F, `Customid: ${interaction.customId}`); + log.debug(F, `CustomId: ${interaction.customId}`); const menuButtons = new ActionRowBuilder() .addComponents( buttonAiHelp.setStyle(ButtonStyle.Primary), @@ -1049,9 +1049,7 @@ async function personasPage( ? await db.ai_personas.findMany() : await db.ai_personas.findMany({ where: { - OR: [ - { name: 'tripbot' }, - ], + public: true, }, }); @@ -1067,7 +1065,7 @@ async function personasPage( value: persona.name, } as SelectMenuComponentOptionData)); - // log.debug(F, `aiPersonaOptions: ${JSON.stringify(aiPersonaOptions, null, 2)}`); + log.debug(F, `aiPersonaOptions1: ${JSON.stringify(aiPersonaOptions, null, 2)}`); let selectedPersona = undefined as string | undefined; @@ -1115,7 +1113,7 @@ async function personasPage( } } - log.debug(F, `aiPersonaOptions: ${JSON.stringify(aiPersonaOptions, null, 2)}`); + log.debug(F, `aiPersonaOptions2: ${JSON.stringify(aiPersonaOptions, null, 2)}`); // If there's only one persona in the list, set it as the default // if (aiPersonaList.length === 1) { @@ -1141,9 +1139,9 @@ async function personasPage( ]; - log.debug(F, `There are ${components.length} components in the array`); + // log.debug(F, `There are ${components.length} components in the array`); - if (selectedPersona) { + if (selectedPersona !== 'none' && selectedPersona !== undefined) { const persona = await db.ai_personas.findFirstOrThrow({ where: { name: selectedPersona, @@ -1206,8 +1204,8 @@ async function personasPage( ); } - log.debug(F, `Final components: ${JSON.stringify(components, null, 2)}`); - log.debug(F, `components 2 0 : ${JSON.stringify(components[2], null, 2)}`); + // log.debug(F, `Final components: ${JSON.stringify(components, null, 2)}`); + // log.debug(F, `components 2 0 : ${JSON.stringify(components[2], null, 2)}`); return { embeds: [embedTemplate() .setTitle('Persona Information') @@ -1262,10 +1260,9 @@ async function setupPage( const aiPersonaList = interaction.guild?.id === env.DISCORD_GUILD_ID ? await db.ai_personas.findMany() : await db.ai_personas.findMany({ - where: { - OR: [ - { name: 'tripbot' }, - ], + where: + { + public: true, }, }); @@ -2178,13 +2175,13 @@ export async function aiMessage( }, 30000); // Failsafe to stop typing indicator after 30 seconds try { - const chatResponse = await aiChat(aiPersona, messageList, messageData.author.id, attachmentInfo); + const chatResponse = await aiChat(aiPersona, messageList, messageData, attachmentInfo); response = chatResponse.response; promptTokens = chatResponse.promptTokens; completionTokens = chatResponse.completionTokens; } finally { clearInterval(typingInterval); // Stop sending typing indicator - clearTimeout(typingFailsafe); // Clear the failsafe timeout to prevent it from running if we've successfully stopped typing + clearTimeout(typingFailsafe); // Clear the failsafe timeout to prevent it from running if we've successfully stopped typing } log.debug(F, `response from API: ${response}`); @@ -2240,6 +2237,8 @@ export async function aiMessage( components: [], allowedMentions: { parse: [] }, }); + } else if (response === 'functionFinished') { + log.debug(F, 'Function finished, returning'); } else { // await messageData.channel.sendTyping(); // const wpm = 120; diff --git a/src/discord/commands/global/d.drug.ts b/src/discord/commands/global/d.drug.ts index cb4a2f5da..3c8cefa35 100644 --- a/src/discord/commands/global/d.drug.ts +++ b/src/discord/commands/global/d.drug.ts @@ -2,6 +2,7 @@ import { SlashCommandBuilder, Colors, EmbedBuilder, + MessageReplyOptions, } from 'discord.js'; import { stripIndents } from 'common-tags'; import { SlashCommand } from '../../@types/commandDef'; @@ -274,109 +275,130 @@ async function addDosages( // return embed; // } -export const dDrug: SlashCommand = { - data: new SlashCommandBuilder() - .setName('drug') - .setDescription('Check substance information') - .addStringOption(option => option.setName('substance') - .setDescription('Pick a substance!') - .setRequired(true) - .setAutocomplete(true)) - .addStringOption(option => option.setName('section') - .setDescription('What section of the info to respond with? (Defaults to all)') - .addChoices( - { name: 'All', value: 'all' }, - { name: 'Dosage', value: 'dosage' }, - { name: 'Summary', value: 'summary' }, - )) - .addBooleanOption(option => option.setName('ephemeral') - .setDescription('Set to "True" to show the response only to you')), - async execute(interaction) { - log.info(F, await commandContext(interaction)); - await interaction.deferReply({ ephemeral: (interaction.options.getBoolean('ephemeral') !== false) }); - let embed = embedTemplate(); - // Check if the interaction is coming from DM - - // log.debug(F, `ephemeral: ${ephemeral} | interaction.channelId: ${interaction.channelId}`); - // if (interaction.channelId !== null && !ephemeral) { - // embed.setFooter({ text: 'You can use this command in DM for privacy if you want!' }); - // } - // log.debug(F, `ephemeral: ${ephemeral}`); - const drugName = interaction.options.getString('substance', true); - // if (!drugName) { - // embed.setTitle('No drug name was provided'); - // await interaction.editReply({ embeds: [embed] }); - // return false; - // } - const drugData = await drug(drugName); - - if (!drugData) { - embed.setTitle(`${drugName} was not found`); - embed.setDescription(stripIndents`...this shouldn\'t have happened, please tell the developer!`); - // If this happens then something went wrong with the auto-complete - await interaction.editReply({ embeds: [embed] }); - return false; - } - // log.debug(F, `drugData: ${JSON.stringify(drugData, null, 2)}`); - - // if (drugData === null) { - // embed.setTitle(`${drugName} was not found`); - // embed.setDescription(stripIndents`...this shouldn\'t have happened, please tell the developer!`); - // // If this happens then something went wrong with the auto-complete - // await interaction.editReply({ embeds: [embed] }); - // return false; - // } - - const section = interaction.options.getString('section'); +export async function getDrugInfo( + drugName: string, + section?: 'all' | 'dosage' | 'summary', +):Promise { + let embed = embedTemplate(); + log.debug(F, `drugName: ${drugName} | section: ${section}`); + + // if (!drugName) { + // embed.setTitle('No drug name was provided'); + // await interaction.editReply({ embeds: [embed] }); + // return false; + // } + const drugData = await drug(drugName); + + if (!drugData) { + embed.setTitle(`${drugName} was not found`); + embed.setDescription(stripIndents`...this shouldn\'t have happened, please tell the developer!`); + // If this happens then something went wrong with the auto-complete + return { embeds: [embed] }; + } + // log.debug(F, `drugData: ${JSON.stringify(drugData, null, 2)}`); + + // if (drugData === null) { + // embed.setTitle(`${drugName} was not found`); + // embed.setDescription(stripIndents`...this shouldn\'t have happened, please tell the developer!`); + // // If this happens then something went wrong with the auto-complete + // await interaction.editReply({ embeds: [embed] }); + // return false; + // } + + // log.debug(F, `section: ${section} | drugName: ${drugName} | drugData: ${JSON.stringify(drugData, null, 2)}`); + + embed.setColor(Colors.Purple); + embed.setTitle(`🌐 ${drugData.name} Information`); + // embed.setURL(`https://wiki.tripsit.me/wiki/${drugName.replaceAll(' ', '_')}`); + embed.setURL(drugData.url); + + if (section === 'dosage') { + embed = await addDosages(embed, drugData); + return { embeds: [embed] }; + } - // log.debug(F, `section: ${section} | drugName: ${drugName} | drugData: ${JSON.stringify(drugData, null, 2)}`); + embed = await addSummary(embed, drugData); - embed.setColor(Colors.Purple); - embed.setTitle(`🌐 ${drugData.name} Information`); - // embed.setURL(`https://wiki.tripsit.me/wiki/${drugName.replaceAll(' ', '_')}`); - embed.setURL(drugData.url); + if (section === 'summary') { + return { embeds: [embed] }; + } - if (section === 'dosage') { - embed = await addDosages(embed, drugData); - await interaction.editReply({ embeds: [embed] }); - return true; - } + embed = await addAliases(embed, drugData); + embed = await addInteractions(embed, drugData); - embed = await addSummary(embed, drugData); + let embedRowColumns = 0; - log.debug(F, `Embed: ${JSON.stringify(embed, null, 2)}`); + // CLASS + if (drugData.classes) { + embed = await addClasses(embed, drugData); + embedRowColumns += 1; + } - if (section === 'summary') { - await interaction.editReply({ embeds: [embed] }); - return true; - } + // CROSS TOLERANCE + if (drugData.crossTolerances && drugData.crossTolerances.length >= 1) { + embed = await addCrossTolerance(embed, drugData); + embedRowColumns += 1; + } - embed = await addAliases(embed, drugData); - embed = await addInteractions(embed, drugData); + // ADDICTION POTENTIAL + if (drugData.addictionPotential) { + embed = await addAddictions(embed, drugData); + embedRowColumns += 1; + } - let embedRowColumns = 0; + // Make sure that each column has three rows to utilize space + let toleranceAdded = false; + let toxicityAdded = false; + + [ + embed, + toleranceAdded, + toxicityAdded, + ] = await fillInColumns( + embed, + drugData, + embedRowColumns, + toleranceAdded, + toxicityAdded, + ); + + // Dosage + if (drugData.roas) { + // Get a list of drug ROA names + const roaNames = drugData.roas.map(roa => roa.name); - // CLASS - if (drugData.classes) { - embed = await addClasses(embed, drugData); - embedRowColumns += 1; + // For HR reasons we prefer non-invasive methods + if (roaNames.includes('Insufflated')) { + roaNames.splice(roaNames.indexOf('Insufflated'), 1); + roaNames.unshift('Insufflated'); } - // CROSS TOLERANCE - if (drugData.crossTolerances && drugData.crossTolerances.length >= 1) { - embed = await addCrossTolerance(embed, drugData); - embedRowColumns += 1; + if (roaNames.includes('Vapourised')) { + roaNames.splice(roaNames.indexOf('Vapourised'), 1); + roaNames.unshift('Vapourised'); } - - // ADDICTION POTENTIAL - if (drugData.addictionPotential) { - embed = await addAddictions(embed, drugData); - embedRowColumns += 1; + if (roaNames.includes('Smoked')) { + roaNames.splice(roaNames.indexOf('Smoked'), 1); + roaNames.unshift('Smoked'); } - // Make sure that each column has three rows to utilize space - let toleranceAdded = false; - let toxicityAdded = false; + // For each roaName, get the dosage and duration + // log.debug(F, `roaNames: ${roaNames}`); + + embedRowColumns = 0; + roaNames.forEach(roaName => { + if (embedRowColumns < 3) { + const roaInfo = (drugData.roas as RoaType[]).find((r:RoaType) => r.name === roaName) as RoaType; + if (roaInfo.dosage) { + let dosageString = ''; + roaInfo.dosage.forEach(d => { + dosageString += `${d.name}: ${d.value}\n`; + }); + embed.addFields({ name: `💊 Dosage (${roaName})`, value: stripIndents`${dosageString}`, inline: true }); + embedRowColumns += 1; + } + } + }); [ embed, @@ -390,90 +412,66 @@ export const dDrug: SlashCommand = { toxicityAdded, ); - // Dosage - if (drugData.roas) { - // Get a list of drug ROA names - const roaNames = drugData.roas.map(roa => roa.name); - - // For HR reasons we prefer non-invasive methods - if (roaNames.includes('Insufflated')) { - roaNames.splice(roaNames.indexOf('Insufflated'), 1); - roaNames.unshift('Insufflated'); - } + // DURATION + [embed, embedRowColumns] = await addDurations(embed, drugData, roaNames); - if (roaNames.includes('Vapourised')) { - roaNames.splice(roaNames.indexOf('Vapourised'), 1); - roaNames.unshift('Vapourised'); - } - if (roaNames.includes('Smoked')) { - roaNames.splice(roaNames.indexOf('Smoked'), 1); - roaNames.unshift('Smoked'); - } + [ + embed, + toleranceAdded, + toxicityAdded, + ] = await fillInColumns( + embed, + drugData, + embedRowColumns, + toleranceAdded, + toxicityAdded, + ); + } - // For each roaName, get the dosage and duration - // log.debug(F, `roaNames: ${roaNames}`); - - embedRowColumns = 0; - roaNames.forEach(roaName => { - if (embedRowColumns < 3) { - const roaInfo = (drugData.roas as RoaType[]).find((r:RoaType) => r.name === roaName) as RoaType; - if (roaInfo.dosage) { - let dosageString = ''; - roaInfo.dosage.forEach(d => { - dosageString += `${d.name}: ${d.value}\n`; - }); - embed.addFields({ name: `💊 Dosage (${roaName})`, value: stripIndents`${dosageString}`, inline: true }); - embedRowColumns += 1; - } - } - }); - - [ - embed, - toleranceAdded, - toxicityAdded, - ] = await fillInColumns( - embed, - drugData, - embedRowColumns, - toleranceAdded, - toxicityAdded, - ); - - // DURATION - [embed, embedRowColumns] = await addDurations(embed, drugData, roaNames); - - [ - embed, - toleranceAdded, - toxicityAdded, - ] = await fillInColumns( - embed, - drugData, - embedRowColumns, - toleranceAdded, - toxicityAdded, - ); - } + // Reagents + await addReagents(embed, drugData); - // Reagents - await addReagents(embed, drugData); + // Tolerance + if (!toleranceAdded) { + await addTolerances(embed, drugData); + } - // Tolerance - if (!toleranceAdded) { - await addTolerances(embed, drugData); - } + // Toxicity + if (!toxicityAdded) { + await addToxicities(embed, drugData); + } - // Toxicity - if (!toxicityAdded) { - await addToxicities(embed, drugData); - } + // Experiences + await addExperiences(embed, drugData); - // Experiences - await addExperiences(embed, drugData); + // log.debug(F, `Embed: ${JSON.stringify(embed, null, 2)}`); - await interaction.editReply({ embeds: [embed] }); + return { embeds: [embed] }; +} +export const dDrug: SlashCommand = { + data: new SlashCommandBuilder() + .setName('drug') + .setDescription('Check substance information') + .addStringOption(option => option.setName('substance') + .setDescription('Pick a substance!') + .setRequired(true) + .setAutocomplete(true)) + .addStringOption(option => option.setName('section') + .setDescription('What section of the info to respond with? (Defaults to all)') + .addChoices( + { name: 'All', value: 'all' }, + { name: 'Dosage', value: 'dosage' }, + { name: 'Summary', value: 'summary' }, + )) + .addBooleanOption(option => option.setName('ephemeral') + .setDescription('Set to "True" to show the response only to you')), + async execute(interaction) { + log.info(F, await commandContext(interaction)); + await interaction.deferReply({ ephemeral: (interaction.options.getBoolean('ephemeral') !== false) }); + const section = interaction.options.getString('section') as 'all' | 'dosage' | 'summary' | undefined; + const drugName = interaction.options.getString('substance', true); + await interaction.editReply(await getDrugInfo(drugName, section)); return true; }, }; diff --git a/src/discord/utils/trust.ts b/src/discord/utils/trust.ts index d8281b182..864c8e230 100644 --- a/src/discord/utils/trust.ts +++ b/src/discord/utils/trust.ts @@ -53,18 +53,28 @@ export async function addedVerified( update: {}, }); + const userData = await db.users.upsert({ + where: { + discord_id: newMember.id, + }, + create: { + discord_id: newMember.id, + }, + update: {}, + }); + let memberData = {} as members; try { memberData = await db.members.upsert({ where: { id_guild_id: { - guild_id: newMember.guild.id, - id: newMember.id, + guild_id: guildData.id, + id: userData.discord_id as string, }, }, create: { - guild_id: newMember.guild.id, - id: newMember.id, + guild_id: guildData.id, + id: userData.discord_id as string, }, update: {}, }); diff --git a/src/global/commands/g.ai.ts b/src/global/commands/g.ai.ts index 7475ddeda..f7bba5427 100644 --- a/src/global/commands/g.ai.ts +++ b/src/global/commands/g.ai.ts @@ -5,21 +5,16 @@ import OpenAI from 'openai'; import { ai_personas } from '@prisma/client'; import { ImagesResponse, ModerationCreateResponse } from 'openai/resources'; import { Assistant } from 'openai/resources/beta/assistants/assistants'; -import { stripIndents } from 'common-tags'; -import { Thread, ThreadDeleted } from 'openai/resources/beta/threads/threads'; +import { ThreadDeleted } from 'openai/resources/beta/threads/threads'; +import { MessageContentText } from 'openai/resources/beta/threads/messages/messages'; import { - MessageCreateParams, MessageListParams, ThreadMessage, ThreadMessagesPage, - MessageContentText, -} from 'openai/resources/beta/threads/messages/messages'; -import { Run, RunCreateParams } from 'openai/resources/beta/threads/runs/runs'; -import { - GoogleGenerativeAI, HarmCategory, HarmBlockThreshold, GenerationConfig, SafetySetting, Part, InputContent, GenerateContentResult, + GoogleGenerativeAI, HarmCategory, HarmBlockThreshold, GenerationConfig, SafetySetting, Part, InputContent, + GenerateContentResult, } from '@google/generative-ai'; import axios from 'axios'; -import { - ChannelType, Events, Message, TextChannel, -} from 'discord.js'; +import { Message, MessageReplyOptions, TextChannel } from 'discord.js'; import { sleep } from '../../discord/commands/guild/d.bottest'; +import { getDrugInfo } from '../../discord/commands/global/d.drug'; const F = f(__filename); @@ -84,26 +79,60 @@ Any role with 'TS' lettering is an official TripSit team member role. Patreon subscribers can use the /imagen command to generate images. `; -// # Example dummy function hard coded to return the same weather -// # In production, this could be your backend API or an external API -// eslint-disable-next-line @typescript-eslint/no-unused-vars -// async function getCurrentWeather(location:string, unit = 'fahrenheit') { -// return { -// location, -// temperature: '72', -// unit, -// forecast: ['sunny', 'windy'], -// }; -// } +const availableFunctions = { + getDrugInfo, +}; + +const aiFunctions = [ + { + type: 'function', + function: { + name: 'getDrugInfo', + description: 'Get information on a drug or substance, such as dosages or summary', + parameters: { + type: 'object', + properties: { + drugName: { type: 'string', description: 'The name of the substance to look up' }, + section: { type: 'string', description: 'The section to return' }, + }, + required: ['drugName'], + }, + }, + }, +] as Assistant.Function[]; + +export async function aiModerateReport( + message: string, +):Promise { + // log.debug(F, `message: ${message}`); + + // log.debug(F, `results: ${JSON.stringify(results, null, 2)}`); + + if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return {} as ModerationCreateResponse; + return openAi.moderations + .create({ + input: message, + }) + .catch(err => { + if (err instanceof OpenAI.APIError) { + log.error(F, `${err.status}`); // 400 + log.error(F, `${err.name}`); // BadRequestError + log.error(F, `${err.headers}`); // {server: 'nginx', ...} + } else { + throw err; + } + return {} as ModerationCreateResponse; + }); +} export async function getAssistant(name: string):Promise { // Get all the org's assistants const myAssistants = await openAi.beta.assistants.list(); - log.debug(F, `myAssistants: ${myAssistants.data.map(assistant => assistant.name).join(', ')}`); + // log.debug(F, `myAssistants: ${myAssistants.data.map(assistant => assistant.name).join(', ')}`); // Check if the assistant exists const assistantData = myAssistants.data.find(assistant => assistant.name === name); - log.debug(F, `I found the ${name} assistant!`); + // log.debug(F, `assistantData: ${JSON.stringify(assistantData, null, 2)}`); const personaData = await db.ai_personas.findFirstOrThrow({ where: { @@ -113,6 +142,12 @@ export async function getAssistant(name: string):Promise { const modelName = personaData.ai_model.toLowerCase() === 'gpt_3_5_turbo' ? 'gpt-3.5-turbo-1106' : 'gpt-4-turbo-preview'; + // Upload a file with an "assistants" purpose + // const combinedDb = await openAi.files.create({ + // file: fs.createReadStream('../../../assets/data/combinedDb.json') , + // purpose: 'assistants', + // }); + const tripsitAssistantData = { // eslint-disable-next-line sonarjs/no-duplicate-string model: modelName, @@ -122,6 +157,7 @@ export async function getAssistant(name: string):Promise { tools: [ { type: 'code_interpreter' }, { type: 'retrieval' }, + ...aiFunctions, ], file_ids: [], metadata: {}, @@ -134,6 +170,7 @@ export async function getAssistant(name: string):Promise { log.debug(F, `Creating the ${name} assistant!`); return openAi.beta.assistants.create(tripsitAssistantData); } + log.debug(F, `I found the ${name} assistant!`); // If it does exist, update it // log.debug(F, `updatedAssistant: ${JSON.stringify(assistant, null, 2)}`); return openAi.beta.assistants.update(assistantData.id, tripsitAssistantData); @@ -145,25 +182,6 @@ export async function deleteThread(threadId: string):Promise { return threadData; } -export async function getMessages( - inputThreadData: Thread, - options: MessageListParams, -):Promise { - // log.debug(F, `threadMessages: ${JSON.stringify(threadMessages, null, 2)}`); - return openAi.beta.threads.messages.list( - inputThreadData.id, - options, - ); -} - -export async function readRun( - thread: Thread, - run: Run, -):Promise { - // log.debug(F, `runData: ${JSON.stringify(runData, null, 2)}`); - return openAi.beta.threads.runs.retrieve(thread.id, run.id); -} - export async function createImage( prompt: string, userId: string, @@ -194,70 +212,13 @@ export async function createImage( }); } -export async function aiModerateReport( - message: string, -):Promise { - // log.debug(F, `message: ${message}`); - - // log.debug(F, `results: ${JSON.stringify(results, null, 2)}`); - - if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return {} as ModerationCreateResponse; - - return openAi.moderations - .create({ - input: message, - }) - .catch(err => { - if (err instanceof OpenAI.APIError) { - log.error(F, `${err.status}`); // 400 - log.error(F, `${err.name}`); // BadRequestError - log.error(F, `${err.headers}`); // {server: 'nginx', ...} - } else { - throw err; - } - return {} as ModerationCreateResponse; - }); -} - -// const aiFunctions = [ -// { -// name: 'getCurrentWeather', -// description: 'Get the current weather in a given location', -// parameters: { -// type: 'object', -// properties: { -// location: { -// type: 'string', -// description: 'The city and state, e.g. San Francisco, CA', -// }, -// unit: { type: 'string', enum: ['celsius', 'fahrenheit'] }, -// }, -// required: ['location'], -// }, -// }, -// { -// name: 'aiModerateReport', -// description: 'Get a report on how the AI rates a message', -// parameters: { -// type: 'object', -// properties: { -// message: { -// type: 'string', -// description: 'The message you want the AI to analyze', -// }, -// }, -// required: ['message'], -// }, -// }, -// ]; - async function googleAiConversation( aiPersona:ai_personas, messages: { role: 'user'; content: string; }[], - user: string, + messageData: Message, attachmentInfo: { url: string | null; mimeType: string | null; @@ -319,7 +280,7 @@ async function googleAiConversation( ] as Part[]; if (modelName === 'gemini-pro-vision' && attachmentInfo.url && attachmentInfo.mimeType) { - // We dont want to include the prompt and objective trusts on a conversation because it's already supplied below + // We don't want to include the prompt and objective trusts on a conversation because it's already supplied below parts.unshift({ text: aiPersona.prompt }); parts.unshift({ text: objectiveTruths }); @@ -362,8 +323,8 @@ async function googleAiConversation( // Get the user's history const userData = await db.users.upsert({ - where: { discord_id: user }, - create: { discord_id: user }, + where: { discord_id: messageData.author.id }, + create: { discord_id: messageData.author.id }, update: {}, }); let userHistory = [] as InputContent[]; @@ -422,7 +383,7 @@ async function googleAiConversation( // Save the user's history await db.users.update({ - where: { discord_id: user }, + where: { discord_id: messageData.author.id }, data: { ai_history_google: JSON.stringify(userHistory), }, @@ -542,93 +503,229 @@ async function googleAiConversation( // return { response: result.response.text(), promptTokens, completionTokens }; // } +async function openAiWaitForRun( + runData: OpenAI.Beta.Threads.Run, + threadData: OpenAI.Beta.Threads.Thread, + messageData: Message, +):Promise<{ + response: string, + promptTokens: number, + completionTokens: number, + }> { + let run = runData; + const thread = threadData; + + const response = ''; + const promptTokens = 0; + const completionTokens = 0; + + // Wait for the run to complete + while (['queued', 'in_progress'].includes(run.status)) { + // eslint-disable-next-line no-await-in-loop + await sleep(200); + // eslint-disable-next-line no-await-in-loop + run = await openAi.beta.threads.runs.retrieve(thread.id, run.id); + } + + // Depending on how the run ended, do something + switch (run.status) { + case 'completed': { + // This should pull the thread and then get the last message in the thread, which should be from the assistant + // We only want the first response, so we limit it to 1 + const messageContent = ( + await openAi.beta.threads.messages.list(thread.id, { limit: 1 }) + ).data[0].content[0]; + log.debug(F, `messageContent: ${JSON.stringify(messageContent, null, 2)}`); + return { response: (messageContent as MessageContentText).text.value.slice(0, 2000), promptTokens, completionTokens }; + } + case 'requires_action': { + log.debug(F, 'requires_action'); + log.debug(F, `run.required_action: ${JSON.stringify(run.required_action, null, 2)}`); + + // // We need to loop through each of the tool_calls and call the function given + // const toolOutputs = [] as RunSubmitToolOutputsParams.ToolOutput[]; + // run.required_action?.submit_tool_outputs.tool_calls.forEach(async toolCall => { + // const functionName = run.required_action?.submit_tool_outputs.tool_calls[0].function?.name; + // log.debug(F, `functionName: ${functionName}`); + // const functionToCall = availableFunctions[functionName as keyof typeof availableFunctions]; + // const functionArgs = JSON.parse(run.required_action?.submit_tool_outputs.tool_calls[0].function?.arguments as string); + // // Check if functionArgs is correctly structured and contains the expected properties + // if (functionArgs && typeof functionArgs === 'object' && 'drugName' in functionArgs) { + // // Call the function with the spread syntax if it expects multiple arguments + // // If the function expects an object, you can pass functionArgs directly + // const reply = await functionToCall(functionArgs.drugName, functionArgs.section); + // await messageData.reply(reply as MessageReplyOptions); + // response = 'functionFinished'; + // } else { + // // Handle the case where functionArgs does not have the expected structure or types + // log.error(F, `Invalid arguments structure for function ${functionName}: ${JSON.stringify(functionArgs)}`); + // } + // // const functionResponse = await functionToCall(functionArgs); + // // log.debug(F, `functionResponse: ${JSON.stringify(functionResponse, null, 2)}`); + + // // toolOutputs.push({ + // // tool_call_id: toolCall.id, + // // output: `${JSON.stringify(functionResponse)}`, + // // }); + // }); + + // run = await openAi.beta.threads.runs.submitToolOutputs( + // thread.id, + // run.id, + // { + // tool_outputs: toolOutputs, + // }, + // ); + + // return openAiWaitForRun(run, thread); + + const toolCalls = run.required_action?.submit_tool_outputs.tool_calls; + + if (!toolCalls) { + // Handle the case where there are no tool calls + break; + } + + // Map each tool call to a promise representing the async operation + const promises = toolCalls.map(async toolCall => { + const functionName = toolCall.function?.name; + log.debug(F, `functionName: ${functionName}`); + if (!functionName) { + // Handle the case where functionName is not provided + log.error(F, 'Function name is missing'); + return; + } + const functionToCall = availableFunctions[functionName as keyof typeof availableFunctions]; + if (!functionToCall) { + // Handle the case where the function is not found in availableFunctions + log.error(F, `Function ${functionName} not found`); + return; + } + const functionArgs = JSON.parse(toolCall.function?.arguments as string); + // Check if functionArgs is correctly structured and contains the expected properties + if (functionArgs && typeof functionArgs === 'object' && 'drugName' in functionArgs) { + // Execute the function call with the provided arguments + const reply = await functionToCall(functionArgs.drugName, functionArgs.section); + await messageData.reply(reply as MessageReplyOptions); + return; + } + // Log an error if the function arguments do not have the expected structure or types + log.error(F, `Invalid arguments structure for function ${functionName}: ${JSON.stringify(functionArgs)}`); + }); + + // Wait for all promises to resolve + const results = await Promise.all(promises); + log.debug(F, 'All tool calls have been processed'); + + return { response: 'functionFinished', promptTokens, completionTokens }; + } + case 'expired': + // This will happen if the requires_action doesn't get a response in time, so this isn't supported either + break; + case 'cancelling': + // This would only happen if i manually cancel the request, which isn't supported + break; + case 'cancelled': + // Take a guess =D + break; + case 'failed': { + // This should send an error to the dev + const devRoom = await discordClient.channels.fetch(env.CHANNEL_BOTERRORS) as TextChannel; + await devRoom.send(`AI Conversation failed: ${JSON.stringify(run, null, 2)}`); + log.error(F, `run ended: ${JSON.stringify(run, null, 2)}`); + return { response, promptTokens, completionTokens }; + } + default: + break; + } + log.debug(F, `Returning response: ${response}`); + return { response, promptTokens, completionTokens }; +} + async function openAiConversation( aiPersona:ai_personas, messages: { role: 'user'; content: string; }[], - user: string, + messageData: Message, ):Promise<{ response: string, promptTokens: number, completionTokens: number, }> { - // if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return; - // if (!messageData.member?.roles.cache.has(env.ROLE_VERIFIED)) return; - // if (messageData.author.bot) return; - // if (messageData.cleanContent.length < 1) return; - // if (messageData.channel.type === ChannelType.DM) return; - // if (messageData.author.id !== env.DISCORD_OWNER_ID) return; - - let response = ''; + // Create the default response if something goes wrong + const response = ''; const promptTokens = 0; const completionTokens = 0; - // Get the assistant for this channel. - // Right now the only assistant is the 'tripsitter' assistant + // Get the assistant associated with that persona + // This will create a new assistant if it doesn't exist + // Regardless, it will update the assistance using the latest persona data const assistant = await getAssistant(aiPersona.name); // log.debug(F, `assistant: ${JSON.stringify(assistant, null, 2)}`); + // Get the user from the DB const userData = await db.users.upsert({ - where: { discord_id: user }, - create: { discord_id: user }, + where: { discord_id: messageData.author.id }, + create: { discord_id: messageData.author.id }, update: {}, }); - // Get the thread for the user who said something + // Get the threadID, or create a new thread and use that ID const thread = userData.ai_history_openai ? await openAi.beta.threads.retrieve(userData.ai_history_openai) : await openAi.beta.threads.create(); - // log.debug(F, `thread: ${JSON.stringify(thread, null, 2)}`); + + // Save the thread id to the user if it doesn't exist if (!userData.ai_history_openai) { - // Save the thread id to the user's data - log.debug(F, `Saving new thread id to user: ${user}`); + log.debug(F, `Saving new thread id to user: ${messageData.author.id}`); await db.users.update({ - where: { discord_id: user }, + where: { discord_id: messageData.author.id }, data: { ai_history_openai: thread.id, }, }); } - // Add the message to the thread + // Before we add the message to the thread, we need to check if the last run is still in progress + // If it is, we need to cancel it before we can add a new message + // This is because the assistant can only handle one request at a time + // If we don't cancel the run, the new message will create an error + + // Get the most recent run + // This will automatically return the most recent runs, so we can just grab the first one + let [recentRun] = (await openAi.beta.threads.runs.list(thread.id, { limit: 1 })).data; + log.debug(F, `recentRun: ${JSON.stringify(recentRun, null, 2)}`); + + // If the most recent run is in progress, queued, or waiting for user action, stop it + if (recentRun && ['queued', 'in_progress', 'requires_action'].includes(recentRun.status)) { + log.debug(F, 'Stopping the run'); + await openAi.beta.threads.runs.cancel(thread.id, recentRun.id); + + // Wait for the run to be cancelled + while (['queued', 'in_progress', 'requires_action'].includes(recentRun.status)) { + // eslint-disable-next-line no-await-in-loop + await sleep(200); + // eslint-disable-next-line no-await-in-loop + recentRun = await openAi.beta.threads.runs.retrieve(thread.id, recentRun.id); + } + } + // Add the message to the thread try { - const message = await openAi.beta.threads.messages.create( + await openAi.beta.threads.messages.create( thread.id, messages[0], ); } catch (error) { log.error(F, `Error sending message: ${error}`); - console.log(error); - - // Get all the runs - const runs = await openAi.beta.threads.runs.list(thread.id, { - limit: 1, - }); - - // The most recent run is the first one in the sorted array - const recentRun = runs.data[0]; - - // If the most recent run is in progress, stop it - if (recentRun.status === 'in_progress') { - log.debug(F, 'Stopping the run'); - await openAi.beta.threads.runs.cancel(thread.id, recentRun.id); - } - - // Add the message to the thread - const message = await openAi.beta.threads.messages.create( - thread.id, - messages[0], - ); + return { response: (error as Error).message, promptTokens, completionTokens }; } - // log.debug(F, `message: ${JSON.stringify(message, null, 2)}`); - - log.debug(F, `Starting new run with assistant: ${assistant.id} and thread: ${thread.id}`); // Run the thread + log.debug(F, `Starting new run with assistant: ${assistant.id} and thread: ${thread.id}`); const run = await openAi.beta.threads.runs.create( thread.id, { @@ -638,69 +735,9 @@ async function openAiConversation( // log.debug(F, `run: ${JSON.stringify(run, null, 2)}`); // Wait for the run to complete - let runStatus = 'queued' as Run['status']; - while (['queued', 'in_progress'].includes(runStatus)) { - // Send the typing indicator to show tripbot is thinking - // eslint-disable-next-line no-await-in-loop - // await messageData.channel.sendTyping(); - - // eslint-disable-next-line no-await-in-loop - await sleep(200); - // eslint-disable-next-line no-await-in-loop - const runStatusResponse = await readRun(thread, run); - // log.debug(F, `runStatusResponse: ${JSON.stringify(runStatusResponse, null, 2)}`); - runStatus = runStatusResponse.status; - } - - const devRoom = await discordClient.channels.fetch(env.CHANNEL_BOTERRORS) as TextChannel; - - // Depending on how the run ended, do something - switch (runStatus) { - case 'completed': { - // This should pull the thread and then get the last message in the thread, which should be from the assistant - const messagePage = await getMessages(thread, { limit: 10 }); - // log.debug(F, `messagePage: ${JSON.stringify(messagePage, null, 2)}`); - const messageItems = messagePage.getPaginatedItems(); - const messageText = messageItems[0]; - const [messageContent] = messageText.content; - if ((messageContent as MessageContentText).text) { - log.debug(F, `messageContent: ${JSON.stringify(messageContent, null, 2)}`); - - // Send the result to the dev room - // await devRoom.send(`AI Conversation succeeded: ${JSON.stringify(messageContent, null, 2)}`); - - // await messages[0].reply(result.response.slice(0, 2000)); - response = (messageContent as MessageContentText).text.value; - // await messageData.reply(response); - - return { response, promptTokens, completionTokens }; - } - - break; - } - case 'requires_action': - // No way to support additional actions at this time - break; - case 'expired': - // This will happen if the requires_action doesn't get a response in time, so this isn't supported either - break; - case 'cancelling': - // This would only happen if i manually cancel the request, which isn't supported - break; - case 'cancelled': - // Take a guess =D - break; - case 'failed': { - // This should send an error to the dev - await devRoom.send(`AI Conversation failed: ${JSON.stringify(run, null, 2)}`); - log.error(F, `run ended: ${JSON.stringify(run, null, 2)}`); - return { response, promptTokens, completionTokens }; - } - default: - break; - } + // This is a separate function because if the run requires action, it will call the function again - return { response, promptTokens, completionTokens }; + return openAiWaitForRun(run, thread, messageData); } async function openAiChat( @@ -730,23 +767,6 @@ async function openAiChat( }] as OpenAI.Chat.ChatCompletionMessageParam[]; chatCompletionMessages.push(...messages); - // eslint-disable-next-line @typescript-eslint/naming-convention - // const { - // id, - // name, - // description, - // public, - // created_at, // eslint-disable-line @typescript-eslint/naming-convention - // created_by, // eslint-disable-line @typescript-eslint/naming-convention - // prompt, - // logit_bias, // eslint-disable-line @typescript-eslint/naming-convention - // total_tokens, // eslint-disable-line @typescript-eslint/naming-convention - // downvotes, - // upvotes, - // ai_model: modelName, // eslint-disable-line @typescript-eslint/naming-convention - // ...restOfAiPersona - // } = aiPersona; - const payload = { temperature: aiPersona.temperature, top_p: aiPersona.top_p, @@ -786,46 +806,42 @@ async function openAiChat( completionTokens = chatCompletion.usage?.completion_tokens ?? 0; // # Step 2: check if GPT wanted to call a function - // if (responseMessage.function_call) { - // log.debug(F, `responseMessage.function_call: ${JSON.stringify(responseMessage.function_call, null, 2)}`); - // // # Step 3: call the function - // // # Note: the JSON response may not always be valid; be sure to handle errors - - // const availableFunctions = { - // getCurrentWeather, - // aiModerateReport, - // }; - // const functionName = responseMessage.function_call.name; - // log.debug(F, `functionName: ${functionName}`); - // const functionToCall = availableFunctions[functionName as keyof typeof availableFunctions]; - // const functionArgs = JSON.parse(responseMessage.function_call.arguments as string); - // const functionResponse = await functionToCall( - // functionArgs.location, - // functionArgs.unit, - // ); - // log.debug(F, `functionResponse: ${JSON.stringify(functionResponse, null, 2)}`); - - // // # Step 4: send the info on the function call and function response to GPT - // payload.messages.push({ - // role: 'function', - // name: functionName, - // content: JSON.stringify(functionResponse), - // }); - - // const chatFunctionCompletion = await openai.chat.completions.create(payload); - - // // responseData = chatFunctionCompletion.data; - - // log.debug(F, `chatFunctionCompletion: ${JSON.stringify(chatFunctionCompletion, null, 2)}`); - - // if (chatFunctionCompletion.choices[0].message) { - // responseMessage = chatFunctionCompletion.choices[0].message; - - // // Sum up the new tokens - // promptTokens += chatCompletion.usage?.prompt_tokens ?? 0; - // completionTokens += chatCompletion.usage?.completion_tokens ?? 0; - // } - // } + if (responseMessage.function_call) { + log.debug(F, `responseMessage.function_call: ${JSON.stringify(responseMessage.function_call, null, 2)}`); + // # Step 3: call the function + // # Note: the JSON response may not always be valid; be sure to handle errors + + const functionName = responseMessage.function_call.name; + log.debug(F, `functionName: ${functionName}`); + const functionToCall = availableFunctions[functionName as keyof typeof availableFunctions]; + const functionArgs = JSON.parse(responseMessage.function_call.arguments); + const functionResponse = await functionToCall( + functionArgs.location, + functionArgs.unit, + ); + log.debug(F, `functionResponse: ${JSON.stringify(functionResponse, null, 2)}`); + + // # Step 4: send the info on the function call and function response to GPT + payload.messages.push({ + role: 'function', + name: functionName, + content: JSON.stringify(functionResponse), + }); + + const chatFunctionCompletion = await openAi.chat.completions.create(payload); + + // responseData = chatFunctionCompletion.data; + + log.debug(F, `chatFunctionCompletion: ${JSON.stringify(chatFunctionCompletion, null, 2)}`); + + if (chatFunctionCompletion.choices[0].message) { + responseMessage = chatFunctionCompletion.choices[0].message; + + // Sum up the new tokens + promptTokens += chatCompletion.usage?.prompt_tokens ?? 0; + completionTokens += chatCompletion.usage?.completion_tokens ?? 0; + } + } response = responseMessage.content ?? 'Sorry, I\'m not sure how to respond to that.'; } @@ -843,7 +859,7 @@ export default async function aiChat( role: 'user'; content: string; }[], - user: string, + messageData: Message, attachmentInfo: { url: string | null; mimeType: string | null; @@ -864,10 +880,10 @@ export default async function aiChat( if (['GEMINI_PRO', 'GEMINI_PRO_VISION', 'AQA'].includes(aiPersona.ai_model)) { // return googleAiChat(aiPersona, messages, user, attachmentInfo); - return googleAiConversation(aiPersona, messages, user, attachmentInfo); + return googleAiConversation(aiPersona, messages, messageData, attachmentInfo); } // return openAiChat(aiPersona, messages, user); - return openAiConversation(aiPersona, messages, user); + return openAiConversation(aiPersona, messages, messageData); } export async function aiFlairMod( @@ -916,8 +932,6 @@ export async function aiFlairMod( ...restOfAiPersona, model, messages: chatCompletionMessages, - // functions: aiFunctions, - // function_call: 'auto', } as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming; // log.debug(F, `payload: ${JSON.stringify(payload, null, 2)}`); @@ -944,48 +958,6 @@ export async function aiFlairMod( promptTokens = chatCompletion.usage?.prompt_tokens ?? 0; completionTokens = chatCompletion.usage?.completion_tokens ?? 0; - // # Step 2: check if GPT wanted to call a function - // if (responseMessage.function_call) { - // log.debug(F, `responseMessage.function_call: ${JSON.stringify(responseMessage.function_call, null, 2)}`); - // // # Step 3: call the function - // // # Note: the JSON response may not always be valid; be sure to handle errors - - // const availableFunctions = { - // getCurrentWeather, - // aiModerateReport, - // }; - // const functionName = responseMessage.function_call.name; - // log.debug(F, `functionName: ${functionName}`); - // const functionToCall = availableFunctions[functionName as keyof typeof availableFunctions]; - // const functionArgs = JSON.parse(responseMessage.function_call.arguments as string); - // const functionResponse = await functionToCall( - // functionArgs.location, - // functionArgs.unit, - // ); - // log.debug(F, `functionResponse: ${JSON.stringify(functionResponse, null, 2)}`); - - // // # Step 4: send the info on the function call and function response to GPT - // payload.messages.push({ - // role: 'function', - // name: functionName, - // content: JSON.stringify(functionResponse), - // }); - - // const chatFunctionCompletion = await openai.chat.completions.create(payload); - - // // responseData = chatFunctionCompletion.data; - - // log.debug(F, `chatFunctionCompletion: ${JSON.stringify(chatFunctionCompletion, null, 2)}`); - - // if (chatFunctionCompletion.choices[0].message) { - // responseMessage = chatFunctionCompletion.choices[0].message; - - // // Sum up the new tokens - // promptTokens += chatCompletion.usage?.prompt_tokens ?? 0; - // completionTokens += chatCompletion.usage?.completion_tokens ?? 0; - // } - // } - response = responseMessage.content ?? 'Sorry, I\'m not sure how to respond to that.'; } @@ -1005,7 +977,7 @@ export async function aiModerate( ):Promise { const moderation = await aiModerateReport(message); - if (!moderation || !moderation.results) { + if (!moderation?.results) { return []; }