From 104843039b4e3a743b69466a7894880ec61b8bfb Mon Sep 17 00:00:00 2001 From: Manfred Ng Date: Sat, 2 Aug 2025 15:19:52 +0100 Subject: [PATCH] BAEL-9368: Google Cloud and Spring AI --- spring-ai-4/pom.xml | 16 +++++++++- .../baeldung/springai/memory/Application.java | 6 +++- .../springai/vertexai/Application.java | 22 +++++++++++++ .../springai/vertexai/ChatController.java | 25 +++++++++++++++ .../springai/vertexai/ChatService.java | 29 +++++++++++++++++ .../MultiModalEmbeddingController.java | 30 +++++++++++++++++ .../vertexai/MultiModalEmbeddingService.java | 30 +++++++++++++++++ .../vertexai/TextEmbeddingController.java | 26 +++++++++++++++ .../vertexai/TextEmbeddingService.java | 24 ++++++++++++++ .../main/resources/application-vertexai.yml | 14 ++++++++ .../vertexai/ChatServiceLiveTest.java | 25 +++++++++++++++ .../MultiModalEmbeddingServiceLiveTest.java | 32 +++++++++++++++++++ .../TextEmbeddingServiceLiveTest.java | 27 ++++++++++++++++ 13 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 spring-ai-4/src/main/java/com/baeldung/springai/vertexai/Application.java create mode 100644 spring-ai-4/src/main/java/com/baeldung/springai/vertexai/ChatController.java create mode 100644 spring-ai-4/src/main/java/com/baeldung/springai/vertexai/ChatService.java create mode 100644 spring-ai-4/src/main/java/com/baeldung/springai/vertexai/MultiModalEmbeddingController.java create mode 100644 spring-ai-4/src/main/java/com/baeldung/springai/vertexai/MultiModalEmbeddingService.java create mode 100644 spring-ai-4/src/main/java/com/baeldung/springai/vertexai/TextEmbeddingController.java create mode 100644 spring-ai-4/src/main/java/com/baeldung/springai/vertexai/TextEmbeddingService.java create mode 100644 spring-ai-4/src/main/resources/application-vertexai.yml create mode 100644 spring-ai-4/src/test/java/com/baeldung/springai/vertexai/ChatServiceLiveTest.java create mode 100644 spring-ai-4/src/test/java/com/baeldung/springai/vertexai/MultiModalEmbeddingServiceLiveTest.java create mode 100644 spring-ai-4/src/test/java/com/baeldung/springai/vertexai/TextEmbeddingServiceLiveTest.java diff --git a/spring-ai-4/pom.xml b/spring-ai-4/pom.xml index ac2466af39f8..02e0aa659c99 100644 --- a/spring-ai-4/pom.xml +++ b/spring-ai-4/pom.xml @@ -61,6 +61,14 @@ org.springframework.ai spring-ai-starter-model-openai + + org.springframework.ai + spring-ai-starter-model-vertex-ai-gemini + + + org.springframework.ai + spring-ai-starter-model-vertex-ai-embedding + org.springframework.ai spring-ai-model-chat-memory-repository-jdbc @@ -89,6 +97,12 @@ com.baeldung.springai.memory.Application + + vertexai + + com.baeldung.springai.vertexai.Application + + @@ -113,7 +127,7 @@ 5.9.0 3.5.0 - 1.0.0 + 1.0.1 diff --git a/spring-ai-4/src/main/java/com/baeldung/springai/memory/Application.java b/spring-ai-4/src/main/java/com/baeldung/springai/memory/Application.java index 5cdaa360c6bc..ab9e2bbe6ecd 100644 --- a/spring-ai-4/src/main/java/com/baeldung/springai/memory/Application.java +++ b/spring-ai-4/src/main/java/com/baeldung/springai/memory/Application.java @@ -3,7 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication +@SpringBootApplication(exclude = { + org.springframework.ai.model.vertexai.autoconfigure.embedding.VertexAiMultiModalEmbeddingAutoConfiguration.class, + org.springframework.ai.model.vertexai.autoconfigure.embedding.VertexAiTextEmbeddingAutoConfiguration.class, + org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiChatAutoConfiguration.class, +}) public class Application { public static void main(String[] args) { diff --git a/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/Application.java b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/Application.java new file mode 100644 index 000000000000..4104be361c7f --- /dev/null +++ b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/Application.java @@ -0,0 +1,22 @@ +package com.baeldung.springai.vertexai; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(exclude = { + org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration.class, + org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration.class, + org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration.class, + org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration.class, + org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration.class, + org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration.class +}) +public class Application { + + public static void main(String[] args) { + SpringApplication app = new SpringApplication(Application.class); + app.setAdditionalProfiles("vertexai"); + app.run(args); + } + +} diff --git a/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/ChatController.java b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/ChatController.java new file mode 100644 index 000000000000..c60018bd56b0 --- /dev/null +++ b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/ChatController.java @@ -0,0 +1,25 @@ +package com.baeldung.springai.vertexai; + +import javax.validation.constraints.NotNull; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ChatController { + + private final ChatService chatService; + + public ChatController(ChatService chatService) { + this.chatService = chatService; + } + + @PostMapping("/chat") + public ResponseEntity chat(@RequestBody @NotNull String prompt) { + String response = chatService.chat(prompt); + return ResponseEntity.ok(response); + } + +} diff --git a/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/ChatService.java b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/ChatService.java new file mode 100644 index 000000000000..db695023ca0a --- /dev/null +++ b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/ChatService.java @@ -0,0 +1,29 @@ +package com.baeldung.springai.vertexai; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.SessionScope; + +@Component +@SessionScope +public class ChatService { + + private final ChatClient chatClient; + + public ChatService(ChatModel chatModel, ChatMemory chatMemory) { + this.chatClient = ChatClient.builder(chatModel) + .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) + .build(); + } + + public String chat(String prompt) { + return chatClient.prompt() + .user(userMessage -> userMessage.text(prompt)) + .call() + .content(); + } + +} diff --git a/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/MultiModalEmbeddingController.java b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/MultiModalEmbeddingController.java new file mode 100644 index 000000000000..8cc834bc71d5 --- /dev/null +++ b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/MultiModalEmbeddingController.java @@ -0,0 +1,30 @@ +package com.baeldung.springai.vertexai; + +import javax.validation.constraints.NotNull; + +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MimeType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +public class MultiModalEmbeddingController { + + private final MultiModalEmbeddingService embeddingService; + + public MultiModalEmbeddingController(MultiModalEmbeddingService embeddingService) { + this.embeddingService = embeddingService; + } + + @PostMapping("/embedding/image") + public ResponseEntity getEmbedding(@RequestParam("image") @NotNull MultipartFile imageFile) { + EmbeddingResponse response = embeddingService.getEmbedding( + MimeType.valueOf(imageFile.getContentType()), + imageFile.getResource()); + return ResponseEntity.ok(response); + } + +} \ No newline at end of file diff --git a/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/MultiModalEmbeddingService.java b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/MultiModalEmbeddingService.java new file mode 100644 index 000000000000..d44ee5864787 --- /dev/null +++ b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/MultiModalEmbeddingService.java @@ -0,0 +1,30 @@ +package com.baeldung.springai.vertexai; + +import java.util.List; +import java.util.Map; + +import org.springframework.ai.content.Media; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.DocumentEmbeddingModel; +import org.springframework.ai.embedding.DocumentEmbeddingRequest; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import org.springframework.util.MimeType; + +@Service +public class MultiModalEmbeddingService { + + private final DocumentEmbeddingModel documentEmbeddingModel; + + public MultiModalEmbeddingService(DocumentEmbeddingModel documentEmbeddingModel) { + this.documentEmbeddingModel = documentEmbeddingModel; + } + + public EmbeddingResponse getEmbedding(MimeType mimeType, Resource resource) { + Document document = new Document(new Media(mimeType, resource), Map.of()); + DocumentEmbeddingRequest request = new DocumentEmbeddingRequest(List.of(document)); + return documentEmbeddingModel.call(request); + } + +} diff --git a/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/TextEmbeddingController.java b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/TextEmbeddingController.java new file mode 100644 index 000000000000..6e45290457dd --- /dev/null +++ b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/TextEmbeddingController.java @@ -0,0 +1,26 @@ +package com.baeldung.springai.vertexai; + +import javax.validation.constraints.NotNull; + +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TextEmbeddingController { + + private final TextEmbeddingService textEmbeddingService; + + public TextEmbeddingController(TextEmbeddingService textEmbeddingService) { + this.textEmbeddingService = textEmbeddingService; + } + + @PostMapping("/embedding/text") + public ResponseEntity getEmbedding(@RequestBody @NotNull String text) { + EmbeddingResponse response = textEmbeddingService.getEmbedding(text); + return ResponseEntity.ok(response); + } + +} \ No newline at end of file diff --git a/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/TextEmbeddingService.java b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/TextEmbeddingService.java new file mode 100644 index 000000000000..3a351134b67e --- /dev/null +++ b/spring-ai-4/src/main/java/com/baeldung/springai/vertexai/TextEmbeddingService.java @@ -0,0 +1,24 @@ +package com.baeldung.springai.vertexai; + +import java.util.Arrays; + +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.embedding.EmbeddingRequest; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.stereotype.Service; + +@Service +public class TextEmbeddingService { + + private final EmbeddingModel embeddingModel; + + public TextEmbeddingService(EmbeddingModel embeddingModel) { + this.embeddingModel = embeddingModel; + } + + public EmbeddingResponse getEmbedding(String... texts) { + EmbeddingRequest request = new EmbeddingRequest(Arrays.asList(texts), null); + return embeddingModel.call(request); + } + +} diff --git a/spring-ai-4/src/main/resources/application-vertexai.yml b/spring-ai-4/src/main/resources/application-vertexai.yml new file mode 100644 index 000000000000..ae2fd5253d1e --- /dev/null +++ b/spring-ai-4/src/main/resources/application-vertexai.yml @@ -0,0 +1,14 @@ +spring: + ai: + vertex: + ai: + gemini: + project-id: "c1-lumion" + location: "europe-west1" + model: "gemini-2.0-flash-lite" + embedding: + project-id: "c1-lumion" + location: "europe-west1" + text: + options: + model: "gemini-embedding-001" diff --git a/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/ChatServiceLiveTest.java b/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/ChatServiceLiveTest.java new file mode 100644 index 000000000000..240efa3886f3 --- /dev/null +++ b/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/ChatServiceLiveTest.java @@ -0,0 +1,25 @@ +package com.baeldung.springai.vertexai; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("vertexai") +class ChatServiceLiveTest { + + private static final String PROMPT = "Tell me who you are?"; + + @Autowired + private ChatService chatService; + + @Test + void whenChatServiceIsCalled_thenServiceReturnsNonEmptyResponse() { + String response = chatService.chat(PROMPT); + assertThat(response).isNotEmpty(); + } + +} \ No newline at end of file diff --git a/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/MultiModalEmbeddingServiceLiveTest.java b/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/MultiModalEmbeddingServiceLiveTest.java new file mode 100644 index 000000000000..17bacc44c5ef --- /dev/null +++ b/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/MultiModalEmbeddingServiceLiveTest.java @@ -0,0 +1,32 @@ +package com.baeldung.springai.vertexai; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.util.MimeTypeUtils; + +@SpringBootTest +@ActiveProfiles("vertexai") +class MultiModalEmbeddingServiceLiveTest { + + private static final String IMAGE_PATH = "image/chiikawa.png"; + + @Autowired + private MultiModalEmbeddingService embeddingService; + + @Test + void whenGetEmbeddings_thenReturnEmbeddingResponse() { + Resource imageResource = new ClassPathResource(IMAGE_PATH); + EmbeddingResponse response = embeddingService.getEmbedding(MimeTypeUtils.IMAGE_PNG, imageResource); + assertThat(response).isNotNull(); + assertThat(response.getResults()).isNotNull(); + assertThat(response.getResults().isEmpty()).isFalse(); + } + +} \ No newline at end of file diff --git a/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/TextEmbeddingServiceLiveTest.java b/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/TextEmbeddingServiceLiveTest.java new file mode 100644 index 000000000000..2af34550adfd --- /dev/null +++ b/spring-ai-4/src/test/java/com/baeldung/springai/vertexai/TextEmbeddingServiceLiveTest.java @@ -0,0 +1,27 @@ +package com.baeldung.springai.vertexai; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("vertexai") +class TextEmbeddingServiceLiveTest { + + @Autowired + private TextEmbeddingService embeddingService; + + @Test + void whenGetEmbeddings_thenReturnEmbeddingResponse() { + String text = "This is a test string for embedding."; + EmbeddingResponse response = embeddingService.getEmbedding(text); + assertThat(response).isNotNull(); + assertThat(response.getResults()).isNotNull(); + assertThat(response.getResults().isEmpty()).isFalse(); + } + +} \ No newline at end of file