From a2e96d589f91f43431180d258e431a03ba90c9fb Mon Sep 17 00:00:00 2001 From: Suleman Date: Thu, 14 Mar 2024 23:22:34 +0000 Subject: [PATCH 1/2] Added GuessLanguageCommand --- .../java/com/togetherjava/tjplays/Bot.java | 4 +++- .../commands/GuessLanguageCommand.java | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/togetherjava/tjplays/listeners/commands/GuessLanguageCommand.java diff --git a/app/src/main/java/com/togetherjava/tjplays/Bot.java b/app/src/main/java/com/togetherjava/tjplays/Bot.java index e893874..160acae 100644 --- a/app/src/main/java/com/togetherjava/tjplays/Bot.java +++ b/app/src/main/java/com/togetherjava/tjplays/Bot.java @@ -23,7 +23,7 @@ public static void main(String[] args) throws IOException { private static Properties readProperties(String... args) throws IOException { Properties properties = new Properties(); - String configPath = args.length == 0 ? "bot-config.properties" : args[0]; + String configPath = args.length == 0 ? "app/src/bot-config.properties" : args[0]; properties.load(new FileInputStream(configPath)); return properties; @@ -47,7 +47,9 @@ private static JDA createJDA(String botToken) { private static List getCommands() { return List.of( new PingCommand(), + new GuessLanguageCommand(), new Game2048Command() + ); } } diff --git a/app/src/main/java/com/togetherjava/tjplays/listeners/commands/GuessLanguageCommand.java b/app/src/main/java/com/togetherjava/tjplays/listeners/commands/GuessLanguageCommand.java new file mode 100644 index 0000000..fd18cb4 --- /dev/null +++ b/app/src/main/java/com/togetherjava/tjplays/listeners/commands/GuessLanguageCommand.java @@ -0,0 +1,20 @@ +package com.togetherjava.tjplays.listeners.commands; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; + +public final class GuessLanguageCommand extends SlashCommand{ + + private static final String COMMAND_NAME = "guess-programming-language"; + public GuessLanguageCommand() { + super(Commands.slash(COMMAND_NAME, "Try to guess the programming language")); + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + + event.reply("Hello").queue(); + + } +} From 9c7af184185bc9422e76efae4d794ffa783e9e9b Mon Sep 17 00:00:00 2001 From: Suleman Date: Sat, 16 Mar 2024 16:06:45 +0000 Subject: [PATCH 2/2] Added ChatGptService and AIResponseParser Class --- app/bot-config.properties.template | 3 +- app/build.gradle | 7 +- .../java/com/togetherjava/tjplays/Bot.java | 1 + .../services/chatgpt/AIResponseParser.java | 82 +++++++++++ .../services/chatgpt/ChatGptService.java | 134 ++++++++++++++++++ 5 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/togetherjava/tjplays/services/chatgpt/AIResponseParser.java create mode 100644 app/src/main/java/com/togetherjava/tjplays/services/chatgpt/ChatGptService.java diff --git a/app/bot-config.properties.template b/app/bot-config.properties.template index 0b77bed..bfe0f7b 100644 --- a/app/bot-config.properties.template +++ b/app/bot-config.properties.template @@ -1 +1,2 @@ -BOT_TOKEN= +BOT_TOKEN= +OPEN_AI_TOKEN= diff --git a/app/build.gradle b/app/build.gradle index 49dc925..682e8b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,7 +31,9 @@ jib { setCreationTime(java.time.Instant.now().toString()) } } - +ext { + chatGPTVersion = '0.18.0' +} shadowJar { archiveBaseName.set('TJ-Plays') archiveClassifier.set('') @@ -40,7 +42,8 @@ shadowJar { dependencies { implementation 'net.dv8tion:JDA:5.0.0-beta.3' - + implementation "com.theokanning.openai-gpt3-java:api:$chatGPTVersion" + implementation "com.theokanning.openai-gpt3-java:service:$chatGPTVersion" testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1' } diff --git a/app/src/main/java/com/togetherjava/tjplays/Bot.java b/app/src/main/java/com/togetherjava/tjplays/Bot.java index 160acae..83109d4 100644 --- a/app/src/main/java/com/togetherjava/tjplays/Bot.java +++ b/app/src/main/java/com/togetherjava/tjplays/Bot.java @@ -16,6 +16,7 @@ public static void main(String[] args) throws IOException { Properties properties = readProperties(args); String botToken = properties.getProperty("BOT_TOKEN"); + String chatGptToken = properties.getProperty("OPEN_AI_TOKEN"); createJDA(botToken); } diff --git a/app/src/main/java/com/togetherjava/tjplays/services/chatgpt/AIResponseParser.java b/app/src/main/java/com/togetherjava/tjplays/services/chatgpt/AIResponseParser.java new file mode 100644 index 0000000..e321c08 --- /dev/null +++ b/app/src/main/java/com/togetherjava/tjplays/services/chatgpt/AIResponseParser.java @@ -0,0 +1,82 @@ +package com.togetherjava.tjplays.services.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/app/src/main/java/com/togetherjava/tjplays/services/chatgpt/ChatGptService.java b/app/src/main/java/com/togetherjava/tjplays/services/chatgpt/ChatGptService.java new file mode 100644 index 0000000..9cb4750 --- /dev/null +++ b/app/src/main/java/com/togetherjava/tjplays/services/chatgpt/ChatGptService.java @@ -0,0 +1,134 @@ +package com.togetherjava.tjplays.services.chatgpt; + +import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; +import com.theokanning.openai.service.OpenAiService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +//import org.togetherjava.tjbot.config.Config; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Service used to communicate to OpenAI API to generate responses. + */ +public class ChatGptService { + private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class); + private static final Duration TIMEOUT = Duration.ofSeconds(90); + + /** The maximum number of tokens allowed for the generated answer. */ + private static final int MAX_TOKENS = 3_000; + + /** + * This parameter reduces the likelihood of the AI repeating itself. A higher frequency penalty + * makes the model less likely to repeat the same lines verbatim. It helps in generating more + * diverse and varied responses. + */ + private static final double FREQUENCY_PENALTY = 0.5; + + /** + * This parameter controls the randomness of the AI's responses. A higher temperature results in + * more varied, unpredictable, and creative responses. Conversely, a lower temperature makes the + * model's responses more deterministic and conservative. + */ + private static final double TEMPERATURE = 0.8; + + /** + * n: This parameter specifies the number of responses to generate for each prompt. If n is more + * than 1, the AI will generate multiple different responses to the same prompt, each one being + * a separate iteration based on the input. + */ + private static final int MAX_NUMBER_OF_RESPONSES = 1; + private static final String AI_MODEL = "gpt-3.5-turbo"; + + private boolean isDisabled = false; + private OpenAiService openAiService; + + /** + * Creates instance of ChatGPTService + * + * @param config needed for token to OpenAI API. + */ + public ChatGptService(String apiKey) { + boolean keyIsDefaultDescription = apiKey.startsWith("<") && apiKey.endsWith(">"); + if (apiKey.isBlank() || keyIsDefaultDescription) { + isDisabled = true; + return; + } + + openAiService = new OpenAiService(apiKey, TIMEOUT); + + ChatMessage setupMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), """ + 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(FREQUENCY_PENALTY) + .temperature(TEMPERATURE) + .maxTokens(50) + .n(MAX_NUMBER_OF_RESPONSES) + .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. + * @param context The category of asked question, to set the context(eg. Java, Database, Other + * etc). + * @return partitioned response from ChatGPT as a String array. + * @see ChatGPT + * Tokens. + */ + public Optional ask(String question, String context) { + if (isDisabled) { + return Optional.empty(); + } + + try { + String instructions = "KEEP IT CONCISE, NOT MORE THAN 280 WORDS"; + String questionWithContext = "context: Category %s on a Java Q&A discord server. %s %s" + .formatted(context, instructions, question); + ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), + Objects.requireNonNull(questionWithContext)); + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(AI_MODEL) + .messages(List.of(chatMessage)) + .frequencyPenalty(FREQUENCY_PENALTY) + .temperature(TEMPERATURE) + .maxTokens(MAX_TOKENS) + .n(MAX_NUMBER_OF_RESPONSES) + .build(); + + String response = openAiService.createChatCompletion(chatCompletionRequest) + .getChoices() + .get(0) + .getMessage() + .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: {}", + 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(); + } +}