Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -29,6 +29,7 @@
import org.togetherjava.tjbot.features.chatgpt.ChatGptService;
import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor;
import org.togetherjava.tjbot.features.utils.Guilds;
import org.togetherjava.tjbot.features.utils.LinkDetection;

import java.awt.Color;
import java.time.Instant;
Expand All @@ -41,6 +42,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
Expand All @@ -57,6 +59,7 @@
public final class HelpSystemHelper {
private static final Logger logger = LoggerFactory.getLogger(HelpSystemHelper.class);
private static final ChatGptModel CHAT_GPT_MODEL = ChatGptModel.FAST;
private static final String BROKEN_LINK_REPLACEMENT = "(broken link removed)";

static final Color AMBIENT_COLOR = new Color(255, 255, 165);

Expand All @@ -74,6 +77,7 @@ public final class HelpSystemHelper {

private final Database database;
private final ChatGptService chatGptService;
private final Function<String, CompletableFuture<String>> brokenLinkReplacer;
private static final int MAX_QUESTION_LENGTH = 200;
private static final int MIN_QUESTION_LENGTH = 10;
private static final String CHATGPT_FAILURE_MESSAGE =
Expand All @@ -87,9 +91,16 @@ public final class HelpSystemHelper {
* @param chatGptService the service used to ask ChatGPT questions via the API.
*/
public HelpSystemHelper(Config config, Database database, ChatGptService chatGptService) {
this(config, database, chatGptService,
answer -> LinkDetection.replaceBrokenLinks(answer, BROKEN_LINK_REPLACEMENT));
}

HelpSystemHelper(Config config, Database database, ChatGptService chatGptService,
Function<String, CompletableFuture<String>> brokenLinkReplacer) {
HelpSystemConfig helpConfig = config.getHelpSystem();
this.database = database;
this.chatGptService = chatGptService;
this.brokenLinkReplacer = brokenLinkReplacer;

isTagManageRole = Pattern.compile(config.getTagManageRolePattern()).asMatchPredicate();
helpForumPattern = helpConfig.getHelpForumPattern();
Expand Down Expand Up @@ -188,6 +199,7 @@ RestAction<Message> constructChatGptAttempt(ThreadChannel threadChannel,
public MessageEmbed generateGptResponseEmbed(String answer, SelfUser selfUser, String title,
ChatGptModel model) {
String responseByGptFooter = "- AI generated response using %s model".formatted(model);
String sanitizedAnswer = sanitizeBrokenLinks(answer);

int embedTitleLimit = MessageEmbed.TITLE_MAX_LENGTH;
String capitalizedTitle = Character.toUpperCase(title.charAt(0)) + title.substring(1);
Expand All @@ -199,12 +211,21 @@ public MessageEmbed generateGptResponseEmbed(String answer, SelfUser selfUser, S
return new EmbedBuilder()
.setAuthor(selfUser.getName(), null, selfUser.getEffectiveAvatarUrl())
.setTitle(titleForEmbed)
.setDescription(answer)
.setDescription(sanitizedAnswer)
.setColor(Color.pink)
.setFooter(responseByGptFooter)
.build();
}

private String sanitizeBrokenLinks(String answer) {
try {
return brokenLinkReplacer.apply(answer).join();
} catch (RuntimeException runtimeException) {
logger.debug("Failed to replace broken links in ChatGPT response", runtimeException);
return answer;
}
}

private Button generateDismissButton(ComponentIdInteractor componentIdInteractor, String id) {
String buttonId = componentIdInteractor.generateComponentId(id);
return Button.danger(buttonId, "Dismiss");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.togetherjava.tjbot.features.help;

import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.SelfUser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import org.togetherjava.tjbot.config.Config;
import org.togetherjava.tjbot.config.HelpSystemConfig;
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.db.generated.tables.HelpThreads;
import org.togetherjava.tjbot.features.chatgpt.ChatGptModel;
import org.togetherjava.tjbot.features.chatgpt.ChatGptService;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

final class HelpSystemHelperTest {
private static final String ORIGINAL_ANSWER = """
Useful links:
- https://broken.example
- https://working.example
""";
private static final String SANITIZED_ANSWER = """
Useful links:
- (broken link removed)
- https://working.example
""";
private HelpSystemHelper helper;
private SelfUser selfUser;

@BeforeEach
void setUp() {
Config config = mock(Config.class);
HelpSystemConfig helpSystemConfig = mock(HelpSystemConfig.class);
when(config.getHelpSystem()).thenReturn(helpSystemConfig);
when(config.getTagManageRolePattern()).thenReturn("tag-manage");
when(helpSystemConfig.getHelpForumPattern()).thenReturn("questions");
when(helpSystemConfig.getCategories()).thenReturn(List.of("java"));
when(helpSystemConfig.getCategoryRoleSuffix()).thenReturn(" helper");

Database database = Database.createMemoryDatabase(HelpThreads.HELP_THREADS);
ChatGptService chatGptService = mock(ChatGptService.class);
helper = new HelpSystemHelper(config, database, chatGptService, answer -> CompletableFuture
.completedFuture(answer.replace("https://broken.example", "(broken link removed)")));

selfUser = mock(SelfUser.class);
when(selfUser.getName()).thenReturn("TJ-Bot");
when(selfUser.getEffectiveAvatarUrl()).thenReturn("https://example.com/avatar.png");
}

@Test
@DisplayName("Replaces broken links before building AI response embed")
void replacesBrokenLinksInGeneratedEmbed() {
MessageEmbed embed = helper.generateGptResponseEmbed(ORIGINAL_ANSWER, selfUser,
"example question", ChatGptModel.FAST);

assertEquals(SANITIZED_ANSWER, embed.getDescription());
}

@Test
@DisplayName("Keeps original answer if broken-link replacement fails")
void keepsOriginalAnswerWhenReplacementFails() {
Config config = mock(Config.class);
HelpSystemConfig helpSystemConfig = mock(HelpSystemConfig.class);
when(config.getHelpSystem()).thenReturn(helpSystemConfig);
when(config.getTagManageRolePattern()).thenReturn("tag-manage");
when(helpSystemConfig.getHelpForumPattern()).thenReturn("questions");
when(helpSystemConfig.getCategories()).thenReturn(List.of("java"));
when(helpSystemConfig.getCategoryRoleSuffix()).thenReturn(" helper");

Database database = Database.createMemoryDatabase(HelpThreads.HELP_THREADS);
ChatGptService chatGptService = mock(ChatGptService.class);
HelpSystemHelper failingHelper = new HelpSystemHelper(config, database, chatGptService,
_ -> CompletableFuture.failedFuture(new IllegalStateException("boom")));

MessageEmbed embed = failingHelper.generateGptResponseEmbed(ORIGINAL_ANSWER, selfUser,
"example question", ChatGptModel.FAST);

assertEquals(ORIGINAL_ANSWER, embed.getDescription());
}
}
Loading