Skip to content

Commit 4ae181d

Browse files
committed
refactor(ChatGptService, XkcdCommand): do not call XKCD stuff et al
- ChatGptService: Refrain from polluting it with XKCD related calls, - ChatGptService: provide JavaDocs to the methods that don't have one, - ChatGptService: remove unused sendWebPrompt method - XkcdCommand and XkcdRetriever: Refactor code into functions for readability. Signed-off-by: Chris Sdogkos <work@chris-sdogkos.com>
1 parent 1ac4e67 commit 4ae181d

3 files changed

Lines changed: 98 additions & 79 deletions

File tree

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

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.openai.models.responses.ResponseCreateParams;
1010
import com.openai.models.responses.ResponseOutputText;
1111
import com.openai.models.responses.Tool;
12-
import com.openai.models.responses.WebSearchTool;
1312
import com.openai.models.vectorstores.VectorStore;
1413
import com.openai.models.vectorstores.VectorStoreCreateParams;
1514
import org.slf4j.Logger;
@@ -33,8 +32,6 @@ public class ChatGptService {
3332
private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class);
3433
private static final Duration TIMEOUT = Duration.ofSeconds(90);
3534

36-
private static final String VECTOR_STORE_XKCD = "xkcd-comics";
37-
3835
/** The maximum number of tokens allowed for the generated answer. */
3936
private static final int MAX_TOKENS = 1000;
4037

@@ -100,20 +97,6 @@ public Optional<String> askRaw(String inputPrompt, ChatGptModel chatModel) {
10097
return sendPrompt(inputPrompt, chatModel);
10198
}
10299

103-
/**
104-
* Sends a prompt to the ChatGPT API with web capabilities and returns the response.
105-
*
106-
* @param prompt The prompt to send to ChatGPT.
107-
* @param chatModel The AI model to use for this request.
108-
* @return response from ChatGPT as a String.
109-
*/
110-
public Optional<String> sendWebPrompt(String prompt, ChatGptModel chatModel) {
111-
Tool webSearchTool = Tool
112-
.ofWebSearch(WebSearchTool.builder().type(WebSearchTool.Type.WEB_SEARCH).build());
113-
114-
return sendPrompt(prompt, chatModel, List.of(webSearchTool));
115-
}
116-
117100
/**
118101
* Sends a prompt to the ChatGPT API and returns the response.
119102
*
@@ -125,6 +108,13 @@ public Optional<String> sendPrompt(String prompt, ChatGptModel chatModel) {
125108
return sendPrompt(prompt, chatModel, List.of());
126109
}
127110

111+
/**
112+
* Lists all files uploaded to OpenAI and returns the ID of the first file matching the given
113+
* filename (case-insensitive).
114+
*
115+
* @param filePath The filename to search for among uploaded files.
116+
* @return An Optional containing the file ID if found, or empty if no matching file exists.
117+
*/
128118
public Optional<String> getUploadedFileId(String filePath) {
129119
return openAIClient.files()
130120
.list()
@@ -135,6 +125,18 @@ public Optional<String> getUploadedFileId(String filePath) {
135125
.findFirst();
136126
}
137127

128+
/**
129+
* Uploads the specified file to OpenAI if it exists locally and hasn't been uploaded before.
130+
*
131+
* @param filePath The local path to the file to upload.
132+
* @param purpose The OpenAI file purpose (e.g., {@link FilePurpose#ASSISTANTS})
133+
* @return an Optional containing the uploaded file ID, or empty if:
134+
* <ul>
135+
* <li>service is disabled</li>
136+
* <li>file doesn't exist locally</li>
137+
* <li>file with matching name already uploaded</li>
138+
* </ul>
139+
*/
138140
public Optional<String> uploadFileIfNotExists(Path filePath, FilePurpose purpose) {
139141
if (isDisabled) {
140142
logger.warn("ChatGPT file upload attempted but service is disabled");
@@ -161,29 +163,40 @@ public Optional<String> uploadFileIfNotExists(Path filePath, FilePurpose purpose
161163
return Optional.of(id);
162164
}
163165

164-
public String createOrGetVectorStore(String fileId) {
166+
/**
167+
* Creates a new vector store with the given file ID if none exists or returns the ID of the
168+
* existing vector store with that name.
169+
* <p>
170+
* You can use this for RAG purposes, it is an effective way to give ChatGPT extra information
171+
* from what it has been trained.
172+
*
173+
* @param fileId The ID of the file to include in the new vector store.
174+
* @return The vector store ID (existing or newly created).
175+
*/
176+
public String createOrGetVectorStore(String fileId, String vectorStoreName) {
165177
List<VectorStore> vectorStores = openAIClient.vectorStores()
166178
.list()
167179
.items()
168180
.stream()
169-
.filter(vectorStore -> vectorStore.name().equalsIgnoreCase(VECTOR_STORE_XKCD))
181+
.filter(vectorStore -> vectorStore.name().equalsIgnoreCase(vectorStoreName))
170182
.toList();
171183
Optional<VectorStore> vectorStore = vectorStores.stream().findFirst();
172184

173185
if (vectorStore.isPresent()) {
174-
return vectorStore.get().id();
186+
String vectorStoreId = vectorStore.get().id();
187+
logger.debug("Got vector store {}", vectorStoreId);
188+
return vectorStoreId;
175189
}
176190

177191
VectorStoreCreateParams params = VectorStoreCreateParams.builder()
178-
.name(VECTOR_STORE_XKCD)
192+
.name(vectorStoreName)
179193
.fileIds(List.of(fileId))
180194
.build();
181195

182196
VectorStore newVectorStore = openAIClient.vectorStores().create(params);
183197
String vectorStoreId = newVectorStore.id();
184198

185-
logger.info("Created vector store {} with XKCD data", vectorStoreId);
186-
199+
logger.debug("Created vector store {}", vectorStoreId);
187200
return vectorStoreId;
188201
}
189202

application/src/main/java/org/togetherjava/tjbot/features/xkcd/XkcdCommand.java

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public final class XkcdCommand extends SlashCommandAdapter {
4949
public static final String SUBCOMMAND_CUSTOM = "custom";
5050
public static final String LAST_MESSAGES_AMOUNT_OPTION_NAME = "amount";
5151
public static final String XKCD_ID_OPTION_NAME = "id";
52+
private static final String VECTOR_STORE_XKCD = "xkcd-comics";
5253
public static final int MAXIMUM_MESSAGE_HISTORY = 100;
5354
private static final ChatGptModel CHAT_GPT_MODEL = ChatGptModel.FAST;
5455
private static final Pattern XKCD_POST_PATTERN = Pattern.compile("^\\D*(\\d+)");
@@ -119,37 +120,8 @@ private void handleRelevantXkcd(SlashCommandInteractionEvent event) {
119120
.getHistory()
120121
.retrievePast(messagesAmount)
121122
.queue(messages -> {
122-
String discordChat = formatDiscordChatHistory(messages);
123-
124123
event.deferReply().queue();
125-
126-
String xkcdComicsFileId = xkcdRetriever.getXkcdUploadedFileId();
127-
String xkcdVectorStore = chatGptService.createOrGetVectorStore(xkcdComicsFileId);
128-
FileSearchTool fileSearch =
129-
FileSearchTool.builder().vectorStoreIds(List.of(xkcdVectorStore)).build();
130-
131-
Tool tool = Tool.ofFileSearch(fileSearch);
132-
133-
Optional<String> responseOptional = chatGptService.sendPrompt(
134-
getChatgptRelevantPrompt(discordChat), CHAT_GPT_MODEL, List.of(tool));
135-
136-
Optional<Integer> responseIdOptional =
137-
getXkcdIdFromMessage(responseOptional.orElseThrow());
138-
139-
if (responseIdOptional.isEmpty()) {
140-
event.getHook().setEphemeral(true).sendMessage(CHATGPT_NO_ID_MESSAGE).queue();
141-
return;
142-
}
143-
144-
int responseId = responseIdOptional.orElseThrow();
145-
146-
logger.debug("Response: {}", responseOptional.orElseThrow());
147-
148-
logger.debug("ChatGPT chose XKCD ID: {}", responseId);
149-
Optional<MessageEmbed> embedOptional =
150-
constructEmbed(responseId, "Most relevant XKCD according to ChatGPT.");
151-
152-
embedOptional.ifPresent(embed -> event.getHook().sendMessageEmbeds(embed).queue());
124+
sendRelevantXkcdEmbedFromMessages(messages, event);
153125
}, error -> logger.error("Failed to retrieve the chat history in #{}",
154126
messageChannelUnion.getName(), error));
155127
}
@@ -176,6 +148,36 @@ private void handleCustomXkcd(SlashCommandInteractionEvent event) {
176148
});
177149
}
178150

151+
private void sendRelevantXkcdEmbedFromMessages(List<Message> messages,
152+
SlashCommandInteractionEvent event) {
153+
String discordChat = formatDiscordChatHistory(messages);
154+
String xkcdComicsFileId = xkcdRetriever.getXkcdUploadedFileId();
155+
String xkcdVectorStore =
156+
chatGptService.createOrGetVectorStore(xkcdComicsFileId, VECTOR_STORE_XKCD);
157+
FileSearchTool fileSearch =
158+
FileSearchTool.builder().vectorStoreIds(List.of(xkcdVectorStore)).build();
159+
160+
Tool tool = Tool.ofFileSearch(fileSearch);
161+
162+
Optional<String> responseOptional = chatGptService
163+
.sendPrompt(getChatgptRelevantPrompt(discordChat), CHAT_GPT_MODEL, List.of(tool));
164+
165+
Optional<Integer> responseIdOptional = getXkcdIdFromMessage(responseOptional.orElseThrow());
166+
167+
if (responseIdOptional.isEmpty()) {
168+
event.getHook().setEphemeral(true).sendMessage(CHATGPT_NO_ID_MESSAGE).queue();
169+
return;
170+
}
171+
172+
int responseId = responseIdOptional.orElseThrow();
173+
174+
logger.debug("ChatGPT chose XKCD ID: {}", responseId);
175+
Optional<MessageEmbed> embedOptional =
176+
constructEmbed(responseId, "Most relevant XKCD according to ChatGPT.");
177+
178+
embedOptional.ifPresent(embed -> event.getHook().sendMessageEmbeds(embed).queue());
179+
}
180+
179181
private Optional<MessageEmbed> constructEmbed(int xkcdId, String footer) {
180182
Optional<XkcdPost> xkcdPostOptional = xkcdRetriever.getXkcdPost(xkcdId);
181183

application/src/main/java/org/togetherjava/tjbot/features/xkcd/XkcdRetriever.java

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public class XkcdRetriever {
3636
private static final String XKCD_GET_URL = "https://xkcd.com/%d/info.0.json";
3737
public static final String SAVED_XKCD_PATH = "xkcd.generated.json";
3838
private static final int XKCD_POSTS_AMOUNT = 3201;
39+
private static final int FETCH_XCKD_POSTS_POOL_SIZE = 20;
40+
private static final int FETCH_XKCD_POSTS_SEMAPHORE_SIZE = 10;
41+
private static final int FETCH_XKCD_POSTS_THREAD_SLEEP_MS = 50;
3942
private static final ObjectMapper objectMapper = new ObjectMapper();
4043

4144
private final Map<Integer, XkcdPost> xkcdPosts = new HashMap<>();
@@ -66,33 +69,34 @@ public XkcdRetriever(ChatGptService chatGptService) {
6669
return;
6770
}
6871

72+
logger.info("Could not find XKCD posts locally saved in '{}' so will fetch...",
73+
SAVED_XKCD_PATH);
74+
fetchAllXkcdPosts(savedXckdsPath);
75+
}
76+
77+
private void fetchAllXkcdPosts(Path savedXckdsPath) {
6978
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
70-
Semaphore semaphore = new Semaphore(10);
71-
72-
logger.info("Could not find file '{}', fetching {} XKCD posts...", SAVED_XKCD_PATH,
73-
XKCD_POSTS_AMOUNT);
74-
try (ExecutorService executor = Executors.newFixedThreadPool(20)) {
75-
try {
76-
CompletableFuture.allOf(IntegerRange.of(1, XKCD_POSTS_AMOUNT)
77-
.toIntStream()
78-
.filter(id -> id != 404) // XKCD has a joke on comic ID 404 so exclude
79-
.mapToObj(xkcdId -> CompletableFuture.runAsync(() -> {
80-
semaphore.acquireUninterruptibly();
81-
try {
82-
Optional<XkcdPost> postOptional = this.retrieveXkcdPost(xkcdId).join();
83-
postOptional.ifPresent(post -> xkcdPosts.put(xkcdId, post));
84-
85-
Thread.sleep(50);
86-
} catch (InterruptedException _) {
87-
Thread.currentThread().interrupt();
88-
} finally {
89-
semaphore.release();
90-
}
91-
}, executor))
92-
.toArray(CompletableFuture[]::new)).join();
93-
} finally {
94-
executor.shutdown();
95-
}
79+
Semaphore semaphore = new Semaphore(FETCH_XKCD_POSTS_SEMAPHORE_SIZE);
80+
81+
logger.info("Fetching {} XKCD posts...", XKCD_POSTS_AMOUNT);
82+
try (ExecutorService executor = Executors.newFixedThreadPool(FETCH_XCKD_POSTS_POOL_SIZE)) {
83+
CompletableFuture.allOf(IntegerRange.of(1, XKCD_POSTS_AMOUNT)
84+
.toIntStream()
85+
.filter(id -> id != 404) // XKCD has a joke on comic ID 404 so exclude
86+
.mapToObj(xkcdId -> CompletableFuture.runAsync(() -> {
87+
semaphore.acquireUninterruptibly();
88+
try {
89+
Optional<XkcdPost> postOptional = this.retrieveXkcdPost(xkcdId).join();
90+
postOptional.ifPresent(post -> xkcdPosts.put(xkcdId, post));
91+
92+
Thread.sleep(FETCH_XKCD_POSTS_THREAD_SLEEP_MS);
93+
} catch (InterruptedException _) {
94+
Thread.currentThread().interrupt();
95+
} finally {
96+
semaphore.release();
97+
}
98+
}, executor))
99+
.toArray(CompletableFuture[]::new)).join();
96100
}
97101

98102
saveToFile(savedXckdsPath, xkcdPosts);

0 commit comments

Comments
 (0)