Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a72b5db
Update ChatGPTCommand.java to include character limiting and rate lim…
Apr 4, 2023
11f3e8a
Requested changes to ChatGptCommand.java:
Apr 5, 2023
cf43e51
Requested changes to ChatGptCommand.java:
Apr 5, 2023
b223792
Add new catch for RuntimeException to ChatGptService.java in order to…
Apr 7, 2023
f054e20
Change Help system to also include having ChatGPT take a shot at the …
Apr 9, 2023
7440572
Add checks to determine length of question and thus where question sh…
Apr 9, 2023
4503f3f
Fix to ensure action is completed via flatmap versus using complete()
Apr 9, 2023
6b547dc
Fix-up logic of code and create better output strings for UX.
Apr 10, 2023
9851358
Include JavaDocs for new methods.
Apr 10, 2023
62ca7b0
Requested change to time calculation for OnSlashCommand of ChatGptCom…
Apr 14, 2023
9f842e4
Revert "Include JavaDocs for new methods."
Apr 14, 2023
bdfb5a4
Revert "Requested change to time calculation for OnSlashCommand of Ch…
Apr 14, 2023
efc92d5
Revert "Revert "Include JavaDocs for new methods.""
Apr 14, 2023
1371381
Merge branch 'develop' into chatGPT-attempt-questions
tmcdonnell2 Apr 17, 2023
b2c7710
Requested changes to Help thread AI generated response code.
Apr 17, 2023
ff775be
Fix AI response in Discord thread to be explanation message followed …
Apr 18, 2023
c3803ff
- Added more context for questions. Either use both the title and mes…
Apr 20, 2023
b8bbcf0
- Fix statement to capture non-helpful AI responses.
Apr 20, 2023
a5113e9
- Remove use of regex in order to avoid long running task. Replace by…
Apr 20, 2023
884cbd6
- Refactor on method names and structure of calling methods.
Apr 21, 2023
c832721
Requested refactor of constructChatGptAttempt():
Apr 22, 2023
ec10fe1
Some of the requested refactor of constructChatGptAttempt():
Apr 28, 2023
881bfd8
- Change loop to stream for better readability
May 9, 2023
aca111f
- Reimplement loops for building question.
May 18, 2023
f8d25a4
ChatGptService
May 21, 2023
22b1dcd
- Remove key in file.
May 21, 2023
46b87ff
- Refactor of code to tune and test ChatGPT giving answers within cha…
May 24, 2023
b9dc0bc
- Change ChatGPTService.ask() to return an Optional<String[]>.
May 29, 2023
54e739e
- Change ChatGptService.java to attempt to break up long messages. Tr…
Jun 5, 2023
a3f0743
- Add ChatGptAPITest (rename ChatGptTest)
tmcdonnell2 Jul 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ public static Collection<Feature> 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
Expand Down
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
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -73,14 +74,20 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
public void onModalSubmitted(ModalInteractionEvent event, List<String> args) {
event.deferReply().queue();

Optional<String> optional =
Optional<String[]> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
*/
public class ChatGptService {
private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class);
private static final Duration TIMEOUT = Duration.ofSeconds(10);
private static final Duration TIMEOUT = Duration.ofSeconds(120);
private static final int MAX_TOKENS = 3_000;
private boolean isDisabled = false;
private final OpenAiService openAiService;

/**
* Creates instance of ChatGPTService
*
*
* @param config needed for token to OpenAI API.
*/
public ChatGptService(Config config) {
Expand All @@ -37,17 +37,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("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);
}

/**
* 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 <a href="https://platform.openai.com/docs/guides/chat/managing-tokens">ChatGPT
* Tokens</a>.
* @return response from ChatGPT as a String.
*/
public Optional<String> ask(String question) {
public Optional<String[]> ask(String question) {
if (isDisabled) {
return Optional.empty();
}
Expand All @@ -59,15 +76,18 @@ public Optional<String> 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();
return Optional.ofNullable(openAiService.createChatCompletion(chatCompletionRequest)

String response = openAiService.createChatCompletion(chatCompletionRequest)
.getChoices()
.get(0)
.getMessage()
.getContent());
.getContent();

return AIResponseParser.parse(response);
} catch (OpenAiHttpException openAiHttpException) {
logger.warn(
"There was an error using the OpenAI API: {} Code: {} Type: {} Status Code: {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
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.
*/
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that's already an issue with /chatgpt that is on master. It silently crashes if it's over the character limit.

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.

Copy link
Member

Choose a reason for hiding this comment

The 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.
But it should work for most cases, and we don't cut the answer of or anything.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 (limit - 500 for optimal results), with a fallback. Or we let it post the full answer as a file.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Thus, we should fit answers in a file?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The 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 max / 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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));
}

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -90,9 +91,17 @@ private void handleHelpThreadCreated(ThreadChannel threadChannel) {
SERVICE.schedule(createMessages, 5, TimeUnit.SECONDS);
}

private RestAction<Message> createAIResponse(ThreadChannel threadChannel) {
RestAction<Message> originalQuestion =
threadChannel.retrieveMessageById(threadChannel.getIdLong());
return originalQuestion.flatMap(
message -> helper.constructChatGptAttempt(threadChannel, message.getContentRaw()));
}

private RestAction<Message> createMessages(ThreadChannel threadChannel) {
return sendHelperHeadsUp(threadChannel).flatMap(Message::pin)
.flatMap(any -> helper.sendExplanationMessage(threadChannel));
.flatMap(any -> helper.sendExplanationMessage(threadChannel))
.flatMap(any -> threadChannel.retrieveMessageById(threadChannel.getIdLong()));
}

private RestAction<Message> sendHelperHeadsUp(ThreadChannel threadChannel) {
Expand Down
Loading