ApiWrapper.java

package de.sesqa.ase.api;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.sesqa.ase.entities.Message;
import io.github.cdimascio.dotenv.Dotenv;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A wrapper class for interacting with an external chat completion API (e.g., OpenAI). This class
 * provides a static method to send a user's message to the API and get a response.
 */
public class ApiWrapper {
  private static final Logger logger = LoggerFactory.getLogger(ApiWrapper.class);

  /** Loads environment variables from a .env file. Used to retrieve the API key. */
  private static Dotenv DOTENV = Dotenv.configure().ignoreIfMissing().load();

  /** A reusable HttpClient instance for making HTTP requests. */
  private static HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

  /** A reusable ObjectMapper for parsing JSON responses. */
  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

  /**
   * Sends a user's message to the chat API and returns the API's response as a new Message.
   *
   * @param message The user's message to be sent to the API.
   * @return A new {@link Message} of type {@code BOT} containing the API's response.
   * @throws IOException if an I/O error occurs when sending or receiving.
   * @throws InterruptedException if the operation is interrupted.
   */
  public static Message query(Message message) throws IOException, InterruptedException {
    HttpRequest request = buildHttpRequest(message.getContent());
    String responseBody = "";

    try {
      HttpResponse<String> response =
          HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
      responseBody = parseHttpResponse(response.body());

    } catch (IOException | InterruptedException e) {
      logger.error("Error during API request: {}", e.getMessage());
    } finally {
      logger.info("Query completed.\n");
    }

    return new Message(Message.MessageType.BOT, message.getConversation(), responseBody);
  }

  /**
   * Builds an HTTP POST request for the chat completion API.
   *
   * @param messageContent The text content of the user's message.
   * @return An {@link HttpRequest} object configured for the API call.
   */
  // CHECKSTYLE:OFF: LineLength
  @SuppressWarnings("checkstyle:LineLength")
  private static HttpRequest buildHttpRequest(String messageContent) {
    String body =
        String.format(
            "{\"model\": \"gpt-4.1-nano\", \"messages\": [{\"role\": \"user\", \"content\": \"%s\"}]}",
            messageContent);
    String apiUrl = "https://api.openai.com/v1/chat/completions";
    String apiKey = DOTENV.get("API_KEY");

    return HttpRequest.newBuilder()
        .uri(URI.create(apiUrl))
        .header("Content-Type", "application/json")
        .header("Authorization", "Bearer " + apiKey)
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();
  }

  // CHECKSTYLE:ON: LineLength
  /**
   * Parses the JSON response body from the API to extract the message content.
   *
   * @param responseBody The JSON string received from the API.
   * @return The extracted message content as a String.
   * @throws IOException if there is a problem parsing the JSON.
   */
  private static String parseHttpResponse(String responseBody) throws IOException {
    // Parse response body to extract content inside message
    JsonNode rootNode = OBJECT_MAPPER.readTree(responseBody);
    return rootNode.path("choices").get(0).path("message").path("content").asText();
  }

  public static void setHttpClient(HttpClient client) {
    HTTP_CLIENT = client;
  }

  public static void setDotenv(Dotenv dotenv) {
    DOTENV = dotenv;
  }
}