Skip to content

Commit d31728a

Browse files
authored
Added /rewrite for improving a message using AI (#1378)
* Feature: Implement /rewrite command for message improvement using ChatGPT * feature "rewrite-msg command": applies changes due to Zabu's first review; * feature "rewrite-msg command": applies changes due to Wazei's first review - Part 1; * feature "rewrite-msg command": applies changes due to Wazei's first review - Part 2; * feature "rewrite-msg command": applies changes due to Wazei's first review - Part 3; * feature "rewrite-msg command": removing Optional and other adjustments * feature "rewrite-msg command" - reply to taz 2nd review; 1. ChatGptService: MAX_TOKENS value updated to 500; 2. RewriteCommand: Adding more precision in Ai Prompt message; * ChatGptService: MAX_TOKENS value updated to 1000;
1 parent eee594c commit d31728a

File tree

3 files changed

+239
-17
lines changed

3 files changed

+239
-17
lines changed

application/src/main/java/org/togetherjava/tjbot/features/Features.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.togetherjava.tjbot.features.mathcommands.wolframalpha.WolframAlphaCommand;
4242
import org.togetherjava.tjbot.features.mediaonly.MediaOnlyChannelListener;
4343
import org.togetherjava.tjbot.features.messages.MessageCommand;
44+
import org.togetherjava.tjbot.features.messages.RewriteCommand;
4445
import org.togetherjava.tjbot.features.moderation.BanCommand;
4546
import org.togetherjava.tjbot.features.moderation.KickCommand;
4647
import org.togetherjava.tjbot.features.moderation.ModerationActionsStore;
@@ -211,6 +212,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
211212
features.add(new ChatGptCommand(chatGptService, helpSystemHelper));
212213
features.add(new JShellCommand(jshellEval));
213214
features.add(new MessageCommand());
215+
features.add(new RewriteCommand(chatGptService));
214216

215217
FeatureBlacklist<Class<?>> blacklist = blacklistConfig.normal();
216218
return blacklist.filterStream(features.stream(), Object::getClass).toList();

application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class ChatGptService {
2424
private static final Duration TIMEOUT = Duration.ofSeconds(90);
2525

2626
/** The maximum number of tokens allowed for the generated answer. */
27-
private static final int MAX_TOKENS = 3_000;
27+
private static final int MAX_TOKENS = 1000;
2828

2929
private boolean isDisabled = false;
3030
private OpenAIClient openAIClient;
@@ -39,9 +39,11 @@ public ChatGptService(Config config) {
3939
boolean keyIsDefaultDescription = apiKey.startsWith("<") && apiKey.endsWith(">");
4040
if (apiKey.isBlank() || keyIsDefaultDescription) {
4141
isDisabled = true;
42+
logger.warn("ChatGPT service is disabled: API key is not configured");
4243
return;
4344
}
4445
openAIClient = OpenAIOkHttpClient.builder().apiKey(apiKey).timeout(TIMEOUT).build();
46+
logger.info("ChatGPT service initialized successfully");
4547
}
4648

4749
/**
@@ -56,10 +58,6 @@ public ChatGptService(Config config) {
5658
* Tokens</a>.
5759
*/
5860
public Optional<String> ask(String question, @Nullable String context, ChatGptModel chatModel) {
59-
if (isDisabled) {
60-
return Optional.empty();
61-
}
62-
6361
String contextText = context == null ? "" : ", Context: %s.".formatted(context);
6462
String inputPrompt = """
6563
For code supplied for review, refer to the old code supplied rather than
@@ -71,35 +69,71 @@ public Optional<String> ask(String question, @Nullable String context, ChatGptMo
7169
Question: %s
7270
""".formatted(contextText, question);
7371

74-
logger.debug("ChatGpt request: {}", inputPrompt);
72+
return sendPrompt(inputPrompt, chatModel);
73+
}
74+
75+
/**
76+
* Prompt ChatGPT with a raw prompt and receive a response without any prefix wrapping.
77+
* <p>
78+
* Use this method when you need full control over the prompt structure without the service's
79+
* opinionated formatting (e.g., for iterative refinement or specialized use cases).
80+
*
81+
* @param inputPrompt The raw prompt to send to ChatGPT. Max is {@value MAX_TOKENS} tokens.
82+
* @param chatModel The AI model to use for this request.
83+
* @return response from ChatGPT as a String.
84+
* @see <a href="https://platform.openai.com/docs/guides/chat/managing-tokens">ChatGPT
85+
* Tokens</a>.
86+
*/
87+
public Optional<String> askRaw(String inputPrompt, ChatGptModel chatModel) {
88+
return sendPrompt(inputPrompt, chatModel);
89+
}
90+
91+
/**
92+
* Sends a prompt to the ChatGPT API and returns the response.
93+
*
94+
* @param prompt The prompt to send to ChatGPT.
95+
* @param chatModel The AI model to use for this request.
96+
* @return response from ChatGPT as a String.
97+
*/
98+
private Optional<String> sendPrompt(String prompt, ChatGptModel chatModel) {
99+
if (isDisabled) {
100+
logger.warn("ChatGPT request attempted but service is disabled");
101+
return Optional.empty();
102+
}
103+
104+
logger.debug("ChatGpt request: {}", prompt);
75105

76-
String response = null;
77106
try {
78107
ResponseCreateParams params = ResponseCreateParams.builder()
79108
.model(chatModel.toChatModel())
80-
.input(inputPrompt)
109+
.input(prompt)
81110
.maxOutputTokens(MAX_TOKENS)
82111
.build();
83112

84113
Response chatGptResponse = openAIClient.responses().create(params);
85114

86-
response = chatGptResponse.output()
115+
String response = chatGptResponse.output()
87116
.stream()
88117
.flatMap(item -> item.message().stream())
89118
.flatMap(message -> message.content().stream())
90119
.flatMap(content -> content.outputText().stream())
91120
.map(ResponseOutputText::text)
92121
.collect(Collectors.joining("\n"));
93-
} catch (RuntimeException runtimeException) {
94-
logger.warn("There was an error using the OpenAI API: {}",
95-
runtimeException.getMessage());
96-
}
97122

98-
logger.debug("ChatGpt Response: {}", response);
99-
if (response == null) {
123+
logger.debug("ChatGpt Response: {}", response);
124+
125+
if (response.isBlank()) {
126+
logger.warn("ChatGPT returned an empty response");
127+
return Optional.empty();
128+
}
129+
130+
logger.debug("ChatGpt response received successfully, length: {} characters",
131+
response.length());
132+
return Optional.of(response);
133+
} catch (RuntimeException runtimeException) {
134+
logger.error("Error communicating with OpenAI API: {}", runtimeException.getMessage(),
135+
runtimeException);
100136
return Optional.empty();
101137
}
102-
103-
return Optional.of(response);
104138
}
105139
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package org.togetherjava.tjbot.features.messages;
2+
3+
import net.dv8tion.jda.api.entities.Message;
4+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
5+
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
6+
import net.dv8tion.jda.api.interactions.commands.OptionType;
7+
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
8+
import org.jetbrains.annotations.Nullable;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
import org.togetherjava.tjbot.features.CommandVisibility;
13+
import org.togetherjava.tjbot.features.SlashCommandAdapter;
14+
import org.togetherjava.tjbot.features.chatgpt.ChatGptModel;
15+
import org.togetherjava.tjbot.features.chatgpt.ChatGptService;
16+
17+
import java.util.Arrays;
18+
19+
/**
20+
* The implemented command is {@code /rewrite}, which allows users to have their message rewritten
21+
* in a clearer, more professional, or better structured form using AI.
22+
* <p>
23+
* The rewritten message is shown as an ephemeral message visible only to the user who triggered the
24+
* command.
25+
* <p>
26+
* Users can optionally specify a tone/style for the rewrite.
27+
*/
28+
public final class RewriteCommand extends SlashCommandAdapter {
29+
private static final Logger logger = LoggerFactory.getLogger(RewriteCommand.class);
30+
private static final String COMMAND_NAME = "rewrite";
31+
private static final String MESSAGE_OPTION = "message";
32+
private static final String TONE_OPTION = "tone";
33+
34+
private static final int MAX_MESSAGE_LENGTH = Message.MAX_CONTENT_LENGTH;
35+
private static final int MIN_MESSAGE_LENGTH = 3;
36+
37+
private static final String AI_REWRITE_PROMPT_TEMPLATE = """
38+
You are rewriting a Discord text chat message for clarity and professionalism.
39+
Keep it conversational and casual, not email or formal document format.
40+
41+
Tone: %s
42+
43+
Rewrite the message to:
44+
- Improve clarity and structure
45+
- Maintain the original meaning
46+
- Avoid em-dashes (—)
47+
- Stay under %d characters (strict limit)
48+
49+
If the message is already well-written, make only minor improvements.
50+
51+
Reply with ONLY the rewritten message, nothing else (greetings, preamble, etc).
52+
53+
Message to rewrite:
54+
%s
55+
""".stripIndent();
56+
57+
private final ChatGptService chatGptService;
58+
59+
/**
60+
* Creates the slash command definition and configures available options for rewriting messages.
61+
*
62+
* @param chatGptService service for interacting with ChatGPT
63+
*/
64+
public RewriteCommand(ChatGptService chatGptService) {
65+
super(COMMAND_NAME, "Let AI rephrase and improve your message", CommandVisibility.GUILD);
66+
67+
this.chatGptService = chatGptService;
68+
69+
OptionData messageOption =
70+
new OptionData(OptionType.STRING, MESSAGE_OPTION, "The message you want to rewrite",
71+
true)
72+
.setMinLength(MIN_MESSAGE_LENGTH)
73+
.setMaxLength(MAX_MESSAGE_LENGTH);
74+
75+
OptionData toneOption = new OptionData(OptionType.STRING, TONE_OPTION,
76+
"The tone/style for the rewritten message (default: "
77+
+ MessageTone.CLEAR.displayName + ")",
78+
false);
79+
80+
Arrays.stream(MessageTone.values())
81+
.forEach(tone -> toneOption.addChoice(tone.displayName, tone.name()));
82+
83+
getData().addOptions(messageOption, toneOption);
84+
}
85+
86+
@Override
87+
public void onSlashCommand(SlashCommandInteractionEvent event) {
88+
89+
OptionMapping messageOption = event.getOption(MESSAGE_OPTION);
90+
91+
if (messageOption == null) {
92+
throw new IllegalArgumentException(
93+
"Required option '" + MESSAGE_OPTION + "' is missing");
94+
}
95+
96+
String userMessage = messageOption.getAsString();
97+
98+
MessageTone tone = parseTone(event.getOption(TONE_OPTION));
99+
100+
event.deferReply(true).queue();
101+
102+
String rewrittenMessage = rewrite(userMessage, tone);
103+
104+
if (rewrittenMessage.isEmpty()) {
105+
logger.debug("Failed to obtain a response for /{}, original message: '{}'",
106+
COMMAND_NAME, userMessage);
107+
108+
event.getHook()
109+
.editOriginal(
110+
"An error occurred while processing your request. Please try again later.")
111+
.queue();
112+
113+
return;
114+
}
115+
116+
logger.debug("Rewrite successful; rewritten message length: {}", rewrittenMessage.length());
117+
118+
event.getHook().sendMessage(rewrittenMessage).setEphemeral(true).queue();
119+
}
120+
121+
private MessageTone parseTone(@Nullable OptionMapping toneOption)
122+
throws IllegalArgumentException {
123+
124+
if (toneOption == null) {
125+
logger.debug("Tone option not provided, using default '{}'", MessageTone.CLEAR.name());
126+
return MessageTone.CLEAR;
127+
}
128+
129+
return MessageTone.valueOf(toneOption.getAsString());
130+
}
131+
132+
private String rewrite(String userMessage, MessageTone tone) {
133+
134+
String rewritePrompt = createAiPrompt(userMessage, tone);
135+
136+
ChatGptModel aiModel = tone.model;
137+
138+
String attempt = askAi(rewritePrompt, aiModel);
139+
140+
if (attempt.length() <= MAX_MESSAGE_LENGTH) {
141+
return attempt;
142+
}
143+
144+
logger.debug("Rewritten message exceeded {} characters; retrying with stricter constraint",
145+
MAX_MESSAGE_LENGTH);
146+
147+
String shortenPrompt =
148+
"""
149+
%s
150+
151+
Constraint reminder: Your previous rewrite exceeded %d characters.
152+
Provide a revised rewrite strictly under %d characters while preserving meaning and tone.
153+
"""
154+
.formatted(rewritePrompt, MAX_MESSAGE_LENGTH, MAX_MESSAGE_LENGTH);
155+
156+
return askAi(shortenPrompt, aiModel);
157+
}
158+
159+
private String askAi(String shortenPrompt, ChatGptModel aiModel) {
160+
return chatGptService.askRaw(shortenPrompt, aiModel).orElse("");
161+
}
162+
163+
private static String createAiPrompt(String userMessage, MessageTone tone) {
164+
return AI_REWRITE_PROMPT_TEMPLATE.formatted(tone.description, MAX_MESSAGE_LENGTH,
165+
userMessage);
166+
}
167+
168+
private enum MessageTone {
169+
CLEAR("Clear", "Make it clear and easy to understand.", ChatGptModel.FASTEST),
170+
PROFESSIONAL("Professional", "Use a professional and polished tone.", ChatGptModel.FASTEST),
171+
DETAILED("Detailed", "Expand with more detail and explanation.", ChatGptModel.HIGH_QUALITY),
172+
TECHNICAL("Technical", "Use technical and specialized language where appropriate.",
173+
ChatGptModel.HIGH_QUALITY);
174+
175+
private final String displayName;
176+
private final String description;
177+
private final ChatGptModel model;
178+
179+
MessageTone(String displayName, String description, ChatGptModel model) {
180+
this.displayName = displayName;
181+
this.description = description;
182+
this.model = model;
183+
}
184+
}
185+
186+
}

0 commit comments

Comments
 (0)