Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/main/java/com/backend/BackendApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
90 changes: 45 additions & 45 deletions src/main/java/com/backend/services/ProdAIService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dish> 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.

Expand Down Expand Up @@ -78,48 +90,38 @@ ALLERGEN CODES (use exact strings):

Return only the JSON array of dishes.
""";
int maxRetries = 5;
List<AIDishDTO> AIdishDTOs = new ArrayList<>();
// main query to the LLM with image and prompt, automatically deserializing response to List<DishDTO>
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<List<AIDishDTO>>() {
});
} 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<Dish>();
}
}
// Convert DishDTOs to Dish entities and save them to the database
List<AIDishDTO> 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<AIDishDTO>>() {});



List<Dish> 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<Dish> 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());
Expand All @@ -128,6 +130,4 @@ private Dish convertToEntity(AIDishDTO dto) {
dish.setAllergens(dto.getAllergens());
return dish;
}
}


}
Loading