Compare commits

...

2 commits

Author SHA1 Message Date
a51aea8013 discord-rest: Add minimal error handling 2024-11-19 20:38:56 +01:00
d15388581b openai-rest: Add OpenAI client
Missing the response deserialization.
2024-11-19 20:36:56 +01:00
11 changed files with 251 additions and 6 deletions

View file

@ -1,14 +1,19 @@
package de.hhhammer.dchat.discord.rest;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Optional;
public class DiscordRest {
private static final Logger logger = LoggerFactory.getLogger(DiscordRest.class);
private static final String USER_AGENT = "DiscordBot (https://git.hhhammer.de/hamburghammer/dchat, main)";
private static final String DISCORD_URL = "https://discord.com/api/v10";
private final HttpClient httpClient;
@ -30,11 +35,11 @@ public class DiscordRest {
HttpResponse<Void> response = httpClient.send(request, HttpResponse.BodyHandlers.discarding());
if (response.statusCode() != 201) {
// TODO: What about errors?
logger.error("Unexpected status code: {}", response.statusCode());
}
}
public String getMessage(int channelId, int messageId) throws IOException, InterruptedException {
public Optional<String> getMessage(int channelId, int messageId) throws IOException, InterruptedException {
var uri = URI.create(DISCORD_URL + "/channels/%s/messages/%s".formatted(channelId, messageId));
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
@ -45,9 +50,14 @@ public class DiscordRest {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
return ""; // TODO: Think about transparently inform caller about error.
logger.error("Could not get message");
return Optional.empty();
}
try {
final var responseJson = new JSONObject(response.body());
return Optional.of(responseJson.getString("content")); // content not be set -> https://discord.com/developers/docs/resources/message#message-object-message-structure
} catch (JSONException e) {
return Optional.empty();
}
final var responseJson = new JSONObject(response.body());
return responseJson.getString("content"); // FIXME: Can throw exception if content is not set -> https://discord.com/developers/docs/resources/message#message-object-message-structure
}
}

23
openai-rest/pom.xml Normal file
View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.hhhammer.dchat</groupId>
<artifactId>dchat</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>de.hhhammer.dchat.openai.rest</groupId>
<artifactId>openai-rest</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,57 @@
package de.hhhammer.dchat.openai.rest;
import de.hhhammer.dchat.openai.rest.MessageContext.PreviousInteraction;
import de.hhhammer.dchat.openai.rest.MessageContext.ReplyInteraction;
import de.hhhammer.dchat.openai.rest.models.ChatGPTRequest;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public final class ChatGPTRequestBuilder {
private static final String contextModel = "gpt-4o-mini";
public ChatGPTRequestBuilder() {
}
@NotNull
private static List<ChatGPTRequest.Message> getMessages(final List<? extends MessageContext> contextMessages, final String message, final String systemMessage) {
final ChatGPTRequest.Message systemMsg = new ChatGPTRequest.Message("system", systemMessage);
final List<ChatGPTRequest.Message> contextMsgs = getContextMessages(contextMessages);
final ChatGPTRequest.Message userMessage = new ChatGPTRequest.Message("user", message);
final List<ChatGPTRequest.Message> messages = new ArrayList<>();
messages.add(systemMsg);
messages.addAll(contextMsgs);
messages.add(userMessage);
return messages;
}
@NotNull
private static List<ChatGPTRequest.Message> getContextMessages(final List<? extends MessageContext> contextMessages) {
return contextMessages.stream()
.map(ChatGPTRequestBuilder::mapContextMessages)
.flatMap(List::stream)
.toList();
}
@NotNull
private static List<ChatGPTRequest.Message> mapContextMessages(final MessageContext contextMessage) {
return switch (contextMessage) {
case PreviousInteraction previousInteractions -> List.of(
new ChatGPTRequest.Message("user", previousInteractions.question()),
new ChatGPTRequest.Message("assistant", previousInteractions.answer())
);
case ReplyInteraction replyInteractions ->
List.of(new ChatGPTRequest.Message("assistant", replyInteractions.answer()));
};
}
public ChatGPTRequest contextRequest(final List<? extends MessageContext> contextMessages, final String message, final String systemMessage) {
final List<ChatGPTRequest.Message> messages = getMessages(contextMessages, message, systemMessage);
return new ChatGPTRequest(
contextModel,
messages,
0.7f
);
}
}

View file

@ -0,0 +1,40 @@
package de.hhhammer.dchat.openai.rest;
import de.hhhammer.dchat.openai.rest.models.ChatGPTRequest;
import de.hhhammer.dchat.openai.rest.models.ChatGPTResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public final class ChatGPTService {
private final String apiKey;
private final HttpClient httpClient;
public ChatGPTService(final String apiKey, final HttpClient httpClient) {
this.apiKey = apiKey;
this.httpClient = httpClient;
}
public ChatGPTResponse submit(final ChatGPTRequest chatGPTRequest) throws IOException, InterruptedException, ResponseException {
final String data = chatGPTRequest.toJson().toString();
final URI uri = URI.create("https://api.openai.com/v1/chat/completions");
final HttpRequest request = HttpRequest.newBuilder(uri)
.POST(HttpRequest.BodyPublishers.ofString(data))
.setHeader("Content-Type", "application/json")
.setHeader("Authorization", "Bearer " + this.apiKey)
.timeout(Duration.ofMinutes(5))
.build();
final HttpResponse<InputStream> responseStream = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (responseStream.statusCode() != 200) {
throw new ResponseException("Response status code was not 200: " + responseStream.statusCode());
}
return mapper.readValue(responseStream.body(), ChatGPTResponse.class);
}
}

View file

@ -0,0 +1,9 @@
package de.hhhammer.dchat.openai.rest;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
public interface JsonSerializable {
@NotNull
JSONObject toJson();
}

View file

@ -0,0 +1,9 @@
package de.hhhammer.dchat.openai.rest;
public sealed interface MessageContext {
record ReplyInteraction(String answer) implements MessageContext {
}
record PreviousInteraction(String question, String answer) implements MessageContext {
}
}

View file

@ -0,0 +1,7 @@
package de.hhhammer.dchat.openai.rest;
public final class ResponseException extends Exception {
public ResponseException(final String message) {
super(message);
}
}

View file

@ -0,0 +1,31 @@
package de.hhhammer.dchat.openai.rest.models;
import de.hhhammer.dchat.openai.rest.JsonSerializable;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.List;
public record ChatGPTRequest(String model, List<Message> messages, float temperature) implements JsonSerializable {
@Override
public @NotNull JSONObject toJson() {
final var json = new JSONObject();
json.put("model", model);
final var messageJson = new JSONArray(messages.stream().map(Message::toJson).toList());
json.put("messages", messageJson);
json.put("temperature", temperature);
return json;
}
public record Message(String role, String content) implements JsonSerializable {
@Override
public @NotNull JSONObject toJson() {
final var json = new JSONObject();
json.put("role", role);
json.put("content", content);
return json;
}
}
}

View file

@ -0,0 +1,49 @@
package de.hhhammer.dchat.openai.rest.models;
import de.hhhammer.dchat.openai.rest.JsonSerializable;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.List;
public record ChatGPTResponse(Usage usage, List<Choice> choices) implements JsonSerializable {
@Override
public @NotNull JSONObject toJson() {
final var json = new JSONObject();
json.put("usage", usage.toJson());
final var jsonChoices = new JSONArray(choices.stream().map(Choice::toJson).toList());
json.put("choices", jsonChoices);
return json;
}
public record Usage(int promptTokens, int completionTokens, int totalTokens) implements JsonSerializable {
@Override
public @NotNull JSONObject toJson() {
final var json = new JSONObject();
json.put("prompt_tokens", promptTokens);
json.put("completion_tokens", completionTokens);
json.put("total_tokens", totalTokens);
return json;
}
}
public record Choice(Message message) implements JsonSerializable {
@Override
public @NotNull JSONObject toJson() {
final var json = new JSONObject();
json.put("message", message.toJson());
return json;
}
public record Message(String role, String content) implements JsonSerializable {
@Override
public @NotNull JSONObject toJson() {
final var json = new JSONObject();
json.put("role", role);
json.put("content", content);
return json;
}
}
}
}

View file

@ -0,0 +1,9 @@
module de.hhhammer.dchat.openai.rest {
exports de.hhhammer.dchat.openai.rest;
exports de.hhhammer.dchat.openai.rest.models;
requires java.net.http;
requires org.json;
requires static org.jetbrains.annotations;
}

View file

@ -98,5 +98,6 @@
<module>bot</module>
<module>discord-ws</module>
<module>discord-rest</module>
<module>openai-rest</module>
</modules>
</project>
</project>