From a72b5db745766ddfa11a775e391f9e9ca5e6c6ca Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Tue, 4 Apr 2023 16:39:06 -0700 Subject: [PATCH 01/29] Update ChatGPTCommand.java to include character limiting and rate limiting on time per user. Include modal when no option is included for formatting question to ChatGPT. --- .../features/chaptgpt/ChatGptCommand.java | 86 ++++++++++++++++++- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index ce1b9c02f0..a9d0a9a2bc 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -1,22 +1,41 @@ package org.togetherjava.tjbot.features.chaptgpt; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.components.Modal; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; + /** * The implemented command is {@code /chatgpt}, which allows users to ask ChatGPT a question, upon * which it will respond with an AI generated answer. */ public final class ChatGptCommand extends SlashCommandAdapter { private static final String QUESTION_OPTION = "question"; + private static final String MESSAGE_INPUT = "message"; + private static final int MESSAGE_INPUT_LENGTH = 200; + private static final int MIN_MESSAGE_INPUT_LENGTH = 4; + private static final Duration CACHE_DURATION = Duration.of(10, ChronoUnit.SECONDS); private final ChatGptService chatGptService; + private final Cache userIdToAskedAtCache = + Caffeine.newBuilder().maximumSize(1_000).expireAfterWrite(CACHE_DURATION).build(); + /** * Creates an instance of the chatgpt command. - * + * * @param chatGptService ChatGptService - Needed to make calls to ChatGPT API */ public ChatGptCommand(ChatGptService chatGptService) { @@ -24,14 +43,73 @@ public ChatGptCommand(ChatGptService chatGptService) { this.chatGptService = chatGptService; - getData().addOption(OptionType.STRING, QUESTION_OPTION, "What do you want to ask?", true); + getData().addOption(OptionType.STRING, QUESTION_OPTION, "What do you want to ask?", false); } @Override public void onSlashCommand(SlashCommandInteractionEvent event) { + Instant previousAskTime = userIdToAskedAtCache.getIfPresent(event.getMember().getId()); + if (previousAskTime != null) { + long timeRemainingUntilNextAsk = CACHE_DURATION.getSeconds() + - Duration.between(previousAskTime, Instant.now()).get(ChronoUnit.SECONDS); + String s = ""; + if (timeRemainingUntilNextAsk > 1) { + s = "s"; + } + + event + .reply("Sorry, you need to wait another " + timeRemainingUntilNextAsk + " second" + + s + " before asking chatGPT another question.") + .setEphemeral(true) + .queue(); + return; + } + + if (event.getInteraction().getOptions().isEmpty()) { + TextInput body = TextInput + .create(MESSAGE_INPUT, "Ask ChatGPT a question or get help with code", + TextInputStyle.PARAGRAPH) + .setPlaceholder("Put your question for ChatGPT here") + .setRequiredRange(MIN_MESSAGE_INPUT_LENGTH, MESSAGE_INPUT_LENGTH) + .build(); + + Modal modal = Modal.create(generateComponentId(), "ChatGPT").addActionRow(body).build(); + event.replyModal(modal).queue(); + return; + } + + if (event.getOption(QUESTION_OPTION).getAsString().length() > MESSAGE_INPUT_LENGTH) { + event.getHook() + .sendMessage("Questions to ChatGPT must be less than " + MESSAGE_INPUT_LENGTH + + " characters in length. Please rephrase and try again.") + .queue(); + return; + } + + event.deferReply().queue(); + + Optional optional = + chatGptService.ask(event.getOption(QUESTION_OPTION).getAsString()); + if (optional.isPresent()) { + userIdToAskedAtCache.put(event.getMember().getId(), Instant.now()); + } + + String response = optional.orElse( + "An error has occurred while trying to communicate with ChatGPT. Please try again later"); + event.getHook().sendMessage(response).queue(); + } + + @Override + public void onModalSubmitted(ModalInteractionEvent event, List args) { event.deferReply().queue(); - String response = chatGptService.ask(event.getOption(QUESTION_OPTION).getAsString()) - .orElse("An error has occurred while trying to communication with ChatGPT. Please try again later"); + + Optional optional = chatGptService.ask(event.getValue(MESSAGE_INPUT).getAsString()); + if (optional.isPresent()) { + userIdToAskedAtCache.put(event.getMember().getId(), Instant.now()); + } + + String response = optional.orElse( + "An error has occurred while trying to communicate with ChatGPT. Please try again later"); event.getHook().sendMessage(response).queue(); } } From 11f3e8a617f2aa70cfe8ccfd97b05d1e0e0ffa09 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Wed, 5 Apr 2023 09:27:17 -0700 Subject: [PATCH 02/29] Requested changes to ChatGptCommand.java: - Variable name clean up - Command now only uses modal - Remove optional s from string --- .../features/chaptgpt/ChatGptCommand.java | 44 ++++--------------- 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index a9d0a9a2bc..a5f18d5293 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -4,7 +4,6 @@ import com.github.benmanes.caffeine.cache.Caffeine; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.components.Modal; import net.dv8tion.jda.api.interactions.components.text.TextInput; import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; @@ -23,9 +22,8 @@ * which it will respond with an AI generated answer. */ public final class ChatGptCommand extends SlashCommandAdapter { - private static final String QUESTION_OPTION = "question"; - private static final String MESSAGE_INPUT = "message"; - private static final int MESSAGE_INPUT_LENGTH = 200; + private static final String QUESTION_INPUT = "question"; + private static final int MAX_MESSAGE_INPUT_LENGTH = 200; private static final int MIN_MESSAGE_INPUT_LENGTH = 4; private static final Duration CACHE_DURATION = Duration.of(10, ChronoUnit.SECONDS); private final ChatGptService chatGptService; @@ -42,8 +40,6 @@ public ChatGptCommand(ChatGptService chatGptService) { super("chatgpt", "Ask the ChatGPT AI a question!", CommandVisibility.GUILD); this.chatGptService = chatGptService; - - getData().addOption(OptionType.STRING, QUESTION_OPTION, "What do you want to ask?", false); } @Override @@ -52,14 +48,10 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { if (previousAskTime != null) { long timeRemainingUntilNextAsk = CACHE_DURATION.getSeconds() - Duration.between(previousAskTime, Instant.now()).get(ChronoUnit.SECONDS); - String s = ""; - if (timeRemainingUntilNextAsk > 1) { - s = "s"; - } event - .reply("Sorry, you need to wait another " + timeRemainingUntilNextAsk + " second" - + s + " before asking chatGPT another question.") + .reply("Sorry, you need to wait another " + timeRemainingUntilNextAsk + + " second(s) before asking chatGPT another question.") .setEphemeral(true) .queue(); return; @@ -67,43 +59,23 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { if (event.getInteraction().getOptions().isEmpty()) { TextInput body = TextInput - .create(MESSAGE_INPUT, "Ask ChatGPT a question or get help with code", + .create(QUESTION_INPUT, "Ask ChatGPT a question or get help with code", TextInputStyle.PARAGRAPH) .setPlaceholder("Put your question for ChatGPT here") - .setRequiredRange(MIN_MESSAGE_INPUT_LENGTH, MESSAGE_INPUT_LENGTH) + .setRequiredRange(MIN_MESSAGE_INPUT_LENGTH, MAX_MESSAGE_INPUT_LENGTH) .build(); Modal modal = Modal.create(generateComponentId(), "ChatGPT").addActionRow(body).build(); event.replyModal(modal).queue(); - return; - } - - if (event.getOption(QUESTION_OPTION).getAsString().length() > MESSAGE_INPUT_LENGTH) { - event.getHook() - .sendMessage("Questions to ChatGPT must be less than " + MESSAGE_INPUT_LENGTH - + " characters in length. Please rephrase and try again.") - .queue(); - return; - } - - event.deferReply().queue(); - - Optional optional = - chatGptService.ask(event.getOption(QUESTION_OPTION).getAsString()); - if (optional.isPresent()) { - userIdToAskedAtCache.put(event.getMember().getId(), Instant.now()); } - - String response = optional.orElse( - "An error has occurred while trying to communicate with ChatGPT. Please try again later"); - event.getHook().sendMessage(response).queue(); } @Override public void onModalSubmitted(ModalInteractionEvent event, List args) { event.deferReply().queue(); - Optional optional = chatGptService.ask(event.getValue(MESSAGE_INPUT).getAsString()); + Optional optional = + chatGptService.ask(event.getValue(QUESTION_INPUT).getAsString()); if (optional.isPresent()) { userIdToAskedAtCache.put(event.getMember().getId(), Instant.now()); } From cf43e51fbdc178d23444049257e641f695b48ba3 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Wed, 5 Apr 2023 13:11:27 -0700 Subject: [PATCH 03/29] Requested changes to ChatGptCommand.java: - CACHE_DURATION -> COMMAND_COOLDOWN - Unwind if statement --- .../features/chaptgpt/ChatGptCommand.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index a5f18d5293..8ec3a38471 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -25,11 +25,11 @@ public final class ChatGptCommand extends SlashCommandAdapter { private static final String QUESTION_INPUT = "question"; private static final int MAX_MESSAGE_INPUT_LENGTH = 200; private static final int MIN_MESSAGE_INPUT_LENGTH = 4; - private static final Duration CACHE_DURATION = Duration.of(10, ChronoUnit.SECONDS); + private static final Duration COMMAND_COOLDOWN = Duration.of(10, ChronoUnit.SECONDS); private final ChatGptService chatGptService; private final Cache userIdToAskedAtCache = - Caffeine.newBuilder().maximumSize(1_000).expireAfterWrite(CACHE_DURATION).build(); + Caffeine.newBuilder().maximumSize(1_000).expireAfterWrite(COMMAND_COOLDOWN).build(); /** * Creates an instance of the chatgpt command. @@ -46,7 +46,7 @@ public ChatGptCommand(ChatGptService chatGptService) { public void onSlashCommand(SlashCommandInteractionEvent event) { Instant previousAskTime = userIdToAskedAtCache.getIfPresent(event.getMember().getId()); if (previousAskTime != null) { - long timeRemainingUntilNextAsk = CACHE_DURATION.getSeconds() + long timeRemainingUntilNextAsk = COMMAND_COOLDOWN.getSeconds() - Duration.between(previousAskTime, Instant.now()).get(ChronoUnit.SECONDS); event @@ -57,17 +57,15 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { return; } - if (event.getInteraction().getOptions().isEmpty()) { - TextInput body = TextInput - .create(QUESTION_INPUT, "Ask ChatGPT a question or get help with code", - TextInputStyle.PARAGRAPH) - .setPlaceholder("Put your question for ChatGPT here") - .setRequiredRange(MIN_MESSAGE_INPUT_LENGTH, MAX_MESSAGE_INPUT_LENGTH) - .build(); + TextInput body = TextInput + .create(QUESTION_INPUT, "Ask ChatGPT a question or get help with code", + TextInputStyle.PARAGRAPH) + .setPlaceholder("Put your question for ChatGPT here") + .setRequiredRange(MIN_MESSAGE_INPUT_LENGTH, MAX_MESSAGE_INPUT_LENGTH) + .build(); - Modal modal = Modal.create(generateComponentId(), "ChatGPT").addActionRow(body).build(); - event.replyModal(modal).queue(); - } + Modal modal = Modal.create(generateComponentId(), "ChatGPT").addActionRow(body).build(); + event.replyModal(modal).queue(); } @Override From b223792a8f6e098700830e57809cc1255fa2a879 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Thu, 6 Apr 2023 20:59:38 -0700 Subject: [PATCH 04/29] Add new catch for RuntimeException to ChatGptService.java in order to allow for correct path to indicate to users that something went wrong. --- .../togetherjava/tjbot/features/chaptgpt/ChatGptService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java index 55c6eeb9bf..08fbe7c134 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -73,6 +73,9 @@ public Optional ask(String question) { "There was an error using the OpenAI API: {} Code: {} Type: {} Status Code: {}", openAiHttpException.getMessage(), openAiHttpException.code, openAiHttpException.type, openAiHttpException.statusCode); + } catch (RuntimeException runtimeException) { + logger.warn("There was an error using the OpenAI API: {}", + runtimeException.getMessage()); } return Optional.empty(); } From f054e20eb64b482a09d7b4691a1f39821503e2ea Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sat, 8 Apr 2023 21:04:45 -0700 Subject: [PATCH 05/29] Change Help system to also include having ChatGPT take a shot at the question. So far, all the system does is read the title and attempt to answer the question. More refinements to come. --- .../togetherjava/tjbot/features/Features.java | 4 ++-- .../features/chaptgpt/ChatGptCommand.java | 3 +-- .../features/chaptgpt/ChatGptService.java | 2 ++ .../tjbot/features/help/HelpSystemHelper.java | 22 +++++++++++++++++-- .../help/HelpThreadCreatedListener.java | 3 ++- 5 files changed, 27 insertions(+), 7 deletions(-) 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 7f1f3af046..1e50106f1e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -72,9 +72,9 @@ public static Collection createFeatures(JDA jda, Database database, Con ModerationActionsStore actionsStore = new ModerationActionsStore(database); ModAuditLogWriter modAuditLogWriter = new ModAuditLogWriter(config); ScamHistoryStore scamHistoryStore = new ScamHistoryStore(database); - HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database); - CodeMessageHandler codeMessageHandler = new CodeMessageHandler(); ChatGptService chatGptService = new ChatGptService(config); + HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database, chatGptService); + CodeMessageHandler codeMessageHandler = new CodeMessageHandler(); // NOTE The system can add special system relevant commands also by itself, // hence this list may not necessarily represent the full list of all commands actually diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index 8ec3a38471..2f824a4f7d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -78,8 +78,7 @@ public void onModalSubmitted(ModalInteractionEvent event, List args) { userIdToAskedAtCache.put(event.getMember().getId(), Instant.now()); } - String response = optional.orElse( - "An error has occurred while trying to communicate with ChatGPT. Please try again later"); + String response = optional.orElse(ChatGptService.ERROR_MESSAGE); event.getHook().sendMessage(response).queue(); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java index 08fbe7c134..77bd5a22d2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -20,6 +20,8 @@ */ public class ChatGptService { private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class); + public static final String ERROR_MESSAGE = + "An error has occurred while trying to communicate with ChatGPT. Please try again later"; private static final Duration TIMEOUT = Duration.ofSeconds(10); private static final int MAX_TOKENS = 3_000; private boolean isDisabled = false; 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 1bae9c6b1d..7353b6788c 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 @@ -24,13 +24,15 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.HelpThreads; import org.togetherjava.tjbot.db.generated.tables.records.HelpThreadsRecord; +import org.togetherjava.tjbot.features.chaptgpt.ChatGptService; import org.togetherjava.tjbot.features.utils.MessageUtils; import javax.annotation.Nullable; -import java.awt.Color; +import java.awt.*; import java.io.InputStream; import java.util.*; +import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -59,15 +61,18 @@ public final class HelpSystemHelper { private final Set threadActivityTagNames; private final String categoryRoleSuffix; private final Database database; + private final ChatGptService chatGptService; /** * Creates a new instance. * * @param config the config to use * @param database the database to store help thread metadata in + * @param chatGptService the access point to the ChatGPT API */ - public HelpSystemHelper(Config config, Database database) { + public HelpSystemHelper(Config config, Database database, ChatGptService chatGptService) { HelpSystemConfig helpConfig = config.getHelpSystem(); + this.chatGptService = chatGptService; this.database = database; helpForumPattern = helpConfig.getHelpForumPattern(); @@ -131,6 +136,19 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha return action.setEmbeds(embeds); } + public RestAction sendChatGPTAttempt(ThreadChannel threadChannel) { + Optional chatGPTAnswer = chatGptService.ask(threadChannel.getName()); + + String response = "While you are waiting, ChatGPT can give your question shot: "; + MessageCreateAction action = threadChannel.sendMessage(response); + + List embedding = List.of(HelpSystemHelper + .embedWith(chatGPTAnswer.orElse("We attempted to answer your question with ChatGPT but " + + ChatGptService.ERROR_MESSAGE))); + + return action.setEmbeds(embedding); + } + void writeHelpThreadToDatabase(long authorId, ThreadChannel threadChannel) { database.write(content -> { HelpThreadsRecord helpThreadsRecord = content.newRecord(HelpThreads.HELP_THREADS) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index f707ad5821..a1832d3ed2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -92,7 +92,8 @@ private void handleHelpThreadCreated(ThreadChannel threadChannel) { private RestAction createMessages(ThreadChannel threadChannel) { return sendHelperHeadsUp(threadChannel).flatMap(Message::pin) - .flatMap(any -> helper.sendExplanationMessage(threadChannel)); + .flatMap(any -> helper.sendExplanationMessage(threadChannel)) + .flatMap(any -> helper.sendChatGPTAttempt(threadChannel)); } private RestAction sendHelperHeadsUp(ThreadChannel threadChannel) { From 744057239743a4057ed5a83230ec4466df0e3cf9 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sat, 8 Apr 2023 23:14:56 -0700 Subject: [PATCH 06/29] Add checks to determine length of question and thus where question should come from and if it's asked at all. Code has an error on line 143 of HelpSystemHelper.java. --- .../tjbot/features/help/HelpSystemHelper.java | 26 ++++++++++++++++--- .../help/HelpThreadCreatedListener.java | 2 +- 2 files changed, 24 insertions(+), 4 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 7353b6788c..512c5b414e 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 @@ -62,6 +62,8 @@ public final class HelpSystemHelper { private final String categoryRoleSuffix; private final Database database; private final ChatGptService chatGptService; + private static final int MAX_QUESTION_LENGTH = 200; + private static final int MIN_QUESTION_LENGTH = 20; /** * Creates a new instance. @@ -136,10 +138,28 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha return action.setEmbeds(embeds); } - public RestAction sendChatGPTAttempt(ThreadChannel threadChannel) { - Optional chatGPTAnswer = chatGptService.ask(threadChannel.getName()); + public RestAction sendChatGptAttempt(ThreadChannel threadChannel) { + String questionTitle = threadChannel.getName(); + String questionFirstMessage = threadChannel.getHistoryFromBeginning(1) + .complete() + .getRetrievedHistory() + .get(0) + .getContentDisplay(); + + String question = questionFirstMessage; + if (questionFirstMessage.length() > MAX_QUESTION_LENGTH + || questionFirstMessage.length() < MIN_QUESTION_LENGTH) { + question = questionTitle; + + if (questionTitle.length() < MIN_QUESTION_LENGTH || !question.contains("?")) { + return threadChannel.sendMessage( + "Your question and title is either too short or too long to send to ChatGPT."); + } + } - String response = "While you are waiting, ChatGPT can give your question shot: "; + String response = "Here is an AI assisted attempt to answer your question, maybe it helps!" + + "In any case, a human is on the way \uD83D\uDC4D"; + Optional chatGPTAnswer = chatGptService.ask(question); MessageCreateAction action = threadChannel.sendMessage(response); List embedding = List.of(HelpSystemHelper diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index a1832d3ed2..2df3658982 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -93,7 +93,7 @@ private void handleHelpThreadCreated(ThreadChannel threadChannel) { private RestAction createMessages(ThreadChannel threadChannel) { return sendHelperHeadsUp(threadChannel).flatMap(Message::pin) .flatMap(any -> helper.sendExplanationMessage(threadChannel)) - .flatMap(any -> helper.sendChatGPTAttempt(threadChannel)); + .flatMap(any -> helper.sendChatGptAttempt(threadChannel)); } private RestAction sendHelperHeadsUp(ThreadChannel threadChannel) { From 4503f3f55771836015fd8f1c33751e0ce8a2d869 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sun, 9 Apr 2023 10:18:07 -0700 Subject: [PATCH 07/29] Fix to ensure action is completed via flatmap versus using complete() --- .../tjbot/features/help/HelpSystemHelper.java | 30 ++++++++++++------- .../help/HelpThreadCreatedListener.java | 2 +- 2 files changed, 20 insertions(+), 12 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 512c5b414e..c02b7fdd63 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 @@ -138,26 +138,34 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha return action.setEmbeds(embeds); } - public RestAction sendChatGptAttempt(ThreadChannel threadChannel) { + public RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { String questionTitle = threadChannel.getName(); - String questionFirstMessage = threadChannel.getHistoryFromBeginning(1) - .complete() - .getRetrievedHistory() - .get(0) - .getContentDisplay(); + return threadChannel.retrieveMessageById(threadChannel.getIdLong()) + .flatMap(message -> sendChatGptAttempt(message.getContentRaw(), questionTitle, + threadChannel)); + } + + public RestAction sendChatGptAttempt(String questionFirstMessage, String questionTitle, + ThreadChannel threadChannel) { - String question = questionFirstMessage; + StringBuilder sb = new StringBuilder(questionFirstMessage); + for (ForumTag tag : threadChannel.getAppliedTags()) { + sb.insert(0, String.format("%s ", tag.getName())); + } if (questionFirstMessage.length() > MAX_QUESTION_LENGTH || questionFirstMessage.length() < MIN_QUESTION_LENGTH) { - question = questionTitle; - - if (questionTitle.length() < MIN_QUESTION_LENGTH || !question.contains("?")) { + if (questionTitle.length() < MIN_QUESTION_LENGTH || !questionTitle.contains("?")) { return threadChannel.sendMessage( "Your question and title is either too short or too long to send to ChatGPT."); } + sb = new StringBuilder(questionTitle); + for (ForumTag tag : threadChannel.getAppliedTags()) { + sb.insert(0, String.format("%s ", tag.getName())); + } } - String response = "Here is an AI assisted attempt to answer your question, maybe it helps!" + String question = sb.toString(); + String response = "Here is an AI assisted attempt to answer your question, maybe it helps! " + "In any case, a human is on the way \uD83D\uDC4D"; Optional chatGPTAnswer = chatGptService.ask(question); MessageCreateAction action = threadChannel.sendMessage(response); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index 2df3658982..fb0966bf96 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -93,7 +93,7 @@ private void handleHelpThreadCreated(ThreadChannel threadChannel) { private RestAction createMessages(ThreadChannel threadChannel) { return sendHelperHeadsUp(threadChannel).flatMap(Message::pin) .flatMap(any -> helper.sendExplanationMessage(threadChannel)) - .flatMap(any -> helper.sendChatGptAttempt(threadChannel)); + .flatMap(any -> helper.prepareChatGptAttempt(threadChannel)); } private RestAction sendHelperHeadsUp(ThreadChannel threadChannel) { From 6b547dceb473551fd23e036a15300871cf6ecc67 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sun, 9 Apr 2023 18:33:47 -0700 Subject: [PATCH 08/29] Fix-up logic of code and create better output strings for UX. --- .../tjbot/features/help/HelpSystemHelper.java | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 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 c02b7fdd63..71991196ec 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 @@ -139,40 +139,48 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha } public RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { - String questionTitle = threadChannel.getName(); + return threadChannel.retrieveMessageById(threadChannel.getIdLong()) - .flatMap(message -> sendChatGptAttempt(message.getContentRaw(), questionTitle, - threadChannel)); + .flatMap(message -> sendChatGptAttempt(message.getContentRaw(), threadChannel)); } - public RestAction sendChatGptAttempt(String questionFirstMessage, String questionTitle, + public RestAction sendChatGptAttempt(String questionFirstMessage, ThreadChannel threadChannel) { + String questionTitle = threadChannel.getName(); + StringBuilder stringBuilder = new StringBuilder(questionFirstMessage); + List tags = threadChannel.getAppliedTags(); - StringBuilder sb = new StringBuilder(questionFirstMessage); - for (ForumTag tag : threadChannel.getAppliedTags()) { - sb.insert(0, String.format("%s ", tag.getName())); - } if (questionFirstMessage.length() > MAX_QUESTION_LENGTH || questionFirstMessage.length() < MIN_QUESTION_LENGTH) { if (questionTitle.length() < MIN_QUESTION_LENGTH || !questionTitle.contains("?")) { - return threadChannel.sendMessage( - "Your question and title is either too short or too long to send to ChatGPT."); - } - sb = new StringBuilder(questionTitle); - for (ForumTag tag : threadChannel.getAppliedTags()) { - sb.insert(0, String.format("%s ", tag.getName())); + return threadChannel.sendMessage(String.format( + """ + Your first message or title did not fit the format to be sent to ChatGPT. \ + Your message needs to be between %s and %s characters. If your question is longer than %s \ + then your title needs to include a question mark." + """, + MIN_QUESTION_LENGTH, MAX_QUESTION_LENGTH, MAX_QUESTION_LENGTH)); } + stringBuilder.replace(0, stringBuilder.length(), questionTitle); + } + + for (ForumTag tag : tags) { + stringBuilder.insert(0, String.format("%s ", tag.getName())); } - String question = sb.toString(); - String response = "Here is an AI assisted attempt to answer your question, maybe it helps! " - + "In any case, a human is on the way \uD83D\uDC4D"; + String question = stringBuilder.toString(); + String response = """ + Here is an AI assisted attempt to answer your question, maybe it helps! \ + It will also the tags you selected to provide more context. \ + In any case, a human is on the way \uD83D\uDC4D + """; Optional chatGPTAnswer = chatGptService.ask(question); MessageCreateAction action = threadChannel.sendMessage(response); - List embedding = List.of(HelpSystemHelper - .embedWith(chatGPTAnswer.orElse("We attempted to answer your question with ChatGPT but " - + ChatGptService.ERROR_MESSAGE))); + List embedding = List.of(HelpSystemHelper.embedWith(chatGPTAnswer.orElse(""" + An attempt was made to answer your question with ChatGPT but \ + an error has occurred while trying to communicate with ChatGPT. + """))); return action.setEmbeds(embedding); } From 9851358ca6a629a7d100f7e49d6c62a93461c601 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sun, 9 Apr 2023 18:56:03 -0700 Subject: [PATCH 09/29] Include JavaDocs for new methods. --- .../tjbot/features/help/HelpSystemHelper.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 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 71991196ec..bdcf660469 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 @@ -138,13 +138,31 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha return action.setEmbeds(embeds); } - public RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { - + /** + * Gather first message from thread and pass into asynchronous sendChatGptAttempt. + * + * @param threadChannel - The forum thread channel where the question originated + * @return A response from the AI service indicating success or failure. + * @see HelpSystemHelper#sendChatGptAttempt + */ + RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { return threadChannel.retrieveMessageById(threadChannel.getIdLong()) .flatMap(message -> sendChatGptAttempt(message.getContentRaw(), threadChannel)); } - public RestAction sendChatGptAttempt(String questionFirstMessage, + /** + * Determine between the title of the thread and the first message which to send to the AI. It + * uses a simple heuristic of length to determine if enough context exists in a question. If the + * title is used, it must also include a question mark since the title is often used more as an + * indicator of topic versus a question. + * + * @param questionFirstMessage - The first message of the thread which originates from the + * question asker. + * @param threadChannel - The thread in which the question was asked. + * @return An answer for the user from the AI service or a message indicating either an error or + * why the message wasn't used. + */ + private RestAction sendChatGptAttempt(String questionFirstMessage, ThreadChannel threadChannel) { String questionTitle = threadChannel.getName(); StringBuilder stringBuilder = new StringBuilder(questionFirstMessage); @@ -158,7 +176,7 @@ public RestAction sendChatGptAttempt(String questionFirstMessage, Your first message or title did not fit the format to be sent to ChatGPT. \ Your message needs to be between %s and %s characters. If your question is longer than %s \ then your title needs to include a question mark." - """, + """, MIN_QUESTION_LENGTH, MAX_QUESTION_LENGTH, MAX_QUESTION_LENGTH)); } stringBuilder.replace(0, stringBuilder.length(), questionTitle); From 62ca7b0f3f6cfc32c4eb3241ffd6f61d5e04fa9e Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Fri, 14 Apr 2023 14:48:30 -0700 Subject: [PATCH 10/29] Requested change to time calculation for OnSlashCommand of ChatGptCommand.java. --- .../togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java | 5 +++-- .../main/resources/db/V14__Alter_Help_Thread_Metadata.sql | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 application/src/main/resources/db/V14__Alter_Help_Thread_Metadata.sql diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index 2f824a4f7d..a0e2b93017 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -46,8 +46,9 @@ public ChatGptCommand(ChatGptService chatGptService) { public void onSlashCommand(SlashCommandInteractionEvent event) { Instant previousAskTime = userIdToAskedAtCache.getIfPresent(event.getMember().getId()); if (previousAskTime != null) { - long timeRemainingUntilNextAsk = COMMAND_COOLDOWN.getSeconds() - - Duration.between(previousAskTime, Instant.now()).get(ChronoUnit.SECONDS); + long timeRemainingUntilNextAsk = + COMMAND_COOLDOWN.minus(Duration.between(previousAskTime, Instant.now())) + .toSeconds(); event .reply("Sorry, you need to wait another " + timeRemainingUntilNextAsk diff --git a/application/src/main/resources/db/V14__Alter_Help_Thread_Metadata.sql b/application/src/main/resources/db/V14__Alter_Help_Thread_Metadata.sql new file mode 100644 index 0000000000..1e42dd7b33 --- /dev/null +++ b/application/src/main/resources/db/V14__Alter_Help_Thread_Metadata.sql @@ -0,0 +1,2 @@ +ALTER TABLE help_threads ADD active BOOLEAN; +ALTER TABLE help_threads ADD closed_at TIMESTAMP; From 9f842e4c108486bbdaec09f604a146b4e2ab0774 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Fri, 14 Apr 2023 14:52:17 -0700 Subject: [PATCH 11/29] Revert "Include JavaDocs for new methods." This reverts commit 9851358ca6a629a7d100f7e49d6c62a93461c601. --- .../tjbot/features/help/HelpSystemHelper.java | 26 +++---------------- 1 file changed, 4 insertions(+), 22 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 bdcf660469..71991196ec 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 @@ -138,31 +138,13 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha return action.setEmbeds(embeds); } - /** - * Gather first message from thread and pass into asynchronous sendChatGptAttempt. - * - * @param threadChannel - The forum thread channel where the question originated - * @return A response from the AI service indicating success or failure. - * @see HelpSystemHelper#sendChatGptAttempt - */ - RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { + public RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { + return threadChannel.retrieveMessageById(threadChannel.getIdLong()) .flatMap(message -> sendChatGptAttempt(message.getContentRaw(), threadChannel)); } - /** - * Determine between the title of the thread and the first message which to send to the AI. It - * uses a simple heuristic of length to determine if enough context exists in a question. If the - * title is used, it must also include a question mark since the title is often used more as an - * indicator of topic versus a question. - * - * @param questionFirstMessage - The first message of the thread which originates from the - * question asker. - * @param threadChannel - The thread in which the question was asked. - * @return An answer for the user from the AI service or a message indicating either an error or - * why the message wasn't used. - */ - private RestAction sendChatGptAttempt(String questionFirstMessage, + public RestAction sendChatGptAttempt(String questionFirstMessage, ThreadChannel threadChannel) { String questionTitle = threadChannel.getName(); StringBuilder stringBuilder = new StringBuilder(questionFirstMessage); @@ -176,7 +158,7 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, Your first message or title did not fit the format to be sent to ChatGPT. \ Your message needs to be between %s and %s characters. If your question is longer than %s \ then your title needs to include a question mark." - """, + """, MIN_QUESTION_LENGTH, MAX_QUESTION_LENGTH, MAX_QUESTION_LENGTH)); } stringBuilder.replace(0, stringBuilder.length(), questionTitle); From bdfb5a47f043cdf7c7f913c1529aef0d87aaad32 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Fri, 14 Apr 2023 15:17:33 -0700 Subject: [PATCH 12/29] Revert "Requested change to time calculation for OnSlashCommand of ChatGptCommand.java." This reverts commit 62ca7b0f3f6cfc32c4eb3241ffd6f61d5e04fa9e. --- .../togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java | 5 ++--- .../main/resources/db/V14__Alter_Help_Thread_Metadata.sql | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 application/src/main/resources/db/V14__Alter_Help_Thread_Metadata.sql diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index a0e2b93017..2f824a4f7d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -46,9 +46,8 @@ public ChatGptCommand(ChatGptService chatGptService) { public void onSlashCommand(SlashCommandInteractionEvent event) { Instant previousAskTime = userIdToAskedAtCache.getIfPresent(event.getMember().getId()); if (previousAskTime != null) { - long timeRemainingUntilNextAsk = - COMMAND_COOLDOWN.minus(Duration.between(previousAskTime, Instant.now())) - .toSeconds(); + long timeRemainingUntilNextAsk = COMMAND_COOLDOWN.getSeconds() + - Duration.between(previousAskTime, Instant.now()).get(ChronoUnit.SECONDS); event .reply("Sorry, you need to wait another " + timeRemainingUntilNextAsk diff --git a/application/src/main/resources/db/V14__Alter_Help_Thread_Metadata.sql b/application/src/main/resources/db/V14__Alter_Help_Thread_Metadata.sql deleted file mode 100644 index 1e42dd7b33..0000000000 --- a/application/src/main/resources/db/V14__Alter_Help_Thread_Metadata.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE help_threads ADD active BOOLEAN; -ALTER TABLE help_threads ADD closed_at TIMESTAMP; From efc92d512ddcc2d260a4cd642a9b4dcc8b2e3b54 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Fri, 14 Apr 2023 15:17:39 -0700 Subject: [PATCH 13/29] Revert "Revert "Include JavaDocs for new methods."" This reverts commit 9f842e4c108486bbdaec09f604a146b4e2ab0774. --- .../tjbot/features/help/HelpSystemHelper.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 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 71991196ec..bdcf660469 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 @@ -138,13 +138,31 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha return action.setEmbeds(embeds); } - public RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { - + /** + * Gather first message from thread and pass into asynchronous sendChatGptAttempt. + * + * @param threadChannel - The forum thread channel where the question originated + * @return A response from the AI service indicating success or failure. + * @see HelpSystemHelper#sendChatGptAttempt + */ + RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { return threadChannel.retrieveMessageById(threadChannel.getIdLong()) .flatMap(message -> sendChatGptAttempt(message.getContentRaw(), threadChannel)); } - public RestAction sendChatGptAttempt(String questionFirstMessage, + /** + * Determine between the title of the thread and the first message which to send to the AI. It + * uses a simple heuristic of length to determine if enough context exists in a question. If the + * title is used, it must also include a question mark since the title is often used more as an + * indicator of topic versus a question. + * + * @param questionFirstMessage - The first message of the thread which originates from the + * question asker. + * @param threadChannel - The thread in which the question was asked. + * @return An answer for the user from the AI service or a message indicating either an error or + * why the message wasn't used. + */ + private RestAction sendChatGptAttempt(String questionFirstMessage, ThreadChannel threadChannel) { String questionTitle = threadChannel.getName(); StringBuilder stringBuilder = new StringBuilder(questionFirstMessage); @@ -158,7 +176,7 @@ public RestAction sendChatGptAttempt(String questionFirstMessage, Your first message or title did not fit the format to be sent to ChatGPT. \ Your message needs to be between %s and %s characters. If your question is longer than %s \ then your title needs to include a question mark." - """, + """, MIN_QUESTION_LENGTH, MAX_QUESTION_LENGTH, MAX_QUESTION_LENGTH)); } stringBuilder.replace(0, stringBuilder.length(), questionTitle); From b2c77103159b7373077821316dfe6ec42aaaeae8 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Mon, 17 Apr 2023 13:39:27 -0700 Subject: [PATCH 14/29] Requested changes to Help thread AI generated response code. --- .../features/chaptgpt/ChatGptCommand.java | 3 +- .../tjbot/features/help/HelpSystemHelper.java | 37 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index e53a34391d..f87d0b78a3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -22,6 +22,7 @@ * which it will respond with an AI generated answer. */ public final class ChatGptCommand extends SlashCommandAdapter { + public static final String COMMAND_NAME = "chatgpt"; private static final String QUESTION_INPUT = "question"; private static final int MAX_MESSAGE_INPUT_LENGTH = 200; private static final int MIN_MESSAGE_INPUT_LENGTH = 4; @@ -37,7 +38,7 @@ public final class ChatGptCommand extends SlashCommandAdapter { * @param chatGptService ChatGptService - Needed to make calls to ChatGPT API */ public ChatGptCommand(ChatGptService chatGptService) { - super("chatgpt", "Ask the ChatGPT AI a question!", CommandVisibility.GUILD); + super(COMMAND_NAME, "Ask the ChatGPT AI a question!", CommandVisibility.GUILD); this.chatGptService = chatGptService; } 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 bdcf660469..26044a560c 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 @@ -24,22 +24,26 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.HelpThreads; import org.togetherjava.tjbot.db.generated.tables.records.HelpThreadsRecord; +import org.togetherjava.tjbot.features.chaptgpt.ChatGptCommand; import org.togetherjava.tjbot.features.chaptgpt.ChatGptService; import org.togetherjava.tjbot.features.utils.MessageUtils; import javax.annotation.Nullable; -import java.awt.*; +import java.awt.Color; import java.io.InputStream; import java.util.*; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; +import static org.togetherjava.tjbot.features.utils.MessageUtils.mentionGuildSlashCommand; + /** * Helper class offering certain methods used by the help system. */ @@ -175,7 +179,7 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, """ Your first message or title did not fit the format to be sent to ChatGPT. \ Your message needs to be between %s and %s characters. If your question is longer than %s \ - then your title needs to include a question mark." + then your title needs to be in the form of a question and include a question mark." """, MIN_QUESTION_LENGTH, MAX_QUESTION_LENGTH, MAX_QUESTION_LENGTH)); } @@ -187,20 +191,27 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, } String question = stringBuilder.toString(); - String response = """ - Here is an AI assisted attempt to answer your question, maybe it helps! \ - It will also the tags you selected to provide more context. \ - In any case, a human is on the way \uD83D\uDC4D - """; Optional chatGPTAnswer = chatGptService.ask(question); - MessageCreateAction action = threadChannel.sendMessage(response); - List embedding = List.of(HelpSystemHelper.embedWith(chatGPTAnswer.orElse(""" - An attempt was made to answer your question with ChatGPT but \ - an error has occurred while trying to communicate with ChatGPT. - """))); + if (chatGPTAnswer.isPresent()) { + UnaryOperator response = + """ + Above is an AI assisted attempt to answer your question, maybe it helps! \ + In any case, a human is on the way 👍. To continue talking to the AI, you can use \ + %s. + """::formatted; + + mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) + .map(response) + .flatMap(threadChannel::sendMessage) + .queue(); + + return threadChannel.sendMessage(chatGPTAnswer.get()); + } - return action.setEmbeds(embedding); + logger.warn("Something went wrong while trying to communicate with the AI API."); + throw new IllegalStateException( + "Something went wrong while trying to communicate with the AI API."); } void writeHelpThreadToDatabase(long authorId, ThreadChannel threadChannel) { From ff775beac51463b0facb029dc81d83cec77cf597 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Tue, 18 Apr 2023 10:05:54 -0700 Subject: [PATCH 15/29] Fix AI response in Discord thread to be explanation message followed by embed. --- .../tjbot/features/help/HelpSystemHelper.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 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 26044a560c..7410ba8a85 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 @@ -196,17 +196,16 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, if (chatGPTAnswer.isPresent()) { UnaryOperator response = """ - Above is an AI assisted attempt to answer your question, maybe it helps! \ + Here is an AI assisted attempt to answer your question, maybe it helps! \ In any case, a human is on the way 👍. To continue talking to the AI, you can use \ %s. """::formatted; - mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) + return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) .map(response) .flatMap(threadChannel::sendMessage) - .queue(); - - return threadChannel.sendMessage(chatGPTAnswer.get()); + .flatMap(embed -> threadChannel + .sendMessageEmbeds(HelpSystemHelper.embedWith(chatGPTAnswer.get()))); } logger.warn("Something went wrong while trying to communicate with the AI API."); From c3803ff7d62463a670349ed7b65851f2632ebf58 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Thu, 20 Apr 2023 12:07:06 -0700 Subject: [PATCH 16/29] - Added more context for questions. Either use both the title and message if possible OR use the title and a shortened version of the message. Appending tags still possible. - Increase to ChatGptService timeout time. Otherwise, responses would just fail for even slightly complex questions. Also change temperature, which should create more strictly technical answers. - Tags are only appended if the question is still less than the maximum length. - Also included info log test to determine what the final question asked is. - Changed thrown error to mentioning slash command to users instead of silently failing. - Minor string edits. --- .../features/chaptgpt/ChatGptService.java | 6 +-- .../tjbot/features/help/HelpSystemHelper.java | 53 ++++++++++++------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java index 77bd5a22d2..84cffb0f0a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -20,9 +20,7 @@ */ public class ChatGptService { private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class); - public static final String ERROR_MESSAGE = - "An error has occurred while trying to communicate with ChatGPT. Please try again later"; - private static final Duration TIMEOUT = Duration.ofSeconds(10); + private static final Duration TIMEOUT = Duration.ofSeconds(20); private static final int MAX_TOKENS = 3_000; private boolean isDisabled = false; private final OpenAiService openAiService; @@ -61,7 +59,7 @@ public Optional ask(String question) { .model("gpt-3.5-turbo") .messages(List.of(chatMessage)) .frequencyPenalty(0.5) - .temperature(0.7) + .temperature(0.3) .maxTokens(MAX_TOKENS) .n(1) .build(); 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 7410ba8a85..9eab8722ca 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 @@ -67,7 +67,7 @@ public final class HelpSystemHelper { private final Database database; private final ChatGptService chatGptService; private static final int MAX_QUESTION_LENGTH = 200; - private static final int MIN_QUESTION_LENGTH = 20; + private static final int MIN_QUESTION_LENGTH = 10; /** * Creates a new instance. @@ -168,35 +168,49 @@ RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { */ private RestAction sendChatGptAttempt(String questionFirstMessage, ThreadChannel threadChannel) { + // Will be at most 100 characters. String questionTitle = threadChannel.getName(); - StringBuilder stringBuilder = new StringBuilder(questionFirstMessage); - List tags = threadChannel.getAppliedTags(); + StringBuilder stringBuilder = new StringBuilder(); - if (questionFirstMessage.length() > MAX_QUESTION_LENGTH - || questionFirstMessage.length() < MIN_QUESTION_LENGTH) { - if (questionTitle.length() < MIN_QUESTION_LENGTH || !questionTitle.contains("?")) { - return threadChannel.sendMessage(String.format( - """ - Your first message or title did not fit the format to be sent to ChatGPT. \ - Your message needs to be between %s and %s characters. If your question is longer than %s \ - then your title needs to be in the form of a question and include a question mark." - """, - MIN_QUESTION_LENGTH, MAX_QUESTION_LENGTH, MAX_QUESTION_LENGTH)); - } - stringBuilder.replace(0, stringBuilder.length(), questionTitle); + if (questionFirstMessage.length() < MIN_QUESTION_LENGTH + && (questionTitle.length() < MIN_QUESTION_LENGTH || !questionTitle.contains("?"))) { + return threadChannel.sendMessage(String.format( + """ + Your first message or title did not fit the format to be sent to ChatGPT. \ + Your message needs to be between %s and %s characters. If your question is longer than %s \ + then your title needs to be in the form of a question and include a question mark. + """, + MIN_QUESTION_LENGTH, MAX_QUESTION_LENGTH, MAX_QUESTION_LENGTH)); + } + + if (questionTitle.length() + questionFirstMessage.length() > MAX_QUESTION_LENGTH) { + questionFirstMessage = + questionFirstMessage.substring(0, MAX_QUESTION_LENGTH - questionTitle.length()); } - for (ForumTag tag : tags) { + stringBuilder.append(questionTitle).append(" ").append(questionFirstMessage); + + for (ForumTag tag : threadChannel.getAppliedTags()) { + if (stringBuilder.length() > MAX_QUESTION_LENGTH) { + break; + } stringBuilder.insert(0, String.format("%s ", tag.getName())); } String question = stringBuilder.toString(); + logger.info("The final question sent to chatGPT: {}", question); Optional chatGPTAnswer = chatGptService.ask(question); if (chatGPTAnswer.isPresent()) { + String aiResponse = chatGPTAnswer.get(); + if (aiResponse.matches("[sS]orry") && aiResponse.contains("As an AI language model")) { + logger.info("Response from ChatGPT, lacks context or unable to respond?: {}", + aiResponse); + } + UnaryOperator response = """ - Here is an AI assisted attempt to answer your question, maybe it helps! \ + Here is an AI assisted attempt to answer your question 🤖. Maybe it helps! \ In any case, a human is on the way 👍. To continue talking to the AI, you can use \ %s. """::formatted; @@ -209,8 +223,9 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, } logger.warn("Something went wrong while trying to communicate with the AI API."); - throw new IllegalStateException( - "Something went wrong while trying to communicate with the AI API."); + return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME).map( + "You can use %s to ask ChatGPT about your question while you wait for a member to respond."::formatted) + .flatMap(threadChannel::sendMessage); } void writeHelpThreadToDatabase(long authorId, ThreadChannel threadChannel) { From b8bbcf0d3bec147db677273888c08183c5f61d41 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Thu, 20 Apr 2023 14:32:23 -0700 Subject: [PATCH 17/29] - Fix statement to capture non-helpful AI responses. - Create method to return information on /chatgpt, used if no useful response or error from AI. - Improve substring clipping to include space for question construction. --- .../tjbot/features/help/HelpSystemHelper.java | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 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 9eab8722ca..3fcef257d3 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 @@ -33,7 +33,6 @@ import java.awt.Color; import java.io.InputStream; import java.util.*; -import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -68,6 +67,8 @@ public final class HelpSystemHelper { private final ChatGptService chatGptService; private static final int MAX_QUESTION_LENGTH = 200; private static final int MIN_QUESTION_LENGTH = 10; + private static final String CHATGPT_FAILURE_MESSAGE = + "You can use %s to ask ChatGPT about your question while you wait for a member to respond."; /** * Creates a new instance. @@ -174,22 +175,16 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, if (questionFirstMessage.length() < MIN_QUESTION_LENGTH && (questionTitle.length() < MIN_QUESTION_LENGTH || !questionTitle.contains("?"))) { - return threadChannel.sendMessage(String.format( - """ - Your first message or title did not fit the format to be sent to ChatGPT. \ - Your message needs to be between %s and %s characters. If your question is longer than %s \ - then your title needs to be in the form of a question and include a question mark. - """, - MIN_QUESTION_LENGTH, MAX_QUESTION_LENGTH, MAX_QUESTION_LENGTH)); + return sendChatGptFallbackMessage(threadChannel); } if (questionTitle.length() + questionFirstMessage.length() > MAX_QUESTION_LENGTH) { - questionFirstMessage = - questionFirstMessage.substring(0, MAX_QUESTION_LENGTH - questionTitle.length()); + // - 1 for the space that will be added afterward. + questionFirstMessage = questionFirstMessage.substring(0, + (MAX_QUESTION_LENGTH - 1) - questionTitle.length()); } stringBuilder.append(questionTitle).append(" ").append(questionFirstMessage); - for (ForumTag tag : threadChannel.getAppliedTags()) { if (stringBuilder.length() > MAX_QUESTION_LENGTH) { break; @@ -198,14 +193,16 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, } String question = stringBuilder.toString(); - logger.info("The final question sent to chatGPT: {}", question); + logger.debug("The final question sent to chatGPT: {}", question); Optional chatGPTAnswer = chatGptService.ask(question); if (chatGPTAnswer.isPresent()) { String aiResponse = chatGPTAnswer.get(); - if (aiResponse.matches("[sS]orry") && aiResponse.contains("As an AI language model")) { - logger.info("Response from ChatGPT, lacks context or unable to respond?: {}", + if (aiResponse.matches(".*[sS]orry.*") + || aiResponse.matches(".*[Aa]s an AI language model.*")) { + logger.debug("Response from ChatGPT, lacks context or unable to respond?: {}", aiResponse); + return sendChatGptFallbackMessage(threadChannel); } UnaryOperator response = @@ -219,12 +216,16 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, .map(response) .flatMap(threadChannel::sendMessage) .flatMap(embed -> threadChannel - .sendMessageEmbeds(HelpSystemHelper.embedWith(chatGPTAnswer.get()))); + .sendMessageEmbeds(HelpSystemHelper.embedWith(aiResponse))); } - logger.warn("Something went wrong while trying to communicate with the AI API."); - return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME).map( - "You can use %s to ask ChatGPT about your question while you wait for a member to respond."::formatted) + logger.warn("Something went wrong while trying to communicate with the ChatGpt API."); + return sendChatGptFallbackMessage(threadChannel); + } + + private RestAction sendChatGptFallbackMessage(ThreadChannel threadChannel) { + return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) + .map(CHATGPT_FAILURE_MESSAGE::formatted) .flatMap(threadChannel::sendMessage); } From a5113e9abf8b980263ca98848ae8afe5eab4d2b3 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Thu, 20 Apr 2023 15:16:00 -0700 Subject: [PATCH 18/29] - Remove use of regex in order to avoid long running task. Replace by checking if string contains desired sequence. --- .../togetherjava/tjbot/features/help/HelpSystemHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 3fcef257d3..898131bd9a 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 @@ -198,8 +198,8 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, if (chatGPTAnswer.isPresent()) { String aiResponse = chatGPTAnswer.get(); - if (aiResponse.matches(".*[sS]orry.*") - || aiResponse.matches(".*[Aa]s an AI language model.*")) { + String lowercaseAiResponse = aiResponse.toLowerCase(); + if (lowercaseAiResponse.contains("as an ai language model")) { logger.debug("Response from ChatGPT, lacks context or unable to respond?: {}", aiResponse); return sendChatGptFallbackMessage(threadChannel); From 884cbd6b2c98005ed043941cf860908957c61b34 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Fri, 21 Apr 2023 09:54:21 -0700 Subject: [PATCH 19/29] - Refactor on method names and structure of calling methods. - Remove need for question mark in title requirement. --- .../tjbot/features/help/HelpSystemHelper.java | 24 +++++-------------- .../help/HelpThreadCreatedListener.java | 4 +++- 2 files changed, 9 insertions(+), 19 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 898131bd9a..fcc473456b 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 @@ -143,18 +143,6 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha return action.setEmbeds(embeds); } - /** - * Gather first message from thread and pass into asynchronous sendChatGptAttempt. - * - * @param threadChannel - The forum thread channel where the question originated - * @return A response from the AI service indicating success or failure. - * @see HelpSystemHelper#sendChatGptAttempt - */ - RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { - return threadChannel.retrieveMessageById(threadChannel.getIdLong()) - .flatMap(message -> sendChatGptAttempt(message.getContentRaw(), threadChannel)); - } - /** * Determine between the title of the thread and the first message which to send to the AI. It * uses a simple heuristic of length to determine if enough context exists in a question. If the @@ -167,15 +155,15 @@ RestAction prepareChatGptAttempt(ThreadChannel threadChannel) { * @return An answer for the user from the AI service or a message indicating either an error or * why the message wasn't used. */ - private RestAction sendChatGptAttempt(String questionFirstMessage, + RestAction constructChatGptAttempt(String questionFirstMessage, ThreadChannel threadChannel) { // Will be at most 100 characters. String questionTitle = threadChannel.getName(); StringBuilder stringBuilder = new StringBuilder(); if (questionFirstMessage.length() < MIN_QUESTION_LENGTH - && (questionTitle.length() < MIN_QUESTION_LENGTH || !questionTitle.contains("?"))) { - return sendChatGptFallbackMessage(threadChannel); + && questionTitle.length() < MIN_QUESTION_LENGTH) { + return useChatGptFallbackMessage(threadChannel); } if (questionTitle.length() + questionFirstMessage.length() > MAX_QUESTION_LENGTH) { @@ -202,7 +190,7 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, if (lowercaseAiResponse.contains("as an ai language model")) { logger.debug("Response from ChatGPT, lacks context or unable to respond?: {}", aiResponse); - return sendChatGptFallbackMessage(threadChannel); + return useChatGptFallbackMessage(threadChannel); } UnaryOperator response = @@ -220,10 +208,10 @@ private RestAction sendChatGptAttempt(String questionFirstMessage, } logger.warn("Something went wrong while trying to communicate with the ChatGpt API."); - return sendChatGptFallbackMessage(threadChannel); + return useChatGptFallbackMessage(threadChannel); } - private RestAction sendChatGptFallbackMessage(ThreadChannel threadChannel) { + private RestAction useChatGptFallbackMessage(ThreadChannel threadChannel) { return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) .map(CHATGPT_FAILURE_MESSAGE::formatted) .flatMap(threadChannel::sendMessage); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index fb0966bf96..6d11b4a727 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -93,7 +93,9 @@ private void handleHelpThreadCreated(ThreadChannel threadChannel) { private RestAction createMessages(ThreadChannel threadChannel) { return sendHelperHeadsUp(threadChannel).flatMap(Message::pin) .flatMap(any -> helper.sendExplanationMessage(threadChannel)) - .flatMap(any -> helper.prepareChatGptAttempt(threadChannel)); + .flatMap(any -> threadChannel.retrieveMessageById(threadChannel.getIdLong()) + .flatMap(message -> helper.constructChatGptAttempt(message.getContentRaw(), + threadChannel))); } private RestAction sendHelperHeadsUp(ThreadChannel threadChannel) { From c8327218758303aa0f68b32277478ce6c7a9e529 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sat, 22 Apr 2023 16:08:56 -0700 Subject: [PATCH 20/29] Requested refactor of constructChatGptAttempt(): - Pull out question preparation code into own function - Use orElseThrow() instead of get() - Use toLowerCase(Locale.US) - Update StringBuilder variable name to questionBuilder. - Remove potentially erroneous comment --- .../tjbot/features/help/HelpSystemHelper.java | 72 +++++++++++-------- .../help/HelpThreadCreatedListener.java | 4 +- 2 files changed, 43 insertions(+), 33 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 fcc473456b..af4957c9d4 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 @@ -155,38 +155,20 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha * @return An answer for the user from the AI service or a message indicating either an error or * why the message wasn't used. */ - RestAction constructChatGptAttempt(String questionFirstMessage, - ThreadChannel threadChannel) { - // Will be at most 100 characters. - String questionTitle = threadChannel.getName(); - StringBuilder stringBuilder = new StringBuilder(); - - if (questionFirstMessage.length() < MIN_QUESTION_LENGTH - && questionTitle.length() < MIN_QUESTION_LENGTH) { - return useChatGptFallbackMessage(threadChannel); - } - - if (questionTitle.length() + questionFirstMessage.length() > MAX_QUESTION_LENGTH) { - // - 1 for the space that will be added afterward. - questionFirstMessage = questionFirstMessage.substring(0, - (MAX_QUESTION_LENGTH - 1) - questionTitle.length()); - } + RestAction constructChatGptAttempt(ThreadChannel threadChannel, + String questionFirstMessage) { - stringBuilder.append(questionTitle).append(" ").append(questionFirstMessage); - for (ForumTag tag : threadChannel.getAppliedTags()) { - if (stringBuilder.length() > MAX_QUESTION_LENGTH) { - break; - } - stringBuilder.insert(0, String.format("%s ", tag.getName())); - } + Optional questionOptional = + prepareChatGptQuestion(threadChannel, questionFirstMessage); + Optional chatGPTAnswer; - String question = stringBuilder.toString(); - logger.debug("The final question sent to chatGPT: {}", question); - Optional chatGPTAnswer = chatGptService.ask(question); + try { + String question = questionOptional.orElseThrow(); + logger.debug("The final question sent to chatGPT: {}", question); + chatGPTAnswer = chatGptService.ask(question); - if (chatGPTAnswer.isPresent()) { - String aiResponse = chatGPTAnswer.get(); - String lowercaseAiResponse = aiResponse.toLowerCase(); + String aiResponse = chatGPTAnswer.orElseThrow(); + String lowercaseAiResponse = aiResponse.toLowerCase(Locale.US); if (lowercaseAiResponse.contains("as an ai language model")) { logger.debug("Response from ChatGPT, lacks context or unable to respond?: {}", aiResponse); @@ -205,10 +187,38 @@ RestAction constructChatGptAttempt(String questionFirstMessage, .flatMap(threadChannel::sendMessage) .flatMap(embed -> threadChannel .sendMessageEmbeds(HelpSystemHelper.embedWith(aiResponse))); + } catch (NoSuchElementException e) { + logger.warn("Something went wrong while trying to communicate with the ChatGpt API: {}", + e.getMessage()); + return useChatGptFallbackMessage(threadChannel); + } + } + + private Optional prepareChatGptQuestion(ThreadChannel threadChannel, + String questionFirstMessage) { + String questionTitle = threadChannel.getName(); + StringBuilder questionBuilder = new StringBuilder(MAX_QUESTION_LENGTH); + + if (questionFirstMessage.length() < MIN_QUESTION_LENGTH + && questionTitle.length() < MIN_QUESTION_LENGTH) { + return Optional.empty(); + } + + if (questionTitle.length() + questionFirstMessage.length() > MAX_QUESTION_LENGTH) { + // - 1 for the space that will be added afterward. + questionFirstMessage = questionFirstMessage.substring(0, + (MAX_QUESTION_LENGTH - 1) - questionTitle.length()); + } + + questionBuilder.append(questionTitle).append(" ").append(questionFirstMessage); + for (ForumTag tag : threadChannel.getAppliedTags()) { + if (questionBuilder.length() > MAX_QUESTION_LENGTH) { + break; + } + questionBuilder.insert(0, String.format("%s ", tag.getName())); } - logger.warn("Something went wrong while trying to communicate with the ChatGpt API."); - return useChatGptFallbackMessage(threadChannel); + return Optional.of(questionBuilder.toString()); } private RestAction useChatGptFallbackMessage(ThreadChannel threadChannel) { diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index 6d11b4a727..f58031d98b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -94,8 +94,8 @@ private RestAction createMessages(ThreadChannel threadChannel) { return sendHelperHeadsUp(threadChannel).flatMap(Message::pin) .flatMap(any -> helper.sendExplanationMessage(threadChannel)) .flatMap(any -> threadChannel.retrieveMessageById(threadChannel.getIdLong()) - .flatMap(message -> helper.constructChatGptAttempt(message.getContentRaw(), - threadChannel))); + .flatMap(message -> helper.constructChatGptAttempt(threadChannel, + message.getContentRaw()))); } private RestAction sendHelperHeadsUp(ThreadChannel threadChannel) { From ec10fe174fc74daecf322bc3be5edcdc4ecc8f25 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Fri, 28 Apr 2023 11:09:16 -0700 Subject: [PATCH 21/29] Some of the requested refactor of constructChatGptAttempt(): - Change from 'member' to 'human' - Refactor 'questionFirstMessage' to 'originalQuestion' - Refactor code for creating question to improve readability in case of adding question text versus remaining characters for question. - Include new tagBuilder StringBuilder to decrease insert calls down to one. - Remove throw block which housed logic. --- .../tjbot/features/help/HelpSystemHelper.java | 93 ++++++++++--------- 1 file changed, 49 insertions(+), 44 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 af4957c9d4..21a97951bd 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 @@ -30,9 +30,10 @@ import javax.annotation.Nullable; -import java.awt.Color; +import java.awt.*; import java.io.InputStream; import java.util.*; +import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -68,7 +69,7 @@ public final class HelpSystemHelper { private static final int MAX_QUESTION_LENGTH = 200; private static final int MIN_QUESTION_LENGTH = 10; private static final String CHATGPT_FAILURE_MESSAGE = - "You can use %s to ask ChatGPT about your question while you wait for a member to respond."; + "You can use %s to ask ChatGPT about your question while you wait for a human to respond."; /** * Creates a new instance. @@ -149,75 +150,79 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha * title is used, it must also include a question mark since the title is often used more as an * indicator of topic versus a question. * - * @param questionFirstMessage - The first message of the thread which originates from the - * question asker. - * @param threadChannel - The thread in which the question was asked. + * @param originalQuestion The first message of the thread which originates from the question + * asker. + * @param threadChannel The thread in which the question was asked. * @return An answer for the user from the AI service or a message indicating either an error or * why the message wasn't used. */ RestAction constructChatGptAttempt(ThreadChannel threadChannel, - String questionFirstMessage) { + String originalQuestion) { - Optional questionOptional = - prepareChatGptQuestion(threadChannel, questionFirstMessage); + Optional questionOptional = prepareChatGptQuestion(threadChannel, originalQuestion); Optional chatGPTAnswer; - try { - String question = questionOptional.orElseThrow(); - logger.debug("The final question sent to chatGPT: {}", question); - chatGPTAnswer = chatGptService.ask(question); - - String aiResponse = chatGPTAnswer.orElseThrow(); - String lowercaseAiResponse = aiResponse.toLowerCase(Locale.US); - if (lowercaseAiResponse.contains("as an ai language model")) { - logger.debug("Response from ChatGPT, lacks context or unable to respond?: {}", - aiResponse); - return useChatGptFallbackMessage(threadChannel); - } + String question = questionOptional.orElseThrow(); + logger.debug("The final question sent to chatGPT: {}", question); + chatGPTAnswer = chatGptService.ask(question); - UnaryOperator response = - """ - Here is an AI assisted attempt to answer your question 🤖. Maybe it helps! \ - In any case, a human is on the way 👍. To continue talking to the AI, you can use \ - %s. - """::formatted; - - return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) - .map(response) - .flatMap(threadChannel::sendMessage) - .flatMap(embed -> threadChannel - .sendMessageEmbeds(HelpSystemHelper.embedWith(aiResponse))); - } catch (NoSuchElementException e) { - logger.warn("Something went wrong while trying to communicate with the ChatGpt API: {}", - e.getMessage()); + if (chatGPTAnswer.isEmpty()) { + logger.warn("Something went wrong while trying to communicate with the ChatGpt API"); return useChatGptFallbackMessage(threadChannel); } + + String aiResponse = chatGPTAnswer.get(); + String lowercaseAiResponse = aiResponse.toLowerCase(Locale.US); + if (lowercaseAiResponse.contains("as an ai language model")) { + logger.debug("Response from ChatGPT, lacks context or unable to respond?: {}", + aiResponse); + return useChatGptFallbackMessage(threadChannel); + } + + UnaryOperator response = """ + Here is an AI assisted attempt to answer your question 🤖. Maybe it helps! \ + In any case, a human is on the way 👍. To continue talking to the AI, you can use \ + %s. + """::formatted; + + return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) + .map(response) + .flatMap(threadChannel::sendMessage) + .flatMap(embed -> threadChannel + .sendMessageEmbeds(HelpSystemHelper.embedWith(aiResponse))); } private Optional prepareChatGptQuestion(ThreadChannel threadChannel, - String questionFirstMessage) { + String originalQuestion) { String questionTitle = threadChannel.getName(); StringBuilder questionBuilder = new StringBuilder(MAX_QUESTION_LENGTH); - if (questionFirstMessage.length() < MIN_QUESTION_LENGTH + if (originalQuestion.length() < MIN_QUESTION_LENGTH && questionTitle.length() < MIN_QUESTION_LENGTH) { return Optional.empty(); } - if (questionTitle.length() + questionFirstMessage.length() > MAX_QUESTION_LENGTH) { - // - 1 for the space that will be added afterward. - questionFirstMessage = questionFirstMessage.substring(0, - (MAX_QUESTION_LENGTH - 1) - questionTitle.length()); + questionBuilder.append(questionTitle).append(" "); + if (originalQuestion.length() > MAX_QUESTION_LENGTH - questionBuilder.length()) { + originalQuestion = + originalQuestion.substring(0, MAX_QUESTION_LENGTH - questionBuilder.length()); } - questionBuilder.append(questionTitle).append(" ").append(questionFirstMessage); + questionBuilder.append(originalQuestion); + + StringBuilder tagBuilder = new StringBuilder(); + int stringLength = questionBuilder.length(); for (ForumTag tag : threadChannel.getAppliedTags()) { - if (questionBuilder.length() > MAX_QUESTION_LENGTH) { + String tagName = tag.getName(); + stringLength += tagName.length(); + if (stringLength > MAX_QUESTION_LENGTH) { break; } - questionBuilder.insert(0, String.format("%s ", tag.getName())); + tagBuilder.append(String.format("%s ", tagName)); } + questionBuilder.insert(0, tagBuilder); + return Optional.of(questionBuilder.toString()); } From 881bfd8e6692dff527ad5d43a8bc038b8594ff72 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Tue, 9 May 2023 16:06:26 -0700 Subject: [PATCH 22/29] - Change loop to stream for better readability - Limit length of question string moved to creation of question string - Remove test for bad AI answer (produces false negatives) --- .../tjbot/features/help/HelpSystemHelper.java | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 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 21a97951bd..1d210b500f 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 @@ -163,7 +163,7 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, Optional chatGPTAnswer; String question = questionOptional.orElseThrow(); - logger.debug("The final question sent to chatGPT: {}", question); + logger.info("The final question sent to chatGPT: {}", question); chatGPTAnswer = chatGptService.ask(question); if (chatGPTAnswer.isEmpty()) { @@ -171,14 +171,6 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, return useChatGptFallbackMessage(threadChannel); } - String aiResponse = chatGPTAnswer.get(); - String lowercaseAiResponse = aiResponse.toLowerCase(Locale.US); - if (lowercaseAiResponse.contains("as an ai language model")) { - logger.debug("Response from ChatGPT, lacks context or unable to respond?: {}", - aiResponse); - return useChatGptFallbackMessage(threadChannel); - } - UnaryOperator response = """ Here is an AI assisted attempt to answer your question 🤖. Maybe it helps! \ In any case, a human is on the way 👍. To continue talking to the AI, you can use \ @@ -189,7 +181,7 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, .map(response) .flatMap(threadChannel::sendMessage) .flatMap(embed -> threadChannel - .sendMessageEmbeds(HelpSystemHelper.embedWith(aiResponse))); + .sendMessageEmbeds(HelpSystemHelper.embedWith(chatGPTAnswer.get()))); } private Optional prepareChatGptQuestion(ThreadChannel threadChannel, @@ -210,20 +202,19 @@ private Optional prepareChatGptQuestion(ThreadChannel threadChannel, questionBuilder.append(originalQuestion); - StringBuilder tagBuilder = new StringBuilder(); - int stringLength = questionBuilder.length(); - for (ForumTag tag : threadChannel.getAppliedTags()) { - String tagName = tag.getName(); - stringLength += tagName.length(); - if (stringLength > MAX_QUESTION_LENGTH) { - break; - } - tagBuilder.append(String.format("%s ", tagName)); - } + String tagsJoined = threadChannel.getAppliedTags() + .stream() + .map(ForumTag::getName) + .collect(Collectors.joining(" ")) + .concat(" "); - questionBuilder.insert(0, tagBuilder); + String question = questionBuilder.insert(0, tagsJoined) + .toString() + .lines() + .limit(MAX_QUESTION_LENGTH) + .collect(Collectors.joining()); - return Optional.of(questionBuilder.toString()); + return Optional.of(question); } private RestAction useChatGptFallbackMessage(ThreadChannel threadChannel) { From aca111fafcf6dcb9afd04fae2424340b12645e15 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Thu, 18 May 2023 10:25:48 -0700 Subject: [PATCH 23/29] - Reimplement loops for building question. - Remove * imports --- .../tjbot/features/help/HelpSystemHelper.java | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 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 1d210b500f..23c5312cd4 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 @@ -32,8 +32,15 @@ import java.awt.*; import java.io.InputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -163,7 +170,7 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, Optional chatGPTAnswer; String question = questionOptional.orElseThrow(); - logger.info("The final question sent to chatGPT: {}", question); + logger.debug("The final question sent to chatGPT: {}", question); chatGPTAnswer = chatGptService.ask(question); if (chatGPTAnswer.isEmpty()) { @@ -202,19 +209,20 @@ private Optional prepareChatGptQuestion(ThreadChannel threadChannel, questionBuilder.append(originalQuestion); - String tagsJoined = threadChannel.getAppliedTags() - .stream() - .map(ForumTag::getName) - .collect(Collectors.joining(" ")) - .concat(" "); + StringBuilder tagBuilder = new StringBuilder(); + int stringLength = questionBuilder.length(); + for (ForumTag tag : threadChannel.getAppliedTags()) { + String tagName = tag.getName(); + stringLength += tagName.length(); + if (stringLength > MAX_QUESTION_LENGTH) { + break; + } + tagBuilder.append(String.format("%s ", tagName)); + } - String question = questionBuilder.insert(0, tagsJoined) - .toString() - .lines() - .limit(MAX_QUESTION_LENGTH) - .collect(Collectors.joining()); + questionBuilder.insert(0, tagBuilder); - return Optional.of(question); + return Optional.of(questionBuilder.toString()); } private RestAction useChatGptFallbackMessage(ThreadChannel threadChannel) { From f8d25a44d62e7e362c8d60c668e9f6c2599ddeae Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sun, 21 May 2023 13:43:24 -0700 Subject: [PATCH 24/29] ChatGptService - Increase of timeout time to two minutes. - Initialize and send setup message to AI per query to restrict answers to limit. ChatGptServiceTest - Include new testing suite. - Include tests to check response time and functioning of error detection. - Test for length of response from AI to be less than 2000 characters. ChatGptCommand - Creation of local variables for better flexibility and readability. HelpSystemHelper - Rename variable to better describe use. --- .../features/chaptgpt/ChatGptCommand.java | 8 +- .../features/chaptgpt/ChatGptService.java | 9 +- .../tjbot/features/help/HelpSystemHelper.java | 4 +- .../features/chatgpt/ChatGptServiceTest.java | 149 ++++++++++++++++++ 4 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index f87d0b78a3..2eb1465cff 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -80,8 +80,12 @@ public void onModalSubmitted(ModalInteractionEvent event, List args) { userIdToAskedAtCache.put(event.getMember().getId(), Instant.now()); } - String response = optional.orElse( - "An error has occurred while trying to communicate with ChatGPT. Please try again later"); + String errorResponse = """ + An error has occurred while trying to communicate with ChatGPT. + Please try again later. + """; + + String response = optional.orElse(errorResponse); event.getHook().sendMessage(response).queue(); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java index 84cffb0f0a..33f5c21e1f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -20,7 +20,7 @@ */ public class ChatGptService { private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class); - private static final Duration TIMEOUT = Duration.ofSeconds(20); + private static final Duration TIMEOUT = Duration.ofSeconds(120); private static final int MAX_TOKENS = 3_000; private boolean isDisabled = false; private final OpenAiService openAiService; @@ -52,17 +52,22 @@ public Optional ask(String question) { return Optional.empty(); } + ChatMessage setUpMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), """ + Please answer questions in 1750 characters. + Please note if your answer needs more room. + """); try { ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), Objects.requireNonNull(question)); ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() .model("gpt-3.5-turbo") - .messages(List.of(chatMessage)) + .messages(List.of(setUpMessage, chatMessage)) .frequencyPenalty(0.5) .temperature(0.3) .maxTokens(MAX_TOKENS) .n(1) .build(); + return Optional.ofNullable(openAiService.createChatCompletion(chatCompletionRequest) .getChoices() .get(0) 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 23c5312cd4..d6135d2bb5 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 @@ -178,14 +178,14 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, return useChatGptFallbackMessage(threadChannel); } - UnaryOperator response = """ + UnaryOperator preambleResponse = """ Here is an AI assisted attempt to answer your question 🤖. Maybe it helps! \ In any case, a human is on the way 👍. To continue talking to the AI, you can use \ %s. """::formatted; return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) - .map(response) + .map(preambleResponse) .flatMap(threadChannel::sendMessage) .flatMap(embed -> threadChannel .sendMessageEmbeds(HelpSystemHelper.embedWith(chatGPTAnswer.get()))); diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java new file mode 100644 index 0000000000..b05659706c --- /dev/null +++ b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java @@ -0,0 +1,149 @@ +package org.togetherjava.tjbot.features.chatgpt; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.features.chaptgpt.ChatGptService; + +import java.util.Optional; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ChatGptServiceTest { + private static final Logger logger = LoggerFactory.getLogger(ChatGptServiceTest.class); + private Config config; + private ChatGptService chatGptService; + + @BeforeEach + void setUp() { + config = mock(); + when(config.getOpenaiApiKey()) + .thenReturn("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"); + chatGptService = new ChatGptService(config); + } + + @Test + void askToGenerateLongPoem() { + Optional response = chatGptService.ask("generate a long poem"); + response.ifPresent(logger::warn); + response.ifPresent(this::testResponse); + } + + @Test + void askHowToSetupJacksonLibraryWithExamples() { + Optional response = + chatGptService.ask("How to setup jackson library with examples"); + response.ifPresent(logger::warn); + response.ifPresent(this::testResponse); + } + + @Test + void askDockerReverseProxyWithNginxGuide() { + Optional response = chatGptService.ask("Docker reverse proxy with nginx guide"); + response.ifPresent(logger::warn); + response.ifPresent(this::testResponse); + } + + @Test + void askWhatIsWrongWithMyCode() { + Optional response = chatGptService.ask(""" + What is wrong with my code? + + package Lab13; + import java.lang.reflect.Array; + import java.util.*; + import java.io.*; + + + public class RandomDoubleFile { + + Random r = new Random(); + //Read File + + + public void createDouble() { + + try { + //Create/Open/Write File + FileOutputStream f = new FileOutputStream("double.dat"); + BufferedOutputStream b = new BufferedOutputStream(f); + DataOutputStream d = new DataOutputStream(b); + + //Write Number of Doubles + int myint = r.nextInt(50); + d.write(myint); + + //Write Doubles + for (int i = 0; i < myint ; i++) { + d.writeDouble(r.nextDouble()); + } + d.close(); + }catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + public void readDouble() { + + double[] doublesArray = new double[0]; + + try { + //Configure Read Streams + FileInputStream f = new FileInputStream("double.dat"); + BufferedInputStream b = new BufferedInputStream(f); + DataInputStream d = new DataInputStream(b); + + //Read Num of Doubles + int numberOfDoubles = d.readInt(); + doublesArray = new double[numberOfDoubles]; + + //Try-Catch End-of-file + try { + //Store Doubles in List + for (int i = 0; ; i++) { + doublesArray[i] = d.readDouble(); + } + + } catch (EOFException eof) { + d.close(); + } + }catch (IOException e) { + System.out.println(e.getMessage()); + } + + //ReadList + for(int i=0;i < Array.getLength(doublesArray); i++){ + System.out.println(doublesArray[i]); + } + } + + public static void main(String[] args){ + RandomDoubleFile rdb = new RandomDoubleFile(); + //rdb.createDouble(); + rdb.readDouble(); + } + } + """); + response.ifPresent(logger::warn); + response.ifPresent(this::testResponse); + } + + @Test + void askWhyDoesItTakeYouMoreThan10SeconsToAnswer() { + Optional response = + chatGptService.ask("Why does it take you more than 10 seconds to answer"); + response.ifPresent(logger::warn); + response.ifPresent(this::testResponse); + } + + private void testResponse(String response) { + int AI_RESPONSE_CHARACTER_LIMIT = 2000; + Assertions.assertTrue(response.length() < AI_RESPONSE_CHARACTER_LIMIT, + "Response length is NOT within character limit: " + response.length()); + } +} From 22b1dcdd574518716cfcd6f2179783d9a5b7b35f Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sun, 21 May 2023 13:56:38 -0700 Subject: [PATCH 25/29] - Remove key in file. --- .../tjbot/features/chatgpt/ChatGptServiceTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java index b05659706c..ea7d38ecbe 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java @@ -22,8 +22,7 @@ class ChatGptServiceTest { @BeforeEach void setUp() { config = mock(); - when(config.getOpenaiApiKey()) - .thenReturn("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"); + when(config.getOpenaiApiKey()).thenReturn("YOUR-KEY-HERE"); chatGptService = new ChatGptService(config); } From 46b87ff944087e2de3f180165dc71c152fb24198 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Tue, 23 May 2023 22:47:15 -0700 Subject: [PATCH 26/29] - Refactor of code to tune and test ChatGPT giving answers within character limit. --- .../features/chaptgpt/ChatGptService.java | 12 +++++++----- .../features/chatgpt/ChatGptServiceTest.java | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java index 33f5c21e1f..9f3ff9a714 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -52,16 +52,18 @@ public Optional ask(String question) { return Optional.empty(); } - ChatMessage setUpMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), """ - Please answer questions in 1750 characters. - Please note if your answer needs more room. - """); try { + question = + """ + Please answer questions in 1500 characters or less. Remember to count spaces in the character limit. + For code supplied for review, refer to the old code supplied rather than rewriting the code. + Don't supply a corrected version of the code.\s""" + + question; ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), Objects.requireNonNull(question)); ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() .model("gpt-3.5-turbo") - .messages(List.of(setUpMessage, chatMessage)) + .messages(List.of(chatMessage)) .frequencyPenalty(0.5) .temperature(0.3) .maxTokens(MAX_TOKENS) diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java index ea7d38ecbe..f0409c61ab 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java @@ -22,7 +22,8 @@ class ChatGptServiceTest { @BeforeEach void setUp() { config = mock(); - when(config.getOpenaiApiKey()).thenReturn("YOUR-KEY-HERE"); + String openaiKey = System.getenv("openai-key"); + when(config.getOpenaiApiKey()).thenReturn(openaiKey); chatGptService = new ChatGptService(config); } @@ -30,7 +31,7 @@ void setUp() { void askToGenerateLongPoem() { Optional response = chatGptService.ask("generate a long poem"); response.ifPresent(logger::warn); - response.ifPresent(this::testResponse); + response.ifPresent(this::testResponseLength); } @Test @@ -38,14 +39,14 @@ void askHowToSetupJacksonLibraryWithExamples() { Optional response = chatGptService.ask("How to setup jackson library with examples"); response.ifPresent(logger::warn); - response.ifPresent(this::testResponse); + response.ifPresent(this::testResponseLength); } @Test void askDockerReverseProxyWithNginxGuide() { Optional response = chatGptService.ask("Docker reverse proxy with nginx guide"); response.ifPresent(logger::warn); - response.ifPresent(this::testResponse); + response.ifPresent(this::testResponseLength); } @Test @@ -129,20 +130,21 @@ public static void main(String[] args){ } """); response.ifPresent(logger::warn); - response.ifPresent(this::testResponse); + response.ifPresent(this::testResponseLength); } @Test - void askWhyDoesItTakeYouMoreThan10SeconsToAnswer() { + void askWhyDoesItTakeYouMoreThan10SecondsToAnswer() { Optional response = chatGptService.ask("Why does it take you more than 10 seconds to answer"); response.ifPresent(logger::warn); - response.ifPresent(this::testResponse); + response.ifPresent(this::testResponseLength); } - private void testResponse(String response) { + private void testResponseLength(String response) { int AI_RESPONSE_CHARACTER_LIMIT = 2000; Assertions.assertTrue(response.length() < AI_RESPONSE_CHARACTER_LIMIT, "Response length is NOT within character limit: " + response.length()); + logger.warn("Response length was: {}", response.length()); } } From b9dc0bc68dec12eeafe5a87589b5fd8cd45c4490 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sun, 28 May 2023 17:19:20 -0700 Subject: [PATCH 27/29] - Change ChatGPTService.ask() to return an Optional. - Refactoring to accommodate above change in other classes amd methods. - Refactor HelpThreadCreatedListener to make creating AI response its own method. - Refactor HelpSystemHelper.constructChatGptAttempt() to place all required messages from AI response into embeds when answering a question. --- .../features/chaptgpt/ChatGptCommand.java | 12 ++-- .../features/chaptgpt/ChatGptService.java | 56 ++++++++++++++++--- .../tjbot/features/help/HelpSystemHelper.java | 11 ++-- .../help/HelpThreadCreatedListener.java | 12 +++- .../features/chatgpt/ChatGptServiceTest.java | 36 +++++++----- 5 files changed, 92 insertions(+), 35 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java index 2eb1465cff..3c87bd7c24 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -74,18 +74,20 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { public void onModalSubmitted(ModalInteractionEvent event, List args) { event.deferReply().queue(); - Optional optional = + Optional optional = chatGptService.ask(event.getValue(QUESTION_INPUT).getAsString()); 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); - event.getHook().sendMessage(response).queue(); + String[] response = optional.orElse(errorResponse); + for (String message : response) { + event.getHook().sendMessage(message).queue(); + } } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java index 9f3ff9a714..04c43eeeaf 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -22,6 +22,7 @@ public class ChatGptService { private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class); private static final Duration TIMEOUT = Duration.ofSeconds(120); private static final int MAX_TOKENS = 3_000; + private static final int RESPONSE_LENGTH_LIMIT = 2_000; private boolean isDisabled = false; private final OpenAiService openAiService; @@ -37,6 +38,23 @@ public ChatGptService(Config config) { } openAiService = new OpenAiService(apiKey, TIMEOUT); + + ChatMessage setupMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), + """ + Please answer questions in 1500 characters or less. Remember to count spaces in the + character limit. For code supplied for review, refer to the old code supplied rather than + rewriting the code. Don't supply a corrected version of the code.\s"""); + ChatCompletionRequest systemSetupRequest = ChatCompletionRequest.builder() + .model("gpt-3.5-turbo") + .messages(List.of(setupMessage)) + .frequencyPenalty(0.5) + .temperature(0.3) + .maxTokens(50) + .n(1) + .build(); + + // Sending the system setup message to ChatGPT. + openAiService.createChatCompletion(systemSetupRequest); } /** @@ -47,18 +65,12 @@ public ChatGptService(Config config) { * Tokens. * @return response from ChatGPT as a String. */ - public Optional ask(String question) { + public Optional ask(String question) { if (isDisabled) { return Optional.empty(); } try { - question = - """ - Please answer questions in 1500 characters or less. Remember to count spaces in the character limit. - For code supplied for review, refer to the old code supplied rather than rewriting the code. - Don't supply a corrected version of the code.\s""" - + question; ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), Objects.requireNonNull(question)); ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() @@ -70,11 +82,37 @@ public Optional ask(String question) { .n(1) .build(); - return Optional.ofNullable(openAiService.createChatCompletion(chatCompletionRequest) + String response = openAiService.createChatCompletion(chatCompletionRequest) .getChoices() .get(0) .getMessage() - .getContent()); + .getContent(); + + String[] aiResponses; + if (response.length() > RESPONSE_LENGTH_LIMIT) { + logger.warn( + "Response from AI was longer than allowed limit. " + + "The answer was cut up to max {} characters length messages", + RESPONSE_LENGTH_LIMIT); + int begin = 0; + int end = RESPONSE_LENGTH_LIMIT; + int i = 0; + aiResponses = new String[(response.length() / RESPONSE_LENGTH_LIMIT) + 1]; + while (begin < response.length()) { + aiResponses[i] = response.substring(begin, end); + + begin += RESPONSE_LENGTH_LIMIT; + end += RESPONSE_LENGTH_LIMIT; + if (end > response.length()) { + end = response.length(); + } + i++; + } + } else { + aiResponses = new String[] {response}; + } + + return Optional.of(aiResponses); } 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 d6135d2bb5..3d1fa774d2 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 @@ -165,9 +165,8 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha */ RestAction constructChatGptAttempt(ThreadChannel threadChannel, String originalQuestion) { - Optional questionOptional = prepareChatGptQuestion(threadChannel, originalQuestion); - Optional chatGPTAnswer; + Optional chatGPTAnswer; String question = questionOptional.orElseThrow(); logger.debug("The final question sent to chatGPT: {}", question); @@ -184,11 +183,15 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, %s. """::formatted; + List aiResponseEmbeds = new ArrayList<>(); + for (String aiResponse : chatGPTAnswer.get()) { + aiResponseEmbeds.add(HelpSystemHelper.embedWith(aiResponse)); + } + return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) .map(preambleResponse) .flatMap(threadChannel::sendMessage) - .flatMap(embed -> threadChannel - .sendMessageEmbeds(HelpSystemHelper.embedWith(chatGPTAnswer.get()))); + .flatMap(embed -> threadChannel.sendMessageEmbeds(aiResponseEmbeds)); } private Optional prepareChatGptQuestion(ThreadChannel threadChannel, diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index f58031d98b..bce2b4ab6d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -77,6 +77,7 @@ private void handleHelpThreadCreated(ThreadChannel threadChannel) { Runnable createMessages = () -> { try { createMessages(threadChannel).queue(); + createAIResponse(threadChannel).queue(); } catch (Exception e) { logger.error( "Unknown error while creating messages after help-thread ({}) creation", @@ -90,12 +91,17 @@ private void handleHelpThreadCreated(ThreadChannel threadChannel) { SERVICE.schedule(createMessages, 5, TimeUnit.SECONDS); } + private RestAction createAIResponse(ThreadChannel threadChannel) { + RestAction originalQuestion = + threadChannel.retrieveMessageById(threadChannel.getIdLong()); + return originalQuestion.flatMap( + message -> helper.constructChatGptAttempt(threadChannel, message.getContentRaw())); + } + private RestAction createMessages(ThreadChannel threadChannel) { return sendHelperHeadsUp(threadChannel).flatMap(Message::pin) .flatMap(any -> helper.sendExplanationMessage(threadChannel)) - .flatMap(any -> threadChannel.retrieveMessageById(threadChannel.getIdLong()) - .flatMap(message -> helper.constructChatGptAttempt(threadChannel, - message.getContentRaw()))); + .flatMap(any -> threadChannel.retrieveMessageById(threadChannel.getIdLong())); } private RestAction sendHelperHeadsUp(ThreadChannel threadChannel) { diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java index f0409c61ab..6dc02ff9c6 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java @@ -29,29 +29,29 @@ void setUp() { @Test void askToGenerateLongPoem() { - Optional response = chatGptService.ask("generate a long poem"); - response.ifPresent(logger::warn); + Optional response = chatGptService.ask("generate a long poem"); + response.ifPresent(this::toLog); response.ifPresent(this::testResponseLength); } @Test void askHowToSetupJacksonLibraryWithExamples() { - Optional response = + Optional response = chatGptService.ask("How to setup jackson library with examples"); - response.ifPresent(logger::warn); + response.ifPresent(this::toLog); response.ifPresent(this::testResponseLength); } @Test void askDockerReverseProxyWithNginxGuide() { - Optional response = chatGptService.ask("Docker reverse proxy with nginx guide"); - response.ifPresent(logger::warn); + Optional response = chatGptService.ask("Docker reverse proxy with nginx guide"); + response.ifPresent(this::toLog); response.ifPresent(this::testResponseLength); } @Test void askWhatIsWrongWithMyCode() { - Optional response = chatGptService.ask(""" + Optional response = chatGptService.ask(""" What is wrong with my code? package Lab13; @@ -129,22 +129,30 @@ public static void main(String[] args){ } } """); - response.ifPresent(logger::warn); + response.ifPresent(this::toLog); response.ifPresent(this::testResponseLength); } @Test void askWhyDoesItTakeYouMoreThan10SecondsToAnswer() { - Optional response = + Optional response = chatGptService.ask("Why does it take you more than 10 seconds to answer"); - response.ifPresent(logger::warn); + response.ifPresent(this::toLog); response.ifPresent(this::testResponseLength); } - private void testResponseLength(String response) { + private void testResponseLength(String[] responses) { int AI_RESPONSE_CHARACTER_LIMIT = 2000; - Assertions.assertTrue(response.length() < AI_RESPONSE_CHARACTER_LIMIT, - "Response length is NOT within character limit: " + response.length()); - logger.warn("Response length was: {}", response.length()); + 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.warn(response); + } } } From 54e739eaf90a385a7ad20504f4d2d8bfadced475 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Sun, 4 Jun 2023 17:46:30 -0700 Subject: [PATCH 28/29] - Change ChatGptService.java to attempt to break up long messages. Try to only break up by new lines and try not to be in code when doing so. - Change messages sent from ChatGPT from embeds into plain messages. --- .../features/chaptgpt/ChatGptService.java | 67 ++++++++++++++----- .../tjbot/features/help/HelpSystemHelper.java | 13 ++-- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java index 04c43eeeaf..984124449e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -28,7 +28,7 @@ public class ChatGptService { /** * Creates instance of ChatGPTService - * + * * @param config needed for token to OpenAI API. */ public ChatGptService(Config config) { @@ -59,11 +59,11 @@ public ChatGptService(Config config) { /** * Prompt ChatGPT with a question and receive a response. - * + * * @param question The question being asked of ChatGPT. Max is {@value MAX_TOKENS} tokens. + * @return response from ChatGPT as a String. * @see ChatGPT * Tokens. - * @return response from ChatGPT as a String. */ public Optional ask(String question) { if (isDisabled) { @@ -94,20 +94,7 @@ public Optional ask(String question) { "Response from AI was longer than allowed limit. " + "The answer was cut up to max {} characters length messages", RESPONSE_LENGTH_LIMIT); - int begin = 0; - int end = RESPONSE_LENGTH_LIMIT; - int i = 0; - aiResponses = new String[(response.length() / RESPONSE_LENGTH_LIMIT) + 1]; - while (begin < response.length()) { - aiResponses[i] = response.substring(begin, end); - - begin += RESPONSE_LENGTH_LIMIT; - end += RESPONSE_LENGTH_LIMIT; - if (end > response.length()) { - end = response.length(); - } - i++; - } + aiResponses = breakupAiResponse(response); } else { aiResponses = new String[] {response}; } @@ -124,4 +111,50 @@ public Optional ask(String question) { } return Optional.empty(); } + + private String[] breakupAiResponse(String response) { + String[] aiResponses; + int begin = 0; + int end = RESPONSE_LENGTH_LIMIT - 1; + int responseLength = response.length(); + int aiResponsesLength = + responseLength % RESPONSE_LENGTH_LIMIT == 0 ? responseLength / RESPONSE_LENGTH_LIMIT + : (responseLength / RESPONSE_LENGTH_LIMIT) + 1; + aiResponses = new String[aiResponsesLength]; + for (int i = 0; begin < responseLength; i++) { + if (responseLength - begin <= RESPONSE_LENGTH_LIMIT) { + // No point breaking up the response more by new line if leftover bit is + // less than limit. + aiResponses[i] = response.substring(begin, end); + begin += RESPONSE_LENGTH_LIMIT; + } else { + int lastLineFeedIndex = response.lastIndexOf("\n", begin + end); + boolean looking = true; + char character; + while (looking) { + character = response.charAt(lastLineFeedIndex - 1); + switch (character) { + case ';', '}', '{', '`' -> + // Don't break up explanation in code... + // Leads to decoupling with code highlights (```) and formatting + // issues + // in future responses. + lastLineFeedIndex = response.lastIndexOf("\n", lastLineFeedIndex - 1); + case '\n' -> lastLineFeedIndex--; + default -> looking = false; + } + } + + aiResponses[i] = response.substring(begin, lastLineFeedIndex); + + begin += lastLineFeedIndex; + end += RESPONSE_LENGTH_LIMIT - 1; + if (end > response.length() - 1) { + end = response.length() - 1; + } + } + } + + return aiResponses; + } } 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 3d1fa774d2..a9dbf20e85 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 @@ -183,15 +183,16 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, %s. """::formatted; - List aiResponseEmbeds = new ArrayList<>(); + RestAction message = + mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) + .map(preambleResponse) + .flatMap(threadChannel::sendMessage); + for (String aiResponse : chatGPTAnswer.get()) { - aiResponseEmbeds.add(HelpSystemHelper.embedWith(aiResponse)); + message = message.map(aiResponse::formatted).flatMap(threadChannel::sendMessage); } - return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) - .map(preambleResponse) - .flatMap(threadChannel::sendMessage) - .flatMap(embed -> threadChannel.sendMessageEmbeds(aiResponseEmbeds)); + return message; } private Optional prepareChatGptQuestion(ThreadChannel threadChannel, From a3f074327d0de71f5a1da135d146e3842d329624 Mon Sep 17 00:00:00 2001 From: tmcdonnell2 Date: Wed, 5 Jul 2023 22:01:38 -0700 Subject: [PATCH 29/29] - Add ChatGptAPITest (rename ChatGptTest) - Add ChatGptServiceTest.java for testing AI responses of different length - Add response.txt which contains test AI responses (just one for now). - Refactor ChatGptService to not include response parsing/break up. Moved to new util class AIResponseParser --- .../features/chaptgpt/AIResponseParser.java | 88 +++++++++ .../features/chaptgpt/ChatGptService.java | 60 +------ .../features/chatgpt/ChatGptAPITest.java | 158 ++++++++++++++++ .../features/chatgpt/ChatGptServiceTest.java | 168 +++++------------- .../test/resources/AITestResponses/responses | 71 ++++++++ 5 files changed, 358 insertions(+), 187 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/AIResponseParser.java create mode 100644 application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptAPITest.java create mode 100644 application/src/test/resources/AITestResponses/responses diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/AIResponseParser.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/AIResponseParser.java new file mode 100644 index 0000000000..1220a2c6cc --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/AIResponseParser.java @@ -0,0 +1,88 @@ +package org.togetherjava.tjbot.features.chaptgpt; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class AIResponseParser { + + private AIResponseParser() {} + + private static final Logger logger = LoggerFactory.getLogger(AIResponseParser.class); + private static final int RESPONSE_LENGTH_LIMIT = 2_000; + + public static Optional parse(String response) { + String[] aiResponses; + if (response.length() > RESPONSE_LENGTH_LIMIT) { + logger.warn( + "Response from AI was longer than allowed limit. " + + "The answer was cut up to max {} characters length messages", + RESPONSE_LENGTH_LIMIT); + aiResponses = breakupAiResponse(response); + } else { + aiResponses = new String[] {response}; + } + + return Optional.of(aiResponses); + } + + private static String[] breakupAiResponse(String response) { + List codeMarkIndexPairs = new ArrayList<>(); + + int firstCodeBlockMarkIndex; + while ((firstCodeBlockMarkIndex = response.indexOf("```")) != -1) { + // Assuming that code marks come in pairs... + int secondCodeBlockMarkIndex = response.indexOf("```"); + codeMarkIndexPairs + .add(new CodeBlockIndexPair(firstCodeBlockMarkIndex, secondCodeBlockMarkIndex)); + } + + List brokenUpAiResponse = new ArrayList<>(); + if (codeMarkIndexPairs.stream() + .mapToInt(CodeBlockIndexPair::getLength) + .allMatch(i -> i < 2000)) { + + int begin = 0; + for (CodeBlockIndexPair codeBlockIndexPair : codeMarkIndexPairs) { + int end = codeBlockIndexPair.getBeginIndex(); + brokenUpAiResponse.add(response.substring(begin, end)); + + begin = end; + // Add three because index only really captures first `. + end = codeBlockIndexPair.getEndIndex() + 3; + brokenUpAiResponse.add(response.substring(begin, end)); + + begin = end; + } + } else { + // + } + + return brokenUpAiResponse.toArray(new String[0]); + } + + static class CodeBlockIndexPair { + private final int beginIndex; + private final int endIndex; + + public CodeBlockIndexPair(int beginIndex, int endIndex) { + this.beginIndex = beginIndex; + this.endIndex = endIndex; + } + + public int getBeginIndex() { + return beginIndex; + } + + public int getEndIndex() { + return endIndex; + } + + public int getLength() { + return endIndex - beginIndex; + } + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java index 984124449e..7d07d6c818 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -22,7 +22,6 @@ public class ChatGptService { private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class); private static final Duration TIMEOUT = Duration.ofSeconds(120); private static final int MAX_TOKENS = 3_000; - private static final int RESPONSE_LENGTH_LIMIT = 2_000; private boolean isDisabled = false; private final OpenAiService openAiService; @@ -88,18 +87,7 @@ public Optional ask(String question) { .getMessage() .getContent(); - String[] aiResponses; - if (response.length() > RESPONSE_LENGTH_LIMIT) { - logger.warn( - "Response from AI was longer than allowed limit. " - + "The answer was cut up to max {} characters length messages", - RESPONSE_LENGTH_LIMIT); - aiResponses = breakupAiResponse(response); - } else { - aiResponses = new String[] {response}; - } - - return Optional.of(aiResponses); + return AIResponseParser.parse(response); } catch (OpenAiHttpException openAiHttpException) { logger.warn( "There was an error using the OpenAI API: {} Code: {} Type: {} Status Code: {}", @@ -111,50 +99,4 @@ public Optional ask(String question) { } return Optional.empty(); } - - private String[] breakupAiResponse(String response) { - String[] aiResponses; - int begin = 0; - int end = RESPONSE_LENGTH_LIMIT - 1; - int responseLength = response.length(); - int aiResponsesLength = - responseLength % RESPONSE_LENGTH_LIMIT == 0 ? responseLength / RESPONSE_LENGTH_LIMIT - : (responseLength / RESPONSE_LENGTH_LIMIT) + 1; - aiResponses = new String[aiResponsesLength]; - for (int i = 0; begin < responseLength; i++) { - if (responseLength - begin <= RESPONSE_LENGTH_LIMIT) { - // No point breaking up the response more by new line if leftover bit is - // less than limit. - aiResponses[i] = response.substring(begin, end); - begin += RESPONSE_LENGTH_LIMIT; - } else { - int lastLineFeedIndex = response.lastIndexOf("\n", begin + end); - boolean looking = true; - char character; - while (looking) { - character = response.charAt(lastLineFeedIndex - 1); - switch (character) { - case ';', '}', '{', '`' -> - // Don't break up explanation in code... - // Leads to decoupling with code highlights (```) and formatting - // issues - // in future responses. - lastLineFeedIndex = response.lastIndexOf("\n", lastLineFeedIndex - 1); - case '\n' -> lastLineFeedIndex--; - default -> looking = false; - } - } - - aiResponses[i] = response.substring(begin, lastLineFeedIndex); - - begin += lastLineFeedIndex; - end += RESPONSE_LENGTH_LIMIT - 1; - if (end > response.length() - 1) { - end = response.length() - 1; - } - } - } - - return aiResponses; - } } diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptAPITest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptAPITest.java new file mode 100644 index 0000000000..554fe08180 --- /dev/null +++ b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptAPITest.java @@ -0,0 +1,158 @@ +package org.togetherjava.tjbot.features.chatgpt; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.features.chaptgpt.ChatGptService; + +import java.util.Optional; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ChatGptAPITest { + private static final Logger logger = LoggerFactory.getLogger(ChatGptAPITest.class); + private Config config; + private ChatGptService chatGptService; + + @BeforeEach + void setUp() { + config = mock(); + String openaiKey = System.getenv("openai-key"); + when(config.getOpenaiApiKey()).thenReturn(openaiKey); + chatGptService = new ChatGptService(config); + } + + @Test + void askToGenerateLongPoem() { + Optional response = chatGptService.ask("generate a long poem"); + response.ifPresent(this::toLog); + response.ifPresent(this::testResponseLength); + } + + @Test + void askHowToSetupJacksonLibraryWithExamples() { + Optional response = + chatGptService.ask("How to setup jackson library with examples"); + response.ifPresent(this::toLog); + response.ifPresent(this::testResponseLength); + } + + @Test + void askDockerReverseProxyWithNginxGuide() { + Optional response = chatGptService.ask("Docker reverse proxy with nginx guide"); + response.ifPresent(this::toLog); + response.ifPresent(this::testResponseLength); + } + + @Test + void askWhatIsWrongWithMyCode() { + Optional response = chatGptService.ask(""" + What is wrong with my code? + + package Lab13; + import java.lang.reflect.Array; + import java.util.*; + import java.io.*; + + + public class RandomDoubleFile { + + Random r = new Random(); + //Read File + + + public void createDouble() { + + try { + //Create/Open/Write File + FileOutputStream f = new FileOutputStream("double.dat"); + BufferedOutputStream b = new BufferedOutputStream(f); + DataOutputStream d = new DataOutputStream(b); + + //Write Number of Doubles + int myint = r.nextInt(50); + d.write(myint); + + //Write Doubles + for (int i = 0; i < myint ; i++) { + d.writeDouble(r.nextDouble()); + } + d.close(); + }catch (IOException e) { + System.out.println(e.getMessage()); + } + + } + public void readDouble() { + + double[] doublesArray = new double[0]; + + try { + //Configure Read Streams + FileInputStream f = new FileInputStream("double.dat"); + BufferedInputStream b = new BufferedInputStream(f); + DataInputStream d = new DataInputStream(b); + + //Read Num of Doubles + int numberOfDoubles = d.readInt(); + doublesArray = new double[numberOfDoubles]; + + //Try-Catch End-of-file + try { + //Store Doubles in List + for (int i = 0; ; i++) { + doublesArray[i] = d.readDouble(); + } + + } catch (EOFException eof) { + d.close(); + } + }catch (IOException e) { + System.out.println(e.getMessage()); + } + + //ReadList + for(int i=0;i < Array.getLength(doublesArray); i++){ + System.out.println(doublesArray[i]); + } + } + + public static void main(String[] args){ + RandomDoubleFile rdb = new RandomDoubleFile(); + //rdb.createDouble(); + rdb.readDouble(); + } + } + """); + response.ifPresent(this::toLog); + response.ifPresent(this::testResponseLength); + } + + @Test + void askWhyDoesItTakeYouMoreThan10SecondsToAnswer() { + Optional response = + chatGptService.ask("Why does it take you more than 10 seconds to answer"); + response.ifPresent(this::toLog); + response.ifPresent(this::testResponseLength); + } + + 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.warn(response); + } + } +} diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java index 6dc02ff9c6..e807959318 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/ChatGptServiceTest.java @@ -1,144 +1,56 @@ package org.togetherjava.tjbot.features.chatgpt; +import com.theokanning.openai.completion.chat.ChatCompletionChoice; +import com.theokanning.openai.completion.chat.ChatCompletionResult; +import com.theokanning.openai.completion.chat.ChatMessage; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.togetherjava.tjbot.config.Config; -import org.togetherjava.tjbot.features.chaptgpt.ChatGptService; +import org.togetherjava.tjbot.features.chaptgpt.AIResponseParser; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.util.List; import java.util.Optional; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class ChatGptServiceTest { - private static final Logger logger = LoggerFactory.getLogger(ChatGptServiceTest.class); - private Config config; - private ChatGptService chatGptService; - - @BeforeEach - void setUp() { - config = mock(); - String openaiKey = System.getenv("openai-key"); - when(config.getOpenaiApiKey()).thenReturn(openaiKey); - chatGptService = new ChatGptService(config); - } - - @Test - void askToGenerateLongPoem() { - Optional response = chatGptService.ask("generate a long poem"); - response.ifPresent(this::toLog); - response.ifPresent(this::testResponseLength); - } - - @Test - void askHowToSetupJacksonLibraryWithExamples() { - Optional response = - chatGptService.ask("How to setup jackson library with examples"); - response.ifPresent(this::toLog); - response.ifPresent(this::testResponseLength); - } - - @Test - void askDockerReverseProxyWithNginxGuide() { - Optional response = chatGptService.ask("Docker reverse proxy with nginx guide"); - response.ifPresent(this::toLog); - response.ifPresent(this::testResponseLength); +class AIResponseParserTest { + private static final Logger logger = LoggerFactory.getLogger(AIResponseParserTest.class); + private AIResponseParser aiResponseParser; + + @ParameterizedTest + @CsvFileSource(resources = "/AITestResponses/responses") + void correctResponseLength(File file) { + try { + BufferedReader input = + new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()))); + Optional aiResponse = AIResponseParser.parse(input.lines().toString()); + + aiResponse.ifPresentOrElse((responses) -> { + testResponseLength(responses); + toLog(responses); + }, Assertions.fail()); + } catch (IOException ex) { + logger.error("{}", ex.getMessage()); + Assertions.fail(); + } } - @Test - void askWhatIsWrongWithMyCode() { - Optional response = chatGptService.ask(""" - What is wrong with my code? - - package Lab13; - import java.lang.reflect.Array; - import java.util.*; - import java.io.*; - - - public class RandomDoubleFile { - - Random r = new Random(); - //Read File + private ChatCompletionResult createChatCompletionResult(String message) { + ChatMessage chatMessage = new ChatMessage(); + chatMessage.setContent(message); + ChatCompletionChoice chatCompletionChoice = new ChatCompletionChoice(); + chatCompletionChoice.setMessage(chatMessage); - public void createDouble() { - - try { - //Create/Open/Write File - FileOutputStream f = new FileOutputStream("double.dat"); - BufferedOutputStream b = new BufferedOutputStream(f); - DataOutputStream d = new DataOutputStream(b); - - //Write Number of Doubles - int myint = r.nextInt(50); - d.write(myint); - - //Write Doubles - for (int i = 0; i < myint ; i++) { - d.writeDouble(r.nextDouble()); - } - d.close(); - }catch (IOException e) { - System.out.println(e.getMessage()); - } - - } - public void readDouble() { - - double[] doublesArray = new double[0]; - - try { - //Configure Read Streams - FileInputStream f = new FileInputStream("double.dat"); - BufferedInputStream b = new BufferedInputStream(f); - DataInputStream d = new DataInputStream(b); - - //Read Num of Doubles - int numberOfDoubles = d.readInt(); - doublesArray = new double[numberOfDoubles]; - - //Try-Catch End-of-file - try { - //Store Doubles in List - for (int i = 0; ; i++) { - doublesArray[i] = d.readDouble(); - } - - } catch (EOFException eof) { - d.close(); - } - }catch (IOException e) { - System.out.println(e.getMessage()); - } - - //ReadList - for(int i=0;i < Array.getLength(doublesArray); i++){ - System.out.println(doublesArray[i]); - } - } - - public static void main(String[] args){ - RandomDoubleFile rdb = new RandomDoubleFile(); - //rdb.createDouble(); - rdb.readDouble(); - } - } - """); - response.ifPresent(this::toLog); - response.ifPresent(this::testResponseLength); - } - - @Test - void askWhyDoesItTakeYouMoreThan10SecondsToAnswer() { - Optional response = - chatGptService.ask("Why does it take you more than 10 seconds to answer"); - response.ifPresent(this::toLog); - response.ifPresent(this::testResponseLength); + ChatCompletionResult chatCompletionResult = new ChatCompletionResult(); + chatCompletionResult.setChoices(List.of(chatCompletionChoice)); + return chatCompletionResult; } private void testResponseLength(String[] responses) { diff --git a/application/src/test/resources/AITestResponses/responses b/application/src/test/resources/AITestResponses/responses new file mode 100644 index 0000000000..beda6295c5 --- /dev/null +++ b/application/src/test/resources/AITestResponses/responses @@ -0,0 +1,71 @@ +Jackson is a popular Java library for JSON processing. It provides a simple and efficient way to convert Java objects to JSON and vice versa. In this tutorial, we will learn how to set up Jackson in a Java project and use it to serialize and deserialize JSON data. + +Step 1: Add Jackson Dependency + +The first step is to add the Jackson dependency to your project. You can do this by adding the following code to your build.gradle file: + +``` +dependencies { + implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3' +} +``` + +Step 2: Create Java Objects + +Next, we need to create some Java objects that we want to serialize and deserialize as JSON. For example, let's create a simple Person class: + +``` +public class Person { + private String name; + private int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } +} +``` + +Step 3: Serialize Java Object to JSON + +To serialize a Java object as JSON, we need to create an ObjectMapper object from the com.fasterxml.jackson.databind.ObjectMapper class and call its writeValueAsString() method. + +``` +ObjectMapper objectMapper = new ObjectMapper(); +Person person = new Person("John Doe", 30); +String json = objectMapper.writeValueAsString(person); +System.out.println(json); // {"name":"John Doe","age":30} +``` + +Step 4: Deserialize JSON to Java Object + +To deserialize JSON data into a Java object, we need to call the readValue() method of the ObjectMapper class. + +``` +String json = "{\"name\":\"John Doe\",\"age\":30}"; +Person person = objectMapper.readValue(json, Person.class); +System.out.println(person.getName()); // John Doe +System.out.println(person.getAge()); // 30 +``` + +Conclusion + +In this tutorial, we learned how to set up Jackson in a Java project and use it to serialize and deserialize JSON data. +Jackson is a powerful library that provides many features for working with JSON data. +With its simple API and efficient performance, it is a great choice for any Java project that needs to work with JSON data. \ No newline at end of file