diff --git a/application/build.gradle b/application/build.gradle index 39b9a2a20b..d0556d9263 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -80,6 +80,9 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + + implementation "com.theokanning.openai-gpt3-java:api:$chatGPTVersion" + implementation "com.theokanning.openai-gpt3-java:service:$chatGPTVersion" } application { diff --git a/application/config.json.template b/application/config.json.template index da7081543f..84ac087cb0 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -88,4 +88,5 @@ ], "logInfoChannelWebhook": "", "logErrorChannelWebhook": "" + "openaiApiKey": "" } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 830d4cadde..e5933ca262 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -36,6 +36,7 @@ public final class Config { private final String mediaOnlyChannelPattern; private final String logInfoChannelWebhook; private final String logErrorChannelWebhook; + private final String openaiApiKey; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -70,7 +71,8 @@ private Config(@JsonProperty(value = "token", required = true) String token, @JsonProperty(value = "logInfoChannelWebhook", required = true) String logInfoChannelWebhook, @JsonProperty(value = "logErrorChannelWebhook", - required = true) String logErrorChannelWebhook) { + required = true) String logErrorChannelWebhook, + @JsonProperty(value = "openaiApiKey", required = true) String openaiApiKey) { this.token = Objects.requireNonNull(token); this.gistApiKey = Objects.requireNonNull(gistApiKey); this.databasePath = Objects.requireNonNull(databasePath); @@ -93,6 +95,7 @@ private Config(@JsonProperty(value = "token", required = true) String token, this.blacklistedFileExtension = Objects.requireNonNull(blacklistedFileExtension); this.logInfoChannelWebhook = Objects.requireNonNull(logInfoChannelWebhook); this.logErrorChannelWebhook = Objects.requireNonNull(logErrorChannelWebhook); + this.openaiApiKey = Objects.requireNonNull(openaiApiKey); } /** @@ -304,4 +307,13 @@ public String getLogInfoChannelWebhook() { public String getLogErrorChannelWebhook() { return logErrorChannelWebhook; } + + /** + * The OpenAI token needed for communicating with OpenAI ChatGPT. + * + * @return the OpenAI API Token + */ + public String getOpenaiApiKey() { + return openaiApiKey; + } } 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 0130fc5b18..7f1f3af046 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -6,6 +6,8 @@ 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.code.CodeMessageAutoDetection; import org.togetherjava.tjbot.features.code.CodeMessageHandler; import org.togetherjava.tjbot.features.code.CodeMessageManualDetection; @@ -72,6 +74,7 @@ public static Collection createFeatures(JDA jda, Database database, Con ScamHistoryStore scamHistoryStore = new ScamHistoryStore(database); HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database); CodeMessageHandler codeMessageHandler = new CodeMessageHandler(); + ChatGptService chatGptService = new ChatGptService(config); // 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 @@ -139,7 +142,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new HelpThreadCommand(config, helpSystemHelper)); features.add(new ReportCommand(config)); features.add(new BookmarksCommand(bookmarksSystem)); - + features.add(new ChatGptCommand(chatGptService)); return features; } } 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 new file mode 100644 index 0000000000..ce1b9c02f0 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptCommand.java @@ -0,0 +1,37 @@ +package org.togetherjava.tjbot.features.chaptgpt; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; + +import org.togetherjava.tjbot.features.CommandVisibility; +import org.togetherjava.tjbot.features.SlashCommandAdapter; + +/** + * 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 final ChatGptService chatGptService; + + /** + * Creates an instance of the chatgpt command. + * + * @param chatGptService ChatGptService - Needed to make calls to ChatGPT API + */ + 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?", true); + } + + @Override + public void onSlashCommand(SlashCommandInteractionEvent event) { + 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"); + 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 new file mode 100644 index 0000000000..55c6eeb9bf --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/ChatGptService.java @@ -0,0 +1,79 @@ +package org.togetherjava.tjbot.features.chaptgpt; + +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(10); + 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) { + String apiKey = config.getOpenaiApiKey(); + if (apiKey.isBlank()) { + isDisabled = true; + } + + openAiService = new OpenAiService(apiKey, TIMEOUT); + } + + /** + * Prompt ChatGPT with a question and receive a response. + * + * @param question The question being asked of ChatGPT. Max is {@value MAX_TOKENS} tokens. + * @see ChatGPT + * Tokens. + * @return response from ChatGPT as a String. + */ + public Optional ask(String question) { + if (isDisabled) { + return Optional.empty(); + } + + try { + ChatMessage chatMessage = + new ChatMessage(ChatMessageRole.USER.value(), Objects.requireNonNull(question)); + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model("gpt-3.5-turbo") + .messages(List.of(chatMessage)) + .frequencyPenalty(0.5) + .temperature(0.7) + .maxTokens(MAX_TOKENS) + .n(1) + .build(); + return Optional.ofNullable(openAiService.createChatCompletion(chatCompletionRequest) + .getChoices() + .get(0) + .getMessage() + .getContent()); + } 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); + } + return Optional.empty(); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/package-info.java b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/package-info.java new file mode 100644 index 0000000000..76c1d28638 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/chaptgpt/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains the functionality to connect with ChatGPT via API calls. + */ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package org.togetherjava.tjbot.features.chaptgpt; + +import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/build.gradle b/build.gradle index b24c08bb33..b3ddd1a4e4 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ version '1.0-SNAPSHOT' ext { jooqVersion = '3.18.0' jacksonVersion = '2.14.0' + chatGPTVersion = '0.11.1' } // Skips sonarlint during the build, useful for testing purposes.