From 37c0ce4481e5a52071282c1afc27a2f49149b587 Mon Sep 17 00:00:00 2001 From: alphaBEE <61616007+ankitsmt211@users.noreply.github.com> Date: Thu, 7 Mar 2024 04:33:25 +0530 Subject: [PATCH 1/4] AI response should be in embed * helper method to generate embed * remove AI parser class that partitioned responses & related tests * refactor dismiss button on generated responses * update docs --- .../togetherjava/tjbot/features/Features.java | 2 +- .../features/chatgpt/AIResponseParser.java | 82 ------------------- .../features/chatgpt/ChatGptCommand.java | 27 ++++-- .../features/chatgpt/ChatGptService.java | 6 +- .../tjbot/features/help/HelpSystemHelper.java | 54 +++++++----- 5 files changed, 57 insertions(+), 114 deletions(-) delete mode 100644 application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 7dbc19d447..1052381988 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -162,7 +162,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new HelpThreadCommand(config, helpSystemHelper)); features.add(new ReportCommand(config)); features.add(new BookmarksCommand(bookmarksSystem)); - features.add(new ChatGptCommand(chatGptService)); + features.add(new ChatGptCommand(chatGptService, helpSystemHelper)); features.add(new JShellCommand(jshellEval)); FeatureBlacklist> blacklist = blacklistConfig.normal(); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java deleted file mode 100644 index 9dce43ff1c..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.togetherjava.tjbot.features.chatgpt; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Represents a class to partition long text blocks into smaller blocks which work with Discord's - * API. Initially constructed to partition text from AI text generation APIs. - */ -public class AIResponseParser { - private AIResponseParser() { - throw new UnsupportedOperationException("Utility class, construction not supported"); - } - - private static final Logger logger = LoggerFactory.getLogger(AIResponseParser.class); - private static final int RESPONSE_LENGTH_LIMIT = 2_000; - - /** - * Parses the response generated by AI. If response is longer than - * {@value RESPONSE_LENGTH_LIMIT}, then breaks apart the response into suitable lengths for - * Discords API. - * - * @param response The response from the AI which we want to send over Discord. - * @return An array potentially holding the original response split up into shorter than - * {@value RESPONSE_LENGTH_LIMIT} length pieces. - */ - public static String[] parse(String response) { - String[] partedResponse = new String[] {response}; - if (response.length() > RESPONSE_LENGTH_LIMIT) { - logger.debug("Response to parse:\n{}", response); - partedResponse = partitionAiResponse(response); - } - - return partedResponse; - } - - private static String[] partitionAiResponse(String response) { - List responseChunks = new ArrayList<>(); - String[] splitResponseOnMarks = response.split("```"); - - for (int i = 0; i < splitResponseOnMarks.length; i++) { - String split = splitResponseOnMarks[i]; - List chunks = new ArrayList<>(); - chunks.add(split); - - // Check each chunk for correct length. If over the length, split in two and check - // again. - while (!chunks.stream().allMatch(s -> s.length() < RESPONSE_LENGTH_LIMIT)) { - for (int j = 0; j < chunks.size(); j++) { - String chunk = chunks.get(j); - if (chunk.length() > RESPONSE_LENGTH_LIMIT) { - int midpointNewline = chunk.lastIndexOf("\n", chunk.length() / 2); - chunks.set(j, chunk.substring(0, midpointNewline)); - chunks.add(j + 1, chunk.substring(midpointNewline)); - } - } - } - - // Given the splitting on ```, the odd numbered entries need to have code marks - // restored. - if (i % 2 != 0) { - // We assume that everything after the ``` on the same line is the language - // declaration. Could be empty. - String lang = split.substring(0, split.indexOf(System.lineSeparator())); - chunks = chunks.stream() - .map(s -> ("```" + lang).concat(s).concat("```")) - // Handle case of doubling language declaration - .map(s -> s.replaceFirst("```" + lang + lang, "```" + lang)) - .collect(Collectors.toList()); - } - - List list = chunks.stream().filter(string -> !string.equals("")).toList(); - responseChunks.addAll(list); - } // end of for loop. - - return responseChunks.toArray(new String[0]); - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java index 30477ba74e..38bb5fc400 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java @@ -2,6 +2,8 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.SelfUser; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.components.Modal; @@ -10,6 +12,7 @@ import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; +import org.togetherjava.tjbot.features.help.HelpSystemHelper; import java.time.Duration; import java.time.Instant; @@ -28,6 +31,7 @@ public final class ChatGptCommand extends SlashCommandAdapter { private static final int MIN_MESSAGE_INPUT_LENGTH = 4; private static final Duration COMMAND_COOLDOWN = Duration.of(10, ChronoUnit.SECONDS); private final ChatGptService chatGptService; + private final HelpSystemHelper helper; private final Cache userIdToAskedAtCache = Caffeine.newBuilder().maximumSize(1_000).expireAfterWrite(COMMAND_COOLDOWN).build(); @@ -36,11 +40,13 @@ public final class ChatGptCommand extends SlashCommandAdapter { * Creates an instance of the chatgpt command. * * @param chatGptService ChatGptService - Needed to make calls to ChatGPT API + * @param helper HelpSystemHelper - Needed to generate response embed for prompt */ - public ChatGptCommand(ChatGptService chatGptService) { + public ChatGptCommand(ChatGptService chatGptService, HelpSystemHelper helper) { super(COMMAND_NAME, "Ask the ChatGPT AI a question!", CommandVisibility.GUILD); this.chatGptService = chatGptService; + this.helper = helper; } @Override @@ -75,20 +81,23 @@ public void onModalSubmitted(ModalInteractionEvent event, List args) { event.deferReply().queue(); String context = ""; - Optional optional = - chatGptService.ask(event.getValue(QUESTION_INPUT).getAsString(), context); + String question = event.getValue(QUESTION_INPUT).getAsString(); + + Optional optional = chatGptService.ask(question, context); if (optional.isPresent()) { userIdToAskedAtCache.put(event.getMember().getId(), Instant.now()); } - String[] errorResponse = {""" + String errorResponse = """ An error has occurred while trying to communicate with ChatGPT. Please try again later. - """}; + """; - String[] response = optional.orElse(errorResponse); - for (String message : response) { - event.getHook().sendMessage(message).queue(); - } + String response = optional.orElse(errorResponse); + SelfUser selfUser = event.getJDA().getSelfUser(); + + MessageEmbed responseEmbed = helper.generateGptResponseEmbed(response, selfUser, question); + + event.getHook().sendMessageEmbeds(responseEmbed).queue(); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java index a145b42139..e8b02d04bb 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java @@ -87,11 +87,11 @@ public ChatGptService(Config config) { * @param question The question being asked of ChatGPT. Max is {@value MAX_TOKENS} tokens. * @param context The category of asked question, to set the context(eg. Java, Database, Other * etc). - * @return partitioned response from ChatGPT as a String array. + * @return response from ChatGPT as a String. * @see ChatGPT * Tokens. */ - public Optional ask(String question, String context) { + public Optional ask(String question, String context) { if (isDisabled) { return Optional.empty(); } @@ -121,7 +121,7 @@ public Optional ask(String question, String context) { return Optional.empty(); } - return Optional.of(AIResponseParser.parse(response)); + return Optional.of(response); } catch (OpenAiHttpException openAiHttpException) { logger.warn( "There was an error using the OpenAI API: {} Code: {} Type: {} Status Code: {}", diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java index 917595d80f..4672eb378d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java @@ -1,5 +1,6 @@ package org.togetherjava.tjbot.features.help; +import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer; import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel; @@ -9,7 +10,6 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.requests.RestAction; -import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +33,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -117,7 +116,7 @@ public HelpSystemHelper(Config config, Database database, ChatGptService chatGpt RestAction constructChatGptAttempt(ThreadChannel threadChannel, String originalQuestion, ComponentIdInteractor componentIdInteractor) { Optional questionOptional = prepareChatGptQuestion(threadChannel, originalQuestion); - Optional chatGPTAnswer; + Optional chatGPTAnswer; if (questionOptional.isEmpty()) { return useChatGptFallbackMessage(threadChannel); @@ -130,11 +129,12 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, String context = matchingTag.getName(); chatGPTAnswer = chatGptService.ask(question, context); + if (chatGPTAnswer.isEmpty()) { return useChatGptFallbackMessage(threadChannel); } - List ids = new CopyOnWriteArrayList<>(); + StringBuilder idForDismissButton = new StringBuilder(); RestAction message = mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) .map(""" @@ -143,27 +143,43 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, %s. """::formatted) .flatMap(threadChannel::sendMessage) - .onSuccess(m -> ids.add(m.getId())); - String[] answers = chatGPTAnswer.orElseThrow(); - - for (int i = 0; i < answers.length; i++) { - MessageCreateAction answer = threadChannel.sendMessage(answers[i]); + .onSuccess(m -> idForDismissButton.append(m.getId())); - if (i == answers.length - 1) { - message = message.flatMap(any -> answer - .addActionRow(generateDismissButton(componentIdInteractor, ids))); - continue; - } + String answer = chatGPTAnswer.orElseThrow(); + SelfUser selfUser = threadChannel.getJDA().getSelfUser(); - message = message.flatMap(ignored -> answer.onSuccess(m -> ids.add(m.getId()))); + int MESSAGE_CHARACTER_LIMIT = MessageEmbed.DESCRIPTION_MAX_LENGTH; + if (answer.length() > MESSAGE_CHARACTER_LIMIT) { + answer = answer.substring(0, MESSAGE_CHARACTER_LIMIT); } - return message; + MessageEmbed responseEmbed = generateGptResponseEmbed(answer, selfUser, originalQuestion); + return message.flatMap(any -> threadChannel.sendMessageEmbeds(responseEmbed) + .addActionRow( + generateDismissButton(componentIdInteractor, idForDismissButton.toString()))); + } + + public MessageEmbed generateGptResponseEmbed(String answer, SelfUser selfUser, String title) { + String responseByGptFooter = "- AI generated response"; + + int TITLE_EMBED_LIMIT = MessageEmbed.TITLE_MAX_LENGTH; + String capitalizedTitle = Character.toUpperCase(title.charAt(0)) + title.substring(1); + + String titleForEmbed = capitalizedTitle.length() > TITLE_EMBED_LIMIT + ? capitalizedTitle.substring(0, TITLE_EMBED_LIMIT) + : capitalizedTitle; + + return new EmbedBuilder() + .setAuthor(selfUser.getName(), null, selfUser.getEffectiveAvatarUrl()) + .setTitle(titleForEmbed) + .setDescription(answer) + .setColor(Color.pink) + .setFooter(responseByGptFooter) + .build(); } - private Button generateDismissButton(ComponentIdInteractor componentIdInteractor, - List ids) { - String buttonId = componentIdInteractor.generateComponentId(ids.toArray(String[]::new)); + private Button generateDismissButton(ComponentIdInteractor componentIdInteractor, String id) { + String buttonId = componentIdInteractor.generateComponentId(id); return Button.danger(buttonId, "Dismiss"); } From 4b11bb0c4bbf349df33277ffdb8da39e5950b59d Mon Sep 17 00:00:00 2001 From: alphaBEE <61616007+ankitsmt211@users.noreply.github.com> Date: Thu, 7 Mar 2024 04:45:29 +0530 Subject: [PATCH 2/4] remove AIResponseParserTest --- .../chatgpt/AIResponseParserTest.java | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java deleted file mode 100644 index 715dc14f0c..0000000000 --- a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.togetherjava.tjbot.features.chatgpt; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Objects; - -class AIResponseParserTest { - private static final Logger logger = LoggerFactory.getLogger(AIResponseParserTest.class); - - @ParameterizedTest - @ValueSource(ints = {1, 2, 3, 4}) - void correctResponseLength(int fileNumber) { - try (InputStream in = getClass().getClassLoader() - .getResourceAsStream("AITestResponses/test" + fileNumber + ".txt")) { - String response = new String(Objects.requireNonNull(in).readAllBytes()); - String[] aiResponse = AIResponseParser.parse(response); - - testResponseLength(aiResponse); - toLog(aiResponse); - } catch (IOException | NullPointerException ex) { - logger.error("{}", ex.getMessage()); - Assertions.fail(); - } - } - - private void testResponseLength(String[] responses) { - int AI_RESPONSE_CHARACTER_LIMIT = 2000; - for (String response : responses) { - Assertions.assertTrue(response.length() <= AI_RESPONSE_CHARACTER_LIMIT, - "Response length is NOT within character limit: " + response.length()); - logger.warn("Response length was: {}", response.length()); - } - } - - private void toLog(String[] responses) { - for (String response : responses) { - logger.info(response); - } - } -} From 5595c1901256118c8997841b350d5f121243760d Mon Sep 17 00:00:00 2001 From: alphaBEE <61616007+ankitsmt211@users.noreply.github.com> Date: Thu, 7 Mar 2024 04:47:57 +0530 Subject: [PATCH 3/4] unnecessary additon From 9f9f2d1f2c24c4243c09e6781043b2234aff4200 Mon Sep 17 00:00:00 2001 From: alphaBEE <61616007+ankitsmt211@users.noreply.github.com> Date: Thu, 7 Mar 2024 04:56:44 +0530 Subject: [PATCH 4/4] refactor variable names as per sonar suggestion --- .../tjbot/features/help/HelpSystemHelper.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java index 4672eb378d..6a93aeadc1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java @@ -148,9 +148,9 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, String answer = chatGPTAnswer.orElseThrow(); SelfUser selfUser = threadChannel.getJDA().getSelfUser(); - int MESSAGE_CHARACTER_LIMIT = MessageEmbed.DESCRIPTION_MAX_LENGTH; - if (answer.length() > MESSAGE_CHARACTER_LIMIT) { - answer = answer.substring(0, MESSAGE_CHARACTER_LIMIT); + int responseCharLimit = MessageEmbed.DESCRIPTION_MAX_LENGTH; + if (answer.length() > responseCharLimit) { + answer = answer.substring(0, responseCharLimit); } MessageEmbed responseEmbed = generateGptResponseEmbed(answer, selfUser, originalQuestion); @@ -162,11 +162,11 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, public MessageEmbed generateGptResponseEmbed(String answer, SelfUser selfUser, String title) { String responseByGptFooter = "- AI generated response"; - int TITLE_EMBED_LIMIT = MessageEmbed.TITLE_MAX_LENGTH; + int embedTitleLimit = MessageEmbed.TITLE_MAX_LENGTH; String capitalizedTitle = Character.toUpperCase(title.charAt(0)) + title.substring(1); - String titleForEmbed = capitalizedTitle.length() > TITLE_EMBED_LIMIT - ? capitalizedTitle.substring(0, TITLE_EMBED_LIMIT) + String titleForEmbed = capitalizedTitle.length() > embedTitleLimit + ? capitalizedTitle.substring(0, embedTitleLimit) : capitalizedTitle; return new EmbedBuilder()