From ae006d2a0d460b6d721bdf50c1093da3705c22c0 Mon Sep 17 00:00:00 2001 From: Loimz <83086076+tmcdonnell2@users.noreply.github.com> Date: Sat, 26 Aug 2023 17:15:29 -0700 Subject: [PATCH] Chat gpt attempt questions (#873) * * Refactor folder name * Add ChatGPT command/service code and utils. * * Fix Features.java import statements and add ChatGPTService being passed in to helper. * Add parsing of question to helper. * * Add AI Help message to when user asks question. (Either direction to the command if the service fails or an explanation that the next response is AI generated) * * Add test files and test suite for AIResponseParser.java * Add logger.debug() to catch responses before being parsed in case of failing to parse correctly to generate new tests [AIResponseParser.java]. * * Refactor how AIResponseParserTest parameterizes file names (ints instead of full file names). * Remove ChatGPTServiceTest.java as it was an older way to generate long responses from ChatGPT for testing. * Add another test file and include in parameters for AIResponseParserTest.java --- .../togetherjava/tjbot/features/Features.java | 38 ++++-- .../features/chatgpt/AIResponseParser.java | 82 ++++++++++++ .../{chaptgpt => chatgpt}/ChatGptCommand.java | 19 ++- .../{chaptgpt => chatgpt}/ChatGptService.java | 43 +++++-- .../{chaptgpt => chatgpt}/package-info.java | 2 +- .../tjbot/features/help/HelpSystemHelper.java | 119 ++++++++++++++++-- .../help/HelpThreadCreatedListener.java | 8 ++ .../chatgpt/AIResponseParserTest.java | 46 +++++++ .../test/resources/AITestResponses/test1.txt | 71 +++++++++++ .../test/resources/AITestResponses/test2.txt | 63 ++++++++++ .../test/resources/AITestResponses/test3.txt | 80 ++++++++++++ .../test/resources/AITestResponses/test4.txt | 27 ++++ 12 files changed, 566 insertions(+), 32 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java rename application/src/main/java/org/togetherjava/tjbot/features/{chaptgpt => chatgpt}/ChatGptCommand.java (84%) rename application/src/main/java/org/togetherjava/tjbot/features/{chaptgpt => chatgpt}/ChatGptService.java (65%) rename application/src/main/java/org/togetherjava/tjbot/features/{chaptgpt => chatgpt}/package-info.java (84%) create mode 100644 application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java create mode 100644 application/src/test/resources/AITestResponses/test1.txt create mode 100644 application/src/test/resources/AITestResponses/test2.txt create mode 100644 application/src/test/resources/AITestResponses/test3.txt create mode 100644 application/src/test/resources/AITestResponses/test4.txt 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 3210269955..58c34ca949 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -4,22 +4,46 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.features.basic.*; -import org.togetherjava.tjbot.features.bookmarks.*; -import org.togetherjava.tjbot.features.chaptgpt.ChatGptCommand; -import org.togetherjava.tjbot.features.chaptgpt.ChatGptService; +import org.togetherjava.tjbot.features.basic.PingCommand; +import org.togetherjava.tjbot.features.basic.RoleSelectCommand; +import org.togetherjava.tjbot.features.basic.SlashCommandEducator; +import org.togetherjava.tjbot.features.basic.SuggestionsUpDownVoter; +import org.togetherjava.tjbot.features.bookmarks.BookmarksCommand; +import org.togetherjava.tjbot.features.bookmarks.BookmarksSystem; +import org.togetherjava.tjbot.features.bookmarks.LeftoverBookmarksCleanupRoutine; +import org.togetherjava.tjbot.features.bookmarks.LeftoverBookmarksListener; +import org.togetherjava.tjbot.features.chatgpt.ChatGptCommand; +import org.togetherjava.tjbot.features.chatgpt.ChatGptService; import org.togetherjava.tjbot.features.code.CodeMessageAutoDetection; import org.togetherjava.tjbot.features.code.CodeMessageHandler; import org.togetherjava.tjbot.features.code.CodeMessageManualDetection; import org.togetherjava.tjbot.features.filesharing.FileSharingMessageListener; -import org.togetherjava.tjbot.features.help.*; +import org.togetherjava.tjbot.features.help.AutoPruneHelperRoutine; +import org.togetherjava.tjbot.features.help.GuildLeaveCloseThreadListener; +import org.togetherjava.tjbot.features.help.HelpSystemHelper; +import org.togetherjava.tjbot.features.help.HelpThreadActivityUpdater; +import org.togetherjava.tjbot.features.help.HelpThreadAutoArchiver; +import org.togetherjava.tjbot.features.help.HelpThreadCommand; +import org.togetherjava.tjbot.features.help.HelpThreadCreatedListener; +import org.togetherjava.tjbot.features.help.HelpThreadMetadataPurger; import org.togetherjava.tjbot.features.jshell.JShellCommand; import org.togetherjava.tjbot.features.jshell.JShellEval; import org.togetherjava.tjbot.features.mathcommands.TeXCommand; import org.togetherjava.tjbot.features.mathcommands.wolframalpha.WolframAlphaCommand; import org.togetherjava.tjbot.features.mediaonly.MediaOnlyChannelListener; -import org.togetherjava.tjbot.features.moderation.*; +import org.togetherjava.tjbot.features.moderation.BanCommand; +import org.togetherjava.tjbot.features.moderation.KickCommand; +import org.togetherjava.tjbot.features.moderation.ModerationActionsStore; +import org.togetherjava.tjbot.features.moderation.MuteCommand; +import org.togetherjava.tjbot.features.moderation.NoteCommand; +import org.togetherjava.tjbot.features.moderation.QuarantineCommand; +import org.togetherjava.tjbot.features.moderation.RejoinModerationRoleListener; import org.togetherjava.tjbot.features.moderation.ReportCommand; +import org.togetherjava.tjbot.features.moderation.UnbanCommand; +import org.togetherjava.tjbot.features.moderation.UnmuteCommand; +import org.togetherjava.tjbot.features.moderation.UnquarantineCommand; +import org.togetherjava.tjbot.features.moderation.WarnCommand; +import org.togetherjava.tjbot.features.moderation.WhoIsCommand; import org.togetherjava.tjbot.features.moderation.attachment.BlacklistedAttachmentListener; import org.togetherjava.tjbot.features.moderation.audit.AuditCommand; import org.togetherjava.tjbot.features.moderation.audit.ModAuditLogRoutine; @@ -76,9 +100,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(jshellEval); ChatGptService chatGptService = new ChatGptService(config); + HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database, chatGptService); // 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/chatgpt/AIResponseParser.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java new file mode 100644 index 0000000000..9dce43ff1c --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java @@ -0,0 +1,82 @@ +package org.togetherjava.tjbot.features.chatgpt; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents a class to partition long text blocks into smaller blocks which work with Discord's + * API. Initially constructed to partition text from AI text generation APIs. + */ +public class AIResponseParser { + private AIResponseParser() { + throw new UnsupportedOperationException("Utility class, construction not supported"); + } + + private static final Logger logger = LoggerFactory.getLogger(AIResponseParser.class); + private static final int RESPONSE_LENGTH_LIMIT = 2_000; + + /** + * Parses the response generated by AI. If response is longer than + * {@value RESPONSE_LENGTH_LIMIT}, then breaks apart the response into suitable lengths for + * Discords API. + * + * @param response The response from the AI which we want to send over Discord. + * @return An array potentially holding the original response split up into shorter than + * {@value RESPONSE_LENGTH_LIMIT} length pieces. + */ + public static String[] parse(String response) { + String[] partedResponse = new String[] {response}; + if (response.length() > RESPONSE_LENGTH_LIMIT) { + logger.debug("Response to parse:\n{}", response); + partedResponse = partitionAiResponse(response); + } + + return partedResponse; + } + + private static String[] partitionAiResponse(String response) { + List responseChunks = new ArrayList<>(); + String[] splitResponseOnMarks = response.split("```"); + + for (int i = 0; i < splitResponseOnMarks.length; i++) { + String split = splitResponseOnMarks[i]; + List chunks = new ArrayList<>(); + chunks.add(split); + + // Check each chunk for correct length. If over the length, split in two and check + // again. + while (!chunks.stream().allMatch(s -> s.length() < RESPONSE_LENGTH_LIMIT)) { + for (int j = 0; j < chunks.size(); j++) { + String chunk = chunks.get(j); + if (chunk.length() > RESPONSE_LENGTH_LIMIT) { + int midpointNewline = chunk.lastIndexOf("\n", chunk.length() / 2); + chunks.set(j, chunk.substring(0, midpointNewline)); + chunks.add(j + 1, chunk.substring(midpointNewline)); + } + } + } + + // Given the splitting on ```, the odd numbered entries need to have code marks + // restored. + if (i % 2 != 0) { + // We assume that everything after the ``` on the same line is the language + // declaration. Could be empty. + String lang = split.substring(0, split.indexOf(System.lineSeparator())); + chunks = chunks.stream() + .map(s -> ("```" + lang).concat(s).concat("```")) + // Handle case of doubling language declaration + .map(s -> s.replaceFirst("```" + lang + lang, "```" + lang)) + .collect(Collectors.toList()); + } + + List list = chunks.stream().filter(string -> !string.equals("")).toList(); + responseChunks.addAll(list); + } // end of for loop. + + return responseChunks.toArray(new String[0]); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java similarity index 84% rename from application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java rename to application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java index e53a34391d..2f06384d90 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java @@ -1,4 +1,4 @@ -package org.togetherjava.tjbot.features.chaptgpt; +package org.togetherjava.tjbot.features.chatgpt; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; @@ -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; } @@ -73,14 +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 response = optional.orElse( - "An error has occurred while trying to communicate with ChatGPT. Please try again later"); - event.getHook().sendMessage(response).queue(); + String[] errorResponse = {""" + An error has occurred while trying to communicate with ChatGPT. + Please try again later. + """}; + + String[] response = optional.orElse(errorResponse); + for (String message : response) { + event.getHook().sendMessage(message).queue(); + } } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java similarity index 65% rename from application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java rename to application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java index 11d7055a28..11f057e296 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java @@ -1,4 +1,4 @@ -package org.togetherjava.tjbot.features.chaptgpt; +package org.togetherjava.tjbot.features.chatgpt; import com.theokanning.openai.OpenAiHttpException; import com.theokanning.openai.completion.chat.ChatCompletionRequest; @@ -22,12 +22,13 @@ public class ChatGptService { private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class); private static final Duration TIMEOUT = Duration.ofSeconds(90); private static final int MAX_TOKENS = 3_000; + private static final String AI_MODEL = "gpt-3.5-turbo"; private boolean isDisabled = false; private final OpenAiService openAiService; /** * Creates instance of ChatGPTService - * + * * @param config needed for token to OpenAI API. */ public ChatGptService(Config config) { @@ -37,17 +38,34 @@ 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(AI_MODEL) + .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); } /** * Prompt ChatGPT with a question and receive a response. - * + * * @param question The question being asked of ChatGPT. Max is {@value MAX_TOKENS} tokens. + * @return partitioned response from ChatGPT as a String array. * @see ChatGPT * Tokens. - * @return response from ChatGPT as a String. */ - public Optional ask(String question) { + public Optional ask(String question) { if (isDisabled) { return Optional.empty(); } @@ -56,18 +74,25 @@ public Optional ask(String question) { ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), Objects.requireNonNull(question)); ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() - .model("gpt-3.5-turbo") + .model(AI_MODEL) .messages(List.of(chatMessage)) .frequencyPenalty(0.5) - .temperature(0.7) + .temperature(0.3) .maxTokens(MAX_TOKENS) .n(1) .build(); - return Optional.ofNullable(openAiService.createChatCompletion(chatCompletionRequest) + + String response = openAiService.createChatCompletion(chatCompletionRequest) .getChoices() .get(0) .getMessage() - .getContent()); + .getContent(); + + if (response == null) { + return Optional.empty(); + } + + return Optional.of(AIResponseParser.parse(response)); } catch (OpenAiHttpException openAiHttpException) { logger.warn( "There was an error using the OpenAI API: {} Code: {} Type: {} Status Code: {}", diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/package-info.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/package-info.java similarity index 84% rename from application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/package-info.java rename to application/src/main/java/org/togetherjava/tjbot/features/chatgpt/package-info.java index 76c1d28638..37d1d88d99 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/package-info.java @@ -3,7 +3,7 @@ */ @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault -package org.togetherjava.tjbot.features.chaptgpt; +package org.togetherjava.tjbot.features.chatgpt; import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault; 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..1fa5dcaf7c 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,22 @@ 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.utils.MessageUtils; +import org.togetherjava.tjbot.features.chatgpt.ChatGptCommand; +import org.togetherjava.tjbot.features.chatgpt.ChatGptService; import javax.annotation.Nullable; -import java.awt.Color; +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; @@ -38,6 +47,8 @@ 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. */ @@ -59,16 +70,23 @@ public final class HelpSystemHelper { private final Set threadActivityTagNames; 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 = 10; + private static final String CHATGPT_FAILURE_MESSAGE = + "You can use %s to ask ChatGPT about your question while you wait for a human to respond."; /** * Creates a new instance. * * @param config the config to use * @param database the database to store help thread metadata in + * @param chatGptService the service used to ask ChatGPT questions via the API. */ - public HelpSystemHelper(Config config, Database database) { + public HelpSystemHelper(Config config, Database database, ChatGptService chatGptService) { HelpSystemConfig helpConfig = config.getHelpSystem(); this.database = database; + this.chatGptService = chatGptService; helpForumPattern = helpConfig.getHelpForumPattern(); isHelpForumName = Pattern.compile(helpForumPattern).asMatchPredicate(); @@ -91,11 +109,10 @@ public HelpSystemHelper(Config config, Database database) { } RestAction sendExplanationMessage(GuildMessageChannel threadChannel) { - return MessageUtils - .mentionGuildSlashCommand(threadChannel.getGuild(), HelpThreadCommand.COMMAND_NAME, - HelpThreadCommand.Subcommand.CLOSE.getCommandName()) - .flatMap(closeCommandMention -> sendExplanationMessage(threadChannel, - closeCommandMention)); + return mentionGuildSlashCommand(threadChannel.getGuild(), HelpThreadCommand.COMMAND_NAME, + HelpThreadCommand.Subcommand.CLOSE.getCommandName()) + .flatMap(closeCommandMention -> sendExplanationMessage(threadChannel, + closeCommandMention)); } private RestAction sendExplanationMessage(GuildMessageChannel threadChannel, @@ -131,6 +148,90 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha return action.setEmbeds(embeds); } + /** + * 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 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 originalQuestion) { + Optional questionOptional = prepareChatGptQuestion(threadChannel, originalQuestion); + Optional chatGPTAnswer; + + if (questionOptional.isEmpty()) { + return useChatGptFallbackMessage(threadChannel); + } + String question = questionOptional.get(); + 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()) { + return useChatGptFallbackMessage(threadChannel); + } + + RestAction message = + mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) + .map(""" + 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) + .flatMap(threadChannel::sendMessage); + + for (String aiResponse : chatGPTAnswer.get()) { + message = message.map(aiResponse::formatted).flatMap(threadChannel::sendMessage); + } + + return message; + } + + private Optional prepareChatGptQuestion(ThreadChannel threadChannel, + String originalQuestion) { + String questionTitle = threadChannel.getName(); + StringBuilder questionBuilder = new StringBuilder(MAX_QUESTION_LENGTH); + + if (originalQuestion.length() < MIN_QUESTION_LENGTH + && questionTitle.length() < MIN_QUESTION_LENGTH) { + return Optional.empty(); + } + + questionBuilder.append(questionTitle).append(" "); + originalQuestion = originalQuestion.substring(0, Math + .min(MAX_QUESTION_LENGTH - questionBuilder.length(), originalQuestion.length())); + + 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)); + } + + questionBuilder.insert(0, tagBuilder); + + return Optional.of(questionBuilder.toString()); + } + + private RestAction useChatGptFallbackMessage(ThreadChannel threadChannel) { + logger.warn("Something went wrong while trying to communicate with the ChatGpt API"); + return mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) + .map(CHATGPT_FAILURE_MESSAGE::formatted) + .flatMap(threadChannel::sendMessage); + } + 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..b38d35fcc1 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,6 +91,13 @@ 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)); diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java new file mode 100644 index 0000000000..715dc14f0c --- /dev/null +++ b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java @@ -0,0 +1,46 @@ +package org.togetherjava.tjbot.features.chatgpt; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +class AIResponseParserTest { + private static final Logger logger = LoggerFactory.getLogger(AIResponseParserTest.class); + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4}) + void correctResponseLength(int fileNumber) { + try (InputStream in = getClass().getClassLoader() + .getResourceAsStream("AITestResponses/test" + fileNumber + ".txt")) { + String response = new String(Objects.requireNonNull(in).readAllBytes()); + String[] aiResponse = AIResponseParser.parse(response); + + testResponseLength(aiResponse); + toLog(aiResponse); + } catch (IOException | NullPointerException ex) { + logger.error("{}", ex.getMessage()); + Assertions.fail(); + } + } + + private void testResponseLength(String[] responses) { + int AI_RESPONSE_CHARACTER_LIMIT = 2000; + for (String response : responses) { + Assertions.assertTrue(response.length() <= AI_RESPONSE_CHARACTER_LIMIT, + "Response length is NOT within character limit: " + response.length()); + logger.warn("Response length was: {}", response.length()); + } + } + + private void toLog(String[] responses) { + for (String response : responses) { + logger.info(response); + } + } +} diff --git a/application/src/test/resources/AITestResponses/test1.txt b/application/src/test/resources/AITestResponses/test1.txt new file mode 100644 index 0000000000..beda6295c5 --- /dev/null +++ b/application/src/test/resources/AITestResponses/test1.txt @@ -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 diff --git a/application/src/test/resources/AITestResponses/test2.txt b/application/src/test/resources/AITestResponses/test2.txt new file mode 100644 index 0000000000..62c70602e5 --- /dev/null +++ b/application/src/test/resources/AITestResponses/test2.txt @@ -0,0 +1,63 @@ +Sure, I can help you with setting up a Docker reverse proxy with Nginx. Here's a step-by-step guide: + +Step 1: Install Docker and Docker Compose +Make sure you have Docker and Docker Compose installed on your system. You can follow the official documentation for installation instructions. + +Step 2: Create a new directory +Create a new directory where you will store your Nginx configuration files. For example, create a directory called "nginx-proxy" in your home directory. + +Step 3: Create the Nginx configuration file +Inside the "nginx-proxy" directory, create a file called "default.conf" and open it in a text editor. This file will contain the reverse proxy configuration. + +Here's an example of a basic Nginx reverse proxy configuration: + +``` +server { + listen 80; + server_name yourdomain.com; + + location / { + proxy_pass http://your-app-container:port; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +Replace "yourdomain.com" with your actual domain name or IP address. Replace "your-app-container" with the name of your Docker container running the application you want to proxy. Replace "port" with the port number on which your application is running inside the container. + +Save and close the file. + +Step 4: Create a Docker Compose file +Inside the "nginx-proxy" directory, create another file called "docker-compose.yml" and open it in a text editor. This file will define the services required for running Nginx as a reverse proxy. + +Here's an example of a basic Docker Compose configuration: + +``` +version: '3' +services: + nginx: + image: nginx + ports: + - 80:80 + volumes: + - ./default.conf:/etc/nginx/conf.d/default.conf + restart: always +``` + +Save and close the file. + +Step 5: Start the Docker containers +Open a terminal or command prompt, navigate to the "nginx-proxy" directory, and run the following command to start the Docker containers: + +``` +docker-compose up -d +``` + +This will start the Nginx +container as a reverse proxy using the configuration specified in the "default.conf" file. + +Step 6: Test the reverse proxy +Assuming your DNS or hosts file is properly configured, you should now be able to access your application through the reverse proxy. Open a web browser and enter your domain name or IP address. The request will be forwarded to your application running inside the Docker container. + +That's it! You have successfully set up a Docker reverse proxy with Nginx. You can add more server blocks in the "default.conf" file to configure additional reverse proxies for different applications if needed \ No newline at end of file diff --git a/application/src/test/resources/AITestResponses/test3.txt b/application/src/test/resources/AITestResponses/test3.txt new file mode 100644 index 0000000000..8c58a44f50 --- /dev/null +++ b/application/src/test/resources/AITestResponses/test3.txt @@ -0,0 +1,80 @@ +```java +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +public class Game extends JFrame implements KeyListener { + private int characterX = 50; + private int characterY = 200; + private int obstacleX = 600; + private int obstacleY = 200; + + public Game() { + setTitle("Game"); + setSize(800, 400); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setResizable(false); + setLocationRelativeTo(null); + + addKeyListener(this); + + setVisible(true); + } + + public void paint(Graphics g) { + super.paint(g); + + g.setColor(Color.BLACK); + g.fillRect(characterX, characterY, 50, 50); // Draw character + + g.setColor(Color.RED); + g.fillRect(obstacleX, obstacleY, 20, 100); // Draw obstacle + } + + public void update() { + characterX += 5; // Move character horizontally + + if (characterX >= obstacleX && characterX <= obstacleX + 20 && characterY >= obstacleY && characterY <= obstacleY + 100) { + System.out.println("Game Over!"); + System.exit(0); + } + + if (characterX > getWidth()) { + characterX = -50; // Reset character position + obstacleY = (int) (Math.random() * getHeight()); // Randomize obstacle position + } + + repaint(); + } + + public static void main(String[] args) { + Game game = new Game(); + + while (true) { + game.update(); + + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public void keyTyped(KeyEvent e) {} + + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_UP && characterY > 0) { + characterY -= 10; // Move character up + } else if (e.getKeyCode() == KeyEvent.VK_DOWN && characterY < getHeight() - 50) { + characterY += 10; // Move character down + } + } + + @Override + public void keyReleased(KeyEvent e) {} +} +``` \ No newline at end of file diff --git a/application/src/test/resources/AITestResponses/test4.txt b/application/src/test/resources/AITestResponses/test4.txt new file mode 100644 index 0000000000..5373c0ea84 --- /dev/null +++ b/application/src/test/resources/AITestResponses/test4.txt @@ -0,0 +1,27 @@ +Software engineering is a multidisciplinary field that encompasses various aspects of developing, designing, testing, and maintaining software systems. It involves the application of engineering principles and practices to create reliable, efficient, and high-quality software solutions. Let's delve into the different aspects of software engineering in detail: + +1. Software Development Life Cycle (SDLC): The SDLC is a systematic approach to software development that consists of several phases including requirements gathering, system design, coding, testing, deployment, and maintenance. Each phase has its own set of activities and deliverables to ensure the successful development of software. + +2. Requirements Engineering: This phase involves understanding and documenting the needs and expectations of stakeholders. It includes gathering functional and non-functional requirements, analyzing them for feasibility, prioritizing them, and creating a comprehensive requirements specification document. + +3. System Design: In this phase, the overall architecture and structure of the software system are defined. It includes creating high-level designs that outline the system components, their interactions, data flow diagrams, database schemas, user interfaces, etc. + +4. Coding: The coding phase involves writing the actual source code based on the design specifications. It requires expertise in programming languages such as Java, C++, Python or JavaScript to implement algorithms and logic to achieve desired functionality. + +5. Testing: This aspect ensures that the developed software meets quality standards by identifying defects or bugs through various testing techniques such as unit testing (testing individual components), integration testing (testing interactions between components), system testing (testing entire system functionality), performance testing (evaluating system performance under load), etc. + +6. Software Configuration Management: This aspect deals with managing changes to software artifacts throughout their lifecycle. It includes version control systems (e.g., Git), build automation tools (e.g., Jenkins), release management processes to ensure proper tracking and control over changes made during development. + +7. Software Maintenance: After deployment, software requires ongoing maintenance to fix bugs or issues, enhance functionality, and adapt to changing requirements. This includes activities like bug fixing, patching, performance optimization, and software updates. + +8. Software Quality Assurance: This aspect focuses on ensuring that the software meets specified quality standards. It involves defining quality metrics, conducting code reviews, performing audits, and implementing quality control processes to identify and rectify defects. + +9. Software Project Management: Effective project management is crucial for successful software development. It involves planning, organizing, coordinating resources (human, financial), setting milestones and deadlines, managing risks, and ensuring timely delivery of high-quality software. + +10. Software Documentation: Comprehensive documentation is essential for understanding the software system's design, functionality, and usage. It includes requirements documents, design specifications, user manuals, API documentation, and other technical documents to aid developers and users. + +11. Software Engineering Ethics: Ethical considerations are important in software engineering to ensure responsible behavior towards stakeholders. This includes protecting user privacy and data security, adhering to legal regulations (e.g., GDPR), avoiding conflicts of interest or bias in decision-making processes. + +12. Software Engineering Tools: Various tools support different aspects of software engineering such as Integrated Development Environments (IDEs) like Visual Studio or Eclipse for coding; project management tools like Jira or Trello for tracking tasks; testing frameworks like JUnit or Selenium for automated testing; collaboration tools like Slack or Microsoft Teams for communication among team members. + +These aspects collectively contribute to the successful development of reliable software systems that meet user requirements while adhering to industry best practices and ethical standards. \ No newline at end of file