From 2516c6097518cb868fb15d39750f0cdc5d685f9c Mon Sep 17 00:00:00 2001 From: avvvis Date: Fri, 10 Apr 2026 18:46:34 +0200 Subject: [PATCH] implemented working error handling and retry system suing Spring Retry --- .../java/com/backend/BackendApplication.java | 2 + .../com/backend/services/ProdAIService.java | 90 +++++++++---------- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/backend/BackendApplication.java b/src/main/java/com/backend/BackendApplication.java index 42c55ed..7dfea56 100644 --- a/src/main/java/com/backend/BackendApplication.java +++ b/src/main/java/com/backend/BackendApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling +@EnableRetry public class BackendApplication { public static void main(String[] args) { diff --git a/src/main/java/com/backend/services/ProdAIService.java b/src/main/java/com/backend/services/ProdAIService.java index 393bdaf..44582b9 100644 --- a/src/main/java/com/backend/services/ProdAIService.java +++ b/src/main/java/com/backend/services/ProdAIService.java @@ -8,28 +8,40 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.ai.content.Media; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.context.annotation.Profile; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ByteArrayResource; -import org.springframework.http.*; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.retry.support.RetrySynchronizationManager; import org.springframework.stereotype.Service; import org.springframework.util.MimeTypeUtils; -import org.springframework.ai.chat.client.ChatClient; - -import java.util.List; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; @Service @RequiredArgsConstructor @Slf4j -@Profile("!dev") +@Profile({"prod"}) public class ProdAIService implements MenuAIService { + private final ChatClient.Builder chatClientBuilder; private final DishRepository dishRepository; - + @Retryable( + retryFor = Exception.class, + maxAttempts = 5, + backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s + ) + @Override public List parseMenuFromImage(byte[] imageBytes) { + int attempt = RetrySynchronizationManager.getContext().getRetryCount() + 1; + log.info("Sending menu to AI - attempt {}/5", attempt); + String prompt = """ You are a specialized menu digitization assistant for Polish restaurants. Analyze the provided image of a handwritten menu and extract ALL visible items. @@ -78,48 +90,38 @@ ALLERGEN CODES (use exact strings): Return only the JSON array of dishes. """; - int maxRetries = 5; - List AIdishDTOs = new ArrayList<>(); - // main query to the LLM with image and prompt, automatically deserializing response to List - for(int i = 0; i < 5; i++) { - System.out.println("sending menu to AI - attempt" + i + "/" + maxRetries); - try { - AIdishDTOs = chatClientBuilder.build() - .prompt() - .user(userSpec -> userSpec - .text(prompt) - .media(Media.builder() - .mimeType(MimeTypeUtils.IMAGE_JPEG) - .data(new ByteArrayResource(imageBytes)) - .build())) - .call() - .entity(new ParameterizedTypeReference>() { - }); - } catch (Exception e) { - System.out.println("gpt api request failed try" + i + "/" + maxRetries); - log.error("Error when sending request to gpt api: {}", e.getMessage()); - continue; - } - if(i < maxRetries-1 && !AIdishDTOs.isEmpty()) { - System.out.println("successfully got valid response from gpt api"); - break; - } else { - log.error("failed to get valid response from gpt api after " + maxRetries + " attempts, returning empty menu. If no errors returned before, there was probably no items visible in the photo"); - return new ArrayList(); - } - } - // Convert DishDTOs to Dish entities and save them to the database + List aiDishDTOs = chatClientBuilder.build() // ← .build() from newer version + .prompt() + .user(userSpec -> userSpec + .text(prompt) + .media(Media.builder() + .mimeType(MimeTypeUtils.IMAGE_JPEG) + .data(new ByteArrayResource(imageBytes)) + .build())) + .call() + .entity(new ParameterizedTypeReference>() {}); + + + List savedDishes = new ArrayList<>(); - for (AIDishDTO dto : AIdishDTOs) { - Dish dish = convertToEntity(dto); - Dish savedDish = dishRepository.save(dish); - savedDishes.add(savedDish); + for (AIDishDTO dto : aiDishDTOs) { + savedDishes.add(dishRepository.save(convertToEntity(dto))); } + if (savedDishes.isEmpty()) { + throw new IllegalStateException("Empty response from GPT - no dishes extracted"); + } + + log.info("Successfully parsed and saved {} dishes from image", savedDishes.size()); return savedDishes; } - // Helper method to convert DishDTO to Dish entity + @Recover + public List recover(Exception e, byte[] imageBytes) { + log.error("All GPT retry attempts exhausted: {}", e.getMessage()); + return Collections.emptyList(); + } + private Dish convertToEntity(AIDishDTO dto) { Dish dish = new Dish(); dish.setName(dto.getName()); @@ -128,6 +130,4 @@ private Dish convertToEntity(AIDishDTO dto) { dish.setAllergens(dto.getAllergens()); return dish; } -} - - +} \ No newline at end of file