Some various refactoring. Added more testing for recipe drafts controller.
This commit is contained in:
parent
0a83a032c8
commit
54118d597e
@ -1,23 +1,36 @@
|
||||
package app.mealsmadeeasy.api.recipe;
|
||||
|
||||
import app.mealsmadeeasy.api.IntegrationTestsExtension;
|
||||
import app.mealsmadeeasy.api.ai.InferenceService;
|
||||
import app.mealsmadeeasy.api.ai.OcrService;
|
||||
import app.mealsmadeeasy.api.auth.AuthService;
|
||||
import app.mealsmadeeasy.api.auth.LoginException;
|
||||
import app.mealsmadeeasy.api.recipe.body.RecipeDraftUpdateBody;
|
||||
import app.mealsmadeeasy.api.recipe.view.RecipeDraftView;
|
||||
import app.mealsmadeeasy.api.user.User;
|
||||
import app.mealsmadeeasy.api.user.UserCreateException;
|
||||
import app.mealsmadeeasy.api.user.UserService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@ -38,6 +51,9 @@ public class RecipeDraftsControllerIntegrationTests {
|
||||
@Autowired
|
||||
private RecipeService recipeService;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private static final String TEST_PASSWORD = "test";
|
||||
|
||||
private User seedUser() {
|
||||
@ -114,4 +130,118 @@ public class RecipeDraftsControllerIntegrationTests {
|
||||
.andExpect(jsonPath("$", hasSize(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manualDraft_returnsDraft() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
this.mockMvc.perform(
|
||||
post("/recipe-drafts/manual")
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.created", is(notNullValue())))
|
||||
.andExpect(jsonPath("$.state", is(RecipeDraft.State.ENTER_DATA.toString())))
|
||||
.andExpect(jsonPath("$.owner.id", is(owner.getId())));
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class AiDraftTestsWithMocks {
|
||||
|
||||
@MockitoBean
|
||||
private OcrService ocrService;
|
||||
|
||||
@MockitoBean
|
||||
private InferenceService inferenceService;
|
||||
|
||||
@Test
|
||||
public void whenAiDraft_returnsDraft() throws Exception {
|
||||
final User owner = seedUser();
|
||||
final MockMultipartFile sourceFile = new MockMultipartFile(
|
||||
"sourceFile",
|
||||
"recipe.jpeg",
|
||||
MimeTypeUtils.IMAGE_JPEG_VALUE,
|
||||
AiDraftTestsWithMocks.class.getResourceAsStream("recipe.jpeg")
|
||||
);
|
||||
mockMvc.perform(
|
||||
multipart("/recipe-drafts/ai")
|
||||
.file(sourceFile)
|
||||
.param("sourceFileName", sourceFile.getOriginalFilename())
|
||||
.header("Authorization", "Bearer " + getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.created", is(notNullValue())))
|
||||
.andExpect(jsonPath("$.state", is(RecipeDraft.State.INFER.toString())))
|
||||
.andExpect(jsonPath("$.owner.id", is(owner.getId())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pollAiDraft_returnsDraft() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final MockMultipartFile sourceFile = new MockMultipartFile(
|
||||
"sourceFile",
|
||||
"recipe.jpeg",
|
||||
MimeTypeUtils.IMAGE_JPEG_VALUE,
|
||||
RecipeDraftsControllerIntegrationTests.class.getResourceAsStream("recipe.jpeg")
|
||||
);
|
||||
final MvcResult multipartResult = this.mockMvc.perform(
|
||||
multipart("/recipe-drafts/ai")
|
||||
.file(sourceFile)
|
||||
.param("sourceFileName", sourceFile.getOriginalFilename())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
final String rawContent = multipartResult.getResponse().getContentAsString();
|
||||
final RecipeDraftView recipeDraftView = this.objectMapper.readValue(rawContent, RecipeDraftView.class);
|
||||
|
||||
await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> {
|
||||
this.mockMvc.perform(
|
||||
get("/recipe-drafts/{id}", recipeDraftView.id())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.state", is(RecipeDraft.State.ENTER_DATA.toString())));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUpdate_returnsUpdated() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
final RecipeDraftUpdateBody updateBody = RecipeDraftUpdateBody.builder()
|
||||
.title("Test Title")
|
||||
.slug("test-slug")
|
||||
.preparationTime(15)
|
||||
.cookingTime(30)
|
||||
.totalTime(45)
|
||||
.rawText("Hello, World!")
|
||||
.ingredients(List.of(
|
||||
RecipeDraftUpdateBody.IngredientDraftUpdateBody.builder()
|
||||
.amount("1 Unit")
|
||||
.name("Test Ingredient")
|
||||
.notes("Separated")
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
this.mockMvc.perform(
|
||||
put("/recipe-drafts/{id}", recipeDraft.getId())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
.header("Content-Type", "application/json")
|
||||
.content(this.objectMapper.writeValueAsString(updateBody))
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.title", is("Test Title")))
|
||||
.andExpect(jsonPath("$.slug", is("test-slug")))
|
||||
.andExpect(jsonPath("$.preparationTime", is(15)))
|
||||
.andExpect(jsonPath("$.cookingTime", is(30)))
|
||||
.andExpect(jsonPath("$.totalTime", is(45)))
|
||||
.andExpect(jsonPath("$.rawText", is("Hello, World!")))
|
||||
.andExpect(jsonPath("$.ingredients", hasSize(1)))
|
||||
.andExpect(jsonPath("$.ingredients[0].amount", is("1 Unit")))
|
||||
.andExpect(jsonPath("$.ingredients[0].name", is("Test Ingredient")))
|
||||
.andExpect(jsonPath("$.ingredients[0].notes", is("Separated")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
41
src/main/java/app/mealsmadeeasy/api/ai/InferenceService.java
Normal file
41
src/main/java/app/mealsmadeeasy/api/ai/InferenceService.java
Normal file
@ -0,0 +1,41 @@
|
||||
package app.mealsmadeeasy.api.ai;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.ollama.api.OllamaChatOptions;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class InferenceService {
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
public <T> @Nullable T extract(
|
||||
Message systemMessage,
|
||||
Message userMessage,
|
||||
BeanOutputConverter<T> converter
|
||||
) {
|
||||
final ChatOptions extractChatOptions = OllamaChatOptions.builder()
|
||||
.model("gemma3:latest")
|
||||
.format(converter.getJsonSchemaMap())
|
||||
.build();
|
||||
final Prompt extractPrompt = Prompt.builder()
|
||||
.messages(systemMessage, userMessage)
|
||||
.chatOptions(extractChatOptions)
|
||||
.build();
|
||||
final ChatResponse extractResponse = this.chatModel.call(extractPrompt);
|
||||
final String extractContent = extractResponse.getResult().getOutput().getText();
|
||||
if (extractContent == null) {
|
||||
return null;
|
||||
}
|
||||
return converter.convert(extractContent);
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/java/app/mealsmadeeasy/api/ai/OcrService.java
Normal file
33
src/main/java/app/mealsmadeeasy/api/ai/OcrService.java
Normal file
@ -0,0 +1,33 @@
|
||||
package app.mealsmadeeasy.api.ai;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.ollama.api.OllamaChatOptions;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OcrService {
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
public @Nullable String readMarkdown(Media inputMedia) {
|
||||
final Message ocrMessage = UserMessage.builder()
|
||||
.text("Convert the text in the given file to Markdown.")
|
||||
.media(inputMedia)
|
||||
.build();
|
||||
final Prompt ocrPrompt = Prompt.builder()
|
||||
.messages(ocrMessage)
|
||||
.chatOptions(OllamaChatOptions.builder().model("deepseek-ocr:latest").build())
|
||||
.build();
|
||||
final ChatResponse ocrResponse = this.chatModel.call(ocrPrompt);
|
||||
return ocrResponse.getResult().getOutput().getText();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
package app.mealsmadeeasy.api.inference;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/inferences")
|
||||
public class InferenceController {
|
||||
|
||||
private final InferenceService inferenceService;
|
||||
|
||||
public InferenceController(InferenceService inferenceService) {
|
||||
this.inferenceService = inferenceService;
|
||||
}
|
||||
|
||||
@PutMapping("/recipe-extract")
|
||||
public ResponseEntity<String> recipeExtract(@RequestParam MultipartFile recipeImageFile) throws IOException {
|
||||
return ResponseEntity.ok(this.inferenceService.extractRecipe(recipeImageFile.getInputStream()));
|
||||
}
|
||||
|
||||
@PutMapping(value = "/recipe-extract-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<ServerSentEvent<Map<String, String>>> recipeExtractStream(@RequestParam MultipartFile recipeImageFile) throws IOException {
|
||||
return this.inferenceService.extractRecipeStream(recipeImageFile.getInputStream())
|
||||
.map(data -> ServerSentEvent.builder(Map.of("delta", data)).build());
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
package app.mealsmadeeasy.api.inference;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.MimeType;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@Service
|
||||
public class InferenceService {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
public InferenceService(ChatClient.Builder chatClientBuilder) {
|
||||
this.chatClient = chatClientBuilder.build();
|
||||
}
|
||||
|
||||
public String extractRecipe(InputStream recipeImageInputStream) {
|
||||
final Media media = Media.builder()
|
||||
.data(new InputStreamResource(recipeImageInputStream))
|
||||
.mimeType(MimeType.valueOf("image/jpeg"))
|
||||
.build();
|
||||
|
||||
final String markdownResponse = this.chatClient.prompt()
|
||||
.user(u ->
|
||||
u.text(new ClassPathResource("app/mealsmadeeasy/api/inference/recipe-extract-user-prompt.md"))
|
||||
.media(media)
|
||||
)
|
||||
.call()
|
||||
.content();
|
||||
return markdownResponse;
|
||||
}
|
||||
|
||||
public Flux<String> extractRecipeStream(InputStream recipeImageInputStream) {
|
||||
final Media media = Media.builder()
|
||||
.data(new InputStreamResource(recipeImageInputStream))
|
||||
.mimeType(MimeType.valueOf("image/jpeg"))
|
||||
.build();
|
||||
|
||||
return this.chatClient.prompt()
|
||||
.user(u ->
|
||||
u.text(new ClassPathResource("app/mealsmadeeasy/api/inference/recipe-extract-user-prompt.md"))
|
||||
.media(media)
|
||||
)
|
||||
.stream().content();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package app.mealsmadeeasy.api.inference;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "recipe_extraction")
|
||||
public class RecipeExtractionEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
@Basic(fetch = FetchType.LAZY)
|
||||
private String markdown;
|
||||
|
||||
public UUID getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMarkdown() {
|
||||
return this.markdown;
|
||||
}
|
||||
|
||||
public void setMarkdown(String markdown) {
|
||||
this.markdown = markdown;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
package app.mealsmadeeasy.api.inference;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class RecipeExtractionView {
|
||||
|
||||
public static RecipeExtractionView from(RecipeExtractionEntity entity) {
|
||||
final var view = new RecipeExtractionView();
|
||||
view.setId(entity.getId());
|
||||
view.setMarkdown(entity.getMarkdown());
|
||||
return view;
|
||||
}
|
||||
|
||||
private UUID id;
|
||||
private String markdown;
|
||||
|
||||
public UUID getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMarkdown() {
|
||||
return this.markdown;
|
||||
}
|
||||
|
||||
public void setMarkdown(String markdown) {
|
||||
this.markdown = markdown;
|
||||
}
|
||||
|
||||
}
|
||||
@ -82,12 +82,12 @@ public class Recipe {
|
||||
private RecipeEmbedding embedding;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
private void prePersist() {
|
||||
this.created = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
private void preUpdate() {
|
||||
this.modified = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
|
||||
@ -71,4 +71,14 @@ public class RecipeDraft {
|
||||
@Column(columnDefinition = "jsonb")
|
||||
private @Nullable List<RecipeDraftInference> inferences;
|
||||
|
||||
@PrePersist
|
||||
private void prePersist() {
|
||||
this.created = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
private void preUpdate() {
|
||||
this.modified = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ public class RecipeDraftsController {
|
||||
throw new MustBeLoggedInException();
|
||||
}
|
||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||
return ResponseEntity.ok(this.draftToViewConverter.convert(recipeDraft, owner));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(this.draftToViewConverter.convert(recipeDraft, owner));
|
||||
}
|
||||
|
||||
@PostMapping("/ai")
|
||||
|
||||
@ -46,7 +46,6 @@ public class RecipeService {
|
||||
|
||||
public Recipe create(User owner, RecipeCreateSpec spec) {
|
||||
final Recipe draft = new Recipe();
|
||||
draft.setCreated(OffsetDateTime.now());
|
||||
draft.setOwner(owner);
|
||||
draft.setSlug(spec.getSlug());
|
||||
draft.setTitle(spec.getTitle());
|
||||
@ -125,33 +124,26 @@ public class RecipeService {
|
||||
}
|
||||
|
||||
private void prepareForUpdate(RecipeUpdateSpec spec, Recipe recipe, User modifier) {
|
||||
boolean didUpdate = false;
|
||||
if (spec.getTitle() != null) {
|
||||
recipe.setTitle(spec.getTitle());
|
||||
didUpdate = true;
|
||||
}
|
||||
if (spec.getPreparationTime() != null) {
|
||||
recipe.setPreparationTime(spec.getPreparationTime());
|
||||
didUpdate = true;
|
||||
}
|
||||
if (spec.getCookingTime() != null) {
|
||||
recipe.setCookingTime(spec.getCookingTime());
|
||||
didUpdate = true;
|
||||
}
|
||||
if (spec.getTotalTime() != null) {
|
||||
recipe.setTotalTime(spec.getTotalTime());
|
||||
didUpdate = true;
|
||||
}
|
||||
|
||||
if (spec.getRawText() != null) {
|
||||
recipe.setRawText(spec.getRawText());
|
||||
recipe.setCachedRenderedText(null);
|
||||
didUpdate = true;
|
||||
}
|
||||
|
||||
if (spec.getIsPublic() != null) {
|
||||
recipe.setIsPublic(spec.getIsPublic());
|
||||
didUpdate = true;
|
||||
}
|
||||
|
||||
// TODO: we have to think about how to unset the main image vs. just leaving it out of the request
|
||||
@ -163,10 +155,6 @@ public class RecipeService {
|
||||
);
|
||||
recipe.setMainImage(mainImage);
|
||||
}
|
||||
|
||||
if (didUpdate) {
|
||||
recipe.setModified(OffsetDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
@PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)")
|
||||
@ -233,7 +221,6 @@ public class RecipeService {
|
||||
public RecipeDraft createDraft(User owner) {
|
||||
final var recipeDraft = new RecipeDraft();
|
||||
recipeDraft.setState(RecipeDraft.State.ENTER_DATA);
|
||||
recipeDraft.setCreated(OffsetDateTime.now());
|
||||
recipeDraft.setOwner(owner);
|
||||
return this.recipeDraftRepository.save(recipeDraft);
|
||||
}
|
||||
@ -242,7 +229,6 @@ public class RecipeService {
|
||||
public RecipeDraft createAiDraft(File sourceFile, User owner) {
|
||||
final var recipeDraft = new RecipeDraft();
|
||||
recipeDraft.setState(RecipeDraft.State.INFER);
|
||||
recipeDraft.setCreated(OffsetDateTime.now());
|
||||
recipeDraft.setOwner(owner);
|
||||
|
||||
final var saved = this.recipeDraftRepository.save(recipeDraft);
|
||||
@ -311,7 +297,6 @@ public class RecipeService {
|
||||
if (spec.mainImage() != null) {
|
||||
recipeDraft.setMainImage(spec.mainImage());
|
||||
}
|
||||
recipeDraft.setModified(OffsetDateTime.now());
|
||||
return this.recipeDraftRepository.save(recipeDraft);
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package app.mealsmadeeasy.api.recipe.body;
|
||||
|
||||
import app.mealsmadeeasy.api.util.SetImageBody;
|
||||
import lombok.Builder;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Builder
|
||||
public record RecipeDraftUpdateBody(
|
||||
@Nullable String slug,
|
||||
@Nullable String title,
|
||||
@ -16,6 +18,7 @@ public record RecipeDraftUpdateBody(
|
||||
@Nullable SetImageBody mainImage
|
||||
) {
|
||||
|
||||
@Builder
|
||||
public record IngredientDraftUpdateBody(
|
||||
@Nullable String amount,
|
||||
String name,
|
||||
|
||||
@ -25,6 +25,7 @@ public class RecipeDraftToViewConverter {
|
||||
.created(recipeDraft.getCreated())
|
||||
.modified(recipeDraft.getModified())
|
||||
.state(recipeDraft.getState())
|
||||
.title(recipeDraft.getTitle())
|
||||
.slug(recipeDraft.getSlug())
|
||||
.preparationTime(recipeDraft.getPreparationTime())
|
||||
.cookingTime(recipeDraft.getCookingTime())
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package app.mealsmadeeasy.api.recipe.job;
|
||||
|
||||
import app.mealsmadeeasy.api.ai.InferenceService;
|
||||
import app.mealsmadeeasy.api.ai.OcrService;
|
||||
import app.mealsmadeeasy.api.file.File;
|
||||
import app.mealsmadeeasy.api.file.FileService;
|
||||
import app.mealsmadeeasy.api.job.Job;
|
||||
@ -14,13 +16,8 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.ollama.api.OllamaChatOptions;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.MimeType;
|
||||
@ -52,7 +49,8 @@ public class RecipeInferJobHandler implements JobHandler<RecipeInferJobPayload>
|
||||
|
||||
private final FileService fileService;
|
||||
private final RecipeService recipeService;
|
||||
private final ChatModel chatModel;
|
||||
private final OcrService ocrService;
|
||||
private final InferenceService inferenceService;
|
||||
private final BeanOutputConverter<RecipeExtraction> extractionConverter =
|
||||
new BeanOutputConverter<>(RecipeExtraction.class);
|
||||
|
||||
@ -82,17 +80,7 @@ public class RecipeInferJobHandler implements JobHandler<RecipeInferJobPayload>
|
||||
.data(new InputStreamResource(sourceFileContent))
|
||||
.mimeType(MimeType.valueOf(sourceFile.getMimeType()))
|
||||
.build();
|
||||
|
||||
final Message ocrMessage = UserMessage.builder()
|
||||
.text("Convert the recipe in the given file to Markdown.")
|
||||
.media(sourceFileMedia)
|
||||
.build();
|
||||
final Prompt ocrPrompt = Prompt.builder()
|
||||
.messages(ocrMessage)
|
||||
.chatOptions(OllamaChatOptions.builder().model("deepseek-ocr:latest").build())
|
||||
.build();
|
||||
final ChatResponse ocrResponse = this.chatModel.call(ocrPrompt);
|
||||
final String fullMarkdownText = ocrResponse.getResult().getOutput().getText();
|
||||
final @Nullable String fullMarkdownText = this.ocrService.readMarkdown(sourceFileMedia);
|
||||
if (fullMarkdownText == null) {
|
||||
throw new RuntimeException("fullMarkdownText from ocr came back null");
|
||||
}
|
||||
@ -132,22 +120,17 @@ public class RecipeInferJobHandler implements JobHandler<RecipeInferJobPayload>
|
||||
final Message extractUserMessage = UserMessage.builder()
|
||||
.text(fullMarkdownText)
|
||||
.build();
|
||||
final ChatOptions extractChatOptions = OllamaChatOptions.builder()
|
||||
.model("gemma3:latest")
|
||||
.format(extractionConverter.getJsonSchemaMap())
|
||||
.build();
|
||||
final Prompt extractPrompt = Prompt.builder()
|
||||
.messages(extractSystemMessage, extractUserMessage)
|
||||
.chatOptions(extractChatOptions)
|
||||
.build();
|
||||
final ChatResponse extractResponse = this.chatModel.call(extractPrompt);
|
||||
final String extractContent = extractResponse.getResult().getOutput().getText();
|
||||
if (extractContent == null) {
|
||||
throw new RuntimeException("extractContent from extract came back null");
|
||||
}
|
||||
logger.debug("extract returned:\n{}", extractContent);
|
||||
|
||||
final RecipeExtraction extraction = this.extractionConverter.convert(extractContent);
|
||||
final @Nullable RecipeExtraction extraction = this.inferenceService.extract(
|
||||
extractSystemMessage,
|
||||
extractUserMessage,
|
||||
this.extractionConverter
|
||||
);
|
||||
if (extraction == null) {
|
||||
throw new RuntimeException("extraction returned null");
|
||||
}
|
||||
|
||||
logger.debug("extract returned:\n{}", extraction);
|
||||
|
||||
// 3. Get draft and set props from extraction
|
||||
final @Nullable RecipeDraft recipeDraft = this.recipeService.getDraftById(payload.recipeDraftId());
|
||||
|
||||
Loading…
Reference in New Issue
Block a user