Skip to content
Merged
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
10 changes: 10 additions & 0 deletions docs/API-feat6.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ Response body:
}
```

**Response 503 SERVICE UNAVAILABLE**

Response body:
```json
{
"error": "SERVICE_UNAVAILABLE",
"message": "Our AI service is currently unavailable. Please try again later."
}
```

**Path:** `api/resume-ai-advisor/remaining-quota`

**Method:** `GET`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.backend.coapp.exception.genai;

/** This exception will be thrown when we reach usage limit. User need to try again later. */
public class GenAIOutOfServiceException extends RuntimeException {
public GenAIOutOfServiceException(String message) {
super("Our AI service failure. " + message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;
import tools.jackson.databind.exc.InvalidFormatException;

/** Exception handler for controller. */
/**
* Exception handler for controller.
*
* <p>Internal exceptions (5** HTTP status) will be logged for debugging.
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
Expand Down Expand Up @@ -63,7 +67,7 @@ public ResponseEntity<Map<String, Object>> handleEmailServiceException(EmailServ

@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex) {
String errorMessage = "ERROR: Reset verification code service failed: " + ex.getMessage();
String errorMessage = "ERROR: Runtime exception: " + ex.getMessage();
log.error(errorMessage);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
Expand Down Expand Up @@ -112,7 +116,7 @@ public ResponseEntity<Map<String, Object>> handleAuthAccountAlreadyVerifyExcepti
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<Map<String, Object>> handleAuthenticationException(
AuthenticationException ex) {
String errorMessage = "ERROR: JWT Service failed: " + ex.getMessage();
String errorMessage = "ERROR: Authentication Service failed: " + ex.getMessage();
log.error(errorMessage);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
Expand Down Expand Up @@ -333,7 +337,7 @@ public ResponseEntity<Map<String, Object>> handleConcurrencyException(Concurrenc
@ExceptionHandler(GenAIUsageManagementServiceException.class)
public ResponseEntity<Map<String, Object>> handleGenAIUsageManagementServiceException(
GenAIUsageManagementServiceException ex) {
String errorMessage = "ERROR: Application Service failed: " + ex.getMessage();
String errorMessage = "ERROR: GenAI Service failed: " + ex.getMessage();
log.error(errorMessage);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
Expand Down Expand Up @@ -385,4 +389,33 @@ public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadable(
return ResponseEntity.badRequest()
.body(Map.of("error", RequestErrorCode.INVALID_FORMAT_FIELD.name(), "message", message));
}

@ExceptionHandler(GenAIOutOfServiceException.class)
public ResponseEntity<Map<String, Object>> handleGenAIOutOfServiceException(
GenAIOutOfServiceException ex) {

String errorMessage = "ERROR: Resume workshop Service failed: " + ex.getMessage();
log.error(errorMessage);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(
Map.of(
"error",
GenAIErrorCode.SERVICE_UNAVAILABLE,
"message",
"Our AI service is currently unavailable. Please try again later."));
}

@ExceptionHandler(GenAIServiceException.class)
public ResponseEntity<Map<String, Object>> handleGenAIServiceException(GenAIServiceException ex) {

String errorMessage = "ERROR: GenAI Service failed: " + ex.getMessage();
log.error(errorMessage);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
Map.of(
"error",
SystemErrorCode.INTERNAL_ERROR,
"message",
"GenAI Service failed. Please try again later."));
}
Comment on lines +408 to +420
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized I missed handling this exception so I added it here

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum GenAIErrorCode {
OVER_LIMIT_CHARACTER,
OVER_LIMIT_CHATBOT_REQUEST,
OTHER_REQUEST_IN_PROGRESS
OTHER_REQUEST_IN_PROGRESS,
SERVICE_UNAVAILABLE
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

import com.backend.coapp.exception.application.ApplicationNotFoundException;
import com.backend.coapp.exception.application.ApplicationNotOwnedException;
import com.backend.coapp.exception.genai.ConcurrencyException;
import com.backend.coapp.exception.genai.GenAIQuotaExceededException;
import com.backend.coapp.exception.genai.GenAIUsageManagementServiceException;
import com.backend.coapp.exception.genai.OverCharacterLimitException;
import com.backend.coapp.exception.genai.*;
import com.backend.coapp.exception.global.UserNotFoundException;
import com.backend.coapp.model.document.ApplicationModel;
import com.backend.coapp.model.document.UserExperienceModel;
Expand Down Expand Up @@ -57,6 +54,8 @@ public GenAIResumeAdvisorService(
* @throws UserNotFoundException when user doesn't exist
* @throws GenAIQuotaExceededException when user exceed GenAI usage limit
* @throws ConcurrencyException when the same user make a request twice
* @throws GenAIOutOfServiceException when we reach usage limit (internally)
* @throws GenAIServiceException when there is something wrong for GenAI service (internally)
*/
public String getAdvice(String userId, String applicationId, String prompt)
throws OverCharacterLimitException,
Expand All @@ -65,7 +64,9 @@ public String getAdvice(String userId, String applicationId, String prompt)
GenAIUsageManagementServiceException,
UserNotFoundException,
GenAIQuotaExceededException,
ConcurrencyException {
ConcurrencyException,
GenAIOutOfServiceException,
GenAIServiceException {
String applicationJobDescription = null;
String applicationJobTitle = null;
if (prompt.length() > GenAIConstants.MAX_PROMPT_CHARACTERS) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.backend.coapp.service.genAI;

import com.backend.coapp.exception.genai.GenAIOutOfServiceException;
import com.backend.coapp.exception.genai.GenAIServiceException;
import com.backend.coapp.exception.genai.OverCharacterLimitException;
import com.backend.coapp.util.GenAIConstants;
import com.google.genai.Client;
import com.google.genai.errors.ApiException;
import com.google.genai.types.GenerateContentResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@ConditionalOnProperty(name = "gen-ai.provider", havingValue = "gemini")
public class GeminiGenAIService implements GenAIService {
private final Client geminiClient;
Expand All @@ -29,11 +34,12 @@ public GeminiGenAIService(Client geminiClient) {
* @param prompt client's prompt
* @return GenAI response
* @throws IllegalArgumentException when invalid input
* @throws GenAIServiceException when there is something wrong with GenAI provider.
* @throws GenAIServiceException when there is something wrong with GenAI provider
* @throws GenAIOutOfServiceException when we reach usage limit (internally)
*/
@Override
public String generateResponse(String prompt)
throws IllegalArgumentException, GenAIServiceException {
throws IllegalArgumentException, GenAIServiceException, GenAIOutOfServiceException {
if (prompt == null || prompt.isBlank()) {
throw new IllegalArgumentException("Prompt can't be null or blank");
}
Expand All @@ -47,6 +53,12 @@ public String generateResponse(String prompt)
GenerateContentResponse response =
geminiClient.models.generateContent(this.model, prompt, null);
return response.text();
} catch (ApiException e) {
if (e.code() == HttpStatus.TOO_MANY_REQUESTS.value() || (e.code() >= 500 && e.code() < 600)) {
// This includes 503 - SERVICE UNAVAILABLE and 500 - INTERNAL ERROR
throw new GenAIOutOfServiceException(e.getMessage());
}
throw new GenAIServiceException(e.getMessage());
} catch (Exception e) {
throw new GenAIServiceException(e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@
import com.backend.coapp.dto.request.GenAIResumeAdvisorRequest;
import com.backend.coapp.exception.application.ApplicationNotFoundException;
import com.backend.coapp.exception.application.ApplicationNotOwnedException;
import com.backend.coapp.exception.genai.ConcurrencyException;
import com.backend.coapp.exception.genai.GenAIQuotaExceededException;
import com.backend.coapp.exception.genai.GenAIUsageManagementServiceException;
import com.backend.coapp.exception.genai.OverCharacterLimitException;
import com.backend.coapp.exception.genai.*;
import com.backend.coapp.exception.global.UserNotFoundException;
import com.backend.coapp.model.document.UserModel;
import com.backend.coapp.model.enumeration.GenAIErrorCode;
import com.backend.coapp.model.enumeration.SystemErrorCode;
import com.backend.coapp.model.enumeration.UserErrorCode;
import com.backend.coapp.service.GenAIResumeAdvisorService;
Expand Down Expand Up @@ -256,6 +254,42 @@ void resumeAdvisor_whenServiceFails_expect500() throws Exception {
.getAdvice(mockUser.getId(), validRequest.getApplicationId(), validRequest.getUserPrompt());
}

@Test
void resumeAdvisor_whenGenAIServiceFails_expect500() throws Exception {
when(genAIResumeAdvisorService.getAdvice(anyString(), any(), anyString()))
.thenThrow(new GenAIServiceException("Internal error"));

mockMvc
.perform(
post("/api/resume-ai-advisor")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(validRequest))
.principal(this.authentication))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.error").value(SystemErrorCode.INTERNAL_ERROR.name()));

verify(genAIResumeAdvisorService, times(1))
.getAdvice(mockUser.getId(), validRequest.getApplicationId(), validRequest.getUserPrompt());
}

@Test
void resumeAdvisor_whenReachLimit_expect503() throws Exception {
when(genAIResumeAdvisorService.getAdvice(anyString(), any(), anyString()))
.thenThrow(new GenAIOutOfServiceException("foo exception"));

mockMvc
.perform(
post("/api/resume-ai-advisor")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(validRequest))
.principal(this.authentication))
.andExpect(status().isServiceUnavailable())
.andExpect(jsonPath("$.error").value(GenAIErrorCode.SERVICE_UNAVAILABLE.name()));

verify(genAIResumeAdvisorService, times(1))
.getAdvice(mockUser.getId(), validRequest.getApplicationId(), validRequest.getUserPrompt());
}

@Test
void getRemainingQuota_whenSuccess_expectOkAndRemainingQuota() throws Exception {
when(genAIUsageManagementService.getNumberOfRequestLeft(anyString())).thenReturn(7);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;

import com.backend.coapp.exception.genai.GenAIOutOfServiceException;
import com.backend.coapp.exception.genai.GenAIServiceException;
import com.backend.coapp.exception.genai.OverCharacterLimitException;
import com.backend.coapp.util.GenAIConstants;
import com.google.genai.Client;
import com.google.genai.Models;
import com.google.genai.errors.ApiException;
import com.google.genai.types.GenerateContentResponse;
import java.lang.reflect.Field;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -18,6 +20,7 @@
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import org.springframework.http.HttpStatus;

class GeminiGenAIServiceTest {

Expand Down Expand Up @@ -90,4 +93,63 @@ void generateResponse_whenPromptExceedsMaxCharacters_expectOverCharacterLimitExc
verifyNoInteractions(models);
assertNotNull(ex);
}

@Test
void generateResponse_whenGeminiClientThrows429_expectGenAIOutOfServiceException() {
when(models.generateContent(eq(MODEL), eq(VALID_PROMPT), isNull()))
.thenThrow(
new ApiException(
HttpStatus.TOO_MANY_REQUESTS.value(),
HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase(),
"Please try again"));

GenAIOutOfServiceException ex =
assertThrows(
GenAIOutOfServiceException.class,
() -> geminiGenAIService.generateResponse(VALID_PROMPT));

assertNotNull(ex.getMessage());
}

@ParameterizedTest
@ValueSource(ints = {429, 503, 500})
void generateResponse_whenGeminiClientThrowsOutOfServiceStatus_expectGenAIOutOfServiceException(
int statusCode) {
HttpStatus httpStatus = HttpStatus.valueOf(statusCode);
when(models.generateContent(eq(MODEL), eq(VALID_PROMPT), isNull()))
.thenThrow(new ApiException(statusCode, httpStatus.getReasonPhrase(), "Please try again"));

GenAIOutOfServiceException ex =
assertThrows(
GenAIOutOfServiceException.class,
() -> geminiGenAIService.generateResponse(VALID_PROMPT));

assertNotNull(ex.getMessage());
}

@ParameterizedTest
@ValueSource(ints = {400, 600})
void generateResponse_whenGeminiClientThrowsNonOutOfServiceStatus_expectGenAIServiceException(
int statusCode) {
when(models.generateContent(eq(MODEL), eq(VALID_PROMPT), isNull()))
.thenThrow(new ApiException(statusCode, "Error", "Something went wrong"));

GenAIServiceException ex =
assertThrows(
GenAIServiceException.class, () -> geminiGenAIService.generateResponse(VALID_PROMPT));

assertNotNull(ex.getMessage());
}

@Test
void generateResponse_whenGeminiClientThrowsRuntimeException_expectGenAIServiceException() {
when(models.generateContent(eq(MODEL), eq(VALID_PROMPT), isNull()))
.thenThrow(new RuntimeException());

GenAIServiceException ex =
assertThrows(
GenAIServiceException.class, () -> geminiGenAIService.generateResponse(VALID_PROMPT));

assertNotNull(ex.getMessage());
}
}
Loading