-
-
Notifications
You must be signed in to change notification settings - Fork 104
ChatGPT attempts new questions from the questions channel #820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a72b5db
11f3e8a
cf43e51
b223792
f054e20
7440572
4503f3f
6b547dc
9851358
62ca7b0
9f842e4
bdfb5a4
efc92d5
1371381
b2c7710
ff775be
c3803ff
b8bbcf0
a5113e9
884cbd6
c832721
ec10fe1
881bfd8
aca111f
f8d25a4
22b1dcd
46b87ff
b9dc0bc
54e739e
a3f0743
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String[]> 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<CodeBlockIndexPair> 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<String> 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; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,20 +24,33 @@ | |
| 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.Color; | ||
| import java.awt.*; | ||
tmcdonnell2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| 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. | ||
| */ | ||
|
|
@@ -59,15 +72,22 @@ public final class HelpSystemHelper { | |
| private final Set<String> 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 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 +151,90 @@ private RestAction<Message> 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<Message> constructChatGptAttempt(ThreadChannel threadChannel, | ||
| String originalQuestion) { | ||
| Optional<String> questionOptional = prepareChatGptQuestion(threadChannel, originalQuestion); | ||
| Optional<String[]> chatGPTAnswer; | ||
|
|
||
| String question = questionOptional.orElseThrow(); | ||
| logger.debug("The final question sent to chatGPT: {}", question); | ||
| chatGPTAnswer = chatGptService.ask(question); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the response is over the character limit? ChatGPT often gives really big responses, so I can't imagine this to not happen.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, that's already an issue with And just cutting the answer to fit the limit is a bad solution, obviously. What's the point of getting 80% of the answer, if it's a code example or a step-by-step guide? Code won't work, guide is incomplete and useless. So one solution is to prompt it to fit the answer within limits, and test it to make sure it works well. With a hard cutoff if a limit is reached. Another would be to post lengthier answers as a file.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we simply ask ChatGPT to limit the answer to a certain limit. (within 4000 characters) ChatGPT can still exceed that limit, so we'd still require a fallback.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's one of my proposed solutions. We either tell it to fit the answer withing the limit ( I like both of them. First one is short and neat, no files. My only worry is that the quality of the answers might lack. For example, can you fit a coherent guide how to install and setup something, with examples, within the limit.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested few examples, it seems embed length is enough for most guides with examples. My worry with embeds is that they are too narrow for code, it might ruin the formatting. Since code would be needed in almost every answer, it's something to consider. And 2000 char limit or regular messages is likely not enough for bigger answers. Close, but not quite there.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Thus, we should fit answers in a file?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Try embeds first, for cosistency. Post a picture of a more complicated answer with code examples. Files are the last restort, since they are kinda ugly and weird UI wise. You know, getting a file in response to a bot command.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A file is fine for PC, you have good UX then, not as good as just a message but it's fine. On phone, yeah uh, have fun opening notepad on your phone to read it. |
||
|
|
||
| if (chatGPTAnswer.isEmpty()) { | ||
| logger.warn("Something went wrong while trying to communicate with the ChatGpt API"); | ||
| return useChatGptFallbackMessage(threadChannel); | ||
| } | ||
|
|
||
| UnaryOperator<String> 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; | ||
|
|
||
| RestAction<Message> message = | ||
| mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) | ||
| .map(preambleResponse) | ||
| .flatMap(threadChannel::sendMessage); | ||
|
|
||
| for (String aiResponse : chatGPTAnswer.get()) { | ||
| message = message.map(aiResponse::formatted).flatMap(threadChannel::sendMessage); | ||
| } | ||
|
|
||
| return message; | ||
| } | ||
|
|
||
| private Optional<String> prepareChatGptQuestion(ThreadChannel threadChannel, | ||
| String originalQuestion) { | ||
| String questionTitle = threadChannel.getName(); | ||
| StringBuilder questionBuilder = new StringBuilder(MAX_QUESTION_LENGTH); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i suppose questions are rarely max length, are they? try to go for something that is more common in practice, maybe
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You would be surprised just how fast 200 characters gets filled up. Just taking some sample questions that are on the forum right now they all are around 200 or exceed 200 characters (MAX_LENGTH as defined of now). If the person includes almost any code (and with Java being verbose) the characters are destroyed. It might even be good if we carve out code in the future and just try to get to the human language bits of the question. The other bit is that I add the tags to the question if a question is too short thus pushing up question lengths. |
||
|
|
||
| if (originalQuestion.length() < MIN_QUESTION_LENGTH | ||
| && questionTitle.length() < MIN_QUESTION_LENGTH) { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| questionBuilder.append(questionTitle).append(" "); | ||
| if (originalQuestion.length() > MAX_QUESTION_LENGTH - questionBuilder.length()) { | ||
| originalQuestion = | ||
| originalQuestion.substring(0, MAX_QUESTION_LENGTH - questionBuilder.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)); | ||
| } | ||
tmcdonnell2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| questionBuilder.insert(0, tagBuilder); | ||
|
|
||
| return Optional.of(questionBuilder.toString()); | ||
| } | ||
|
|
||
| private RestAction<Message> useChatGptFallbackMessage(ThreadChannel threadChannel) { | ||
| 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) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.