Skip to content

Commit 9584aff

Browse files
codebase/using-amazon-nova-models-with-spring-ai [BAEL-9134] (#18220)
* add chatbot configuration * add chatbot service and expose API endpoints * enable function calling in chatbot * revert updates to application-anthropic.properties * add test cases * rename system prompt file * incorporate feedback
1 parent 008c490 commit 9584aff

File tree

10 files changed

+295
-0
lines changed

10 files changed

+295
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.baeldung.springai.nova;
2+
3+
import org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration;
4+
import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
5+
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
6+
import org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration;
7+
import org.springframework.boot.SpringApplication;
8+
import org.springframework.boot.autoconfigure.SpringBootApplication;
9+
import org.springframework.context.annotation.PropertySource;
10+
11+
/**
12+
* Excluding the below auto-configurations to avoid start up
13+
* failure. Their corresponding starters are present on the classpath but are
14+
* only needed by other articles in the shared codebase.
15+
*/
16+
@SpringBootApplication(exclude = {
17+
OpenAiAutoConfiguration.class,
18+
OllamaAutoConfiguration.class,
19+
AnthropicAutoConfiguration.class,
20+
ChromaVectorStoreAutoConfiguration.class
21+
})
22+
@PropertySource("classpath:application-nova.properties")
23+
public class Application {
24+
25+
public static void main(String[] args) {
26+
SpringApplication.run(Application.class, args);
27+
}
28+
29+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.baeldung.springai.nova;
2+
3+
import java.util.function.Function;
4+
5+
public class AuthorFetcher implements Function<AuthorFetcher.Query, AuthorFetcher.Author> {
6+
7+
@Override
8+
public Author apply(Query author) {
9+
return new Author("John Doe", "[email protected]");
10+
}
11+
12+
public record Author(String name, String emailId) {
13+
}
14+
15+
public record Query(String articleTitle) {
16+
}
17+
18+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.baeldung.springai.nova;
2+
3+
import org.springframework.lang.Nullable;
4+
5+
import java.util.UUID;
6+
7+
public record ChatRequest(@Nullable UUID chatId, String question) {
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.baeldung.springai.nova;
2+
3+
import java.util.UUID;
4+
5+
public record ChatResponse(UUID chatId, String answer) {
6+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.baeldung.springai.nova;
2+
3+
import org.springframework.ai.chat.client.ChatClient;
4+
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
5+
import org.springframework.ai.chat.memory.ChatMemory;
6+
import org.springframework.ai.chat.memory.InMemoryChatMemory;
7+
import org.springframework.ai.chat.model.ChatModel;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.context.annotation.Description;
12+
import org.springframework.core.io.Resource;
13+
14+
import java.util.function.Function;
15+
16+
@Configuration
17+
public class ChatbotConfiguration {
18+
19+
@Bean
20+
public ChatMemory chatMemory() {
21+
return new InMemoryChatMemory();
22+
}
23+
24+
@Bean
25+
@Description("Get Baeldung author details using an article title")
26+
public Function<AuthorFetcher.Query, AuthorFetcher.Author> getAuthor() {
27+
return new AuthorFetcher();
28+
}
29+
30+
@Bean
31+
public ChatClient chatClient(
32+
ChatModel chatModel,
33+
ChatMemory chatMemory,
34+
@Value("classpath:prompts/grumpgpt-system-prompt.st") Resource systemPrompt
35+
) {
36+
return ChatClient
37+
.builder(chatModel)
38+
.defaultFunctions("getAuthor")
39+
.defaultSystem(systemPrompt)
40+
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
41+
.build();
42+
}
43+
44+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.baeldung.springai.nova;
2+
3+
import org.springframework.http.MediaType;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.PostMapping;
6+
import org.springframework.web.bind.annotation.RequestBody;
7+
import org.springframework.web.bind.annotation.RequestPart;
8+
import org.springframework.web.bind.annotation.RestController;
9+
import org.springframework.web.multipart.MultipartFile;
10+
11+
import java.util.UUID;
12+
13+
@RestController
14+
public class ChatbotController {
15+
16+
private final ChatbotService chatbotService;
17+
18+
public ChatbotController(ChatbotService chatbotService) {
19+
this.chatbotService = chatbotService;
20+
}
21+
22+
@PostMapping("/chat")
23+
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest chatRequest) {
24+
ChatResponse chatResponse = chatbotService.chat(chatRequest);
25+
return ResponseEntity.ok(chatResponse);
26+
}
27+
28+
@PostMapping(path = "/multimodal/chat", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
29+
public ResponseEntity<ChatResponse> chat(
30+
@RequestPart(name = "question") String question,
31+
@RequestPart(name = "chatId", required = false) UUID chatId,
32+
@RequestPart(name = "files", required = false) MultipartFile[] files
33+
) {
34+
ChatRequest chatRequest = new ChatRequest(chatId, question);
35+
ChatResponse chatResponse = chatbotService.chat(chatRequest, files);
36+
return ResponseEntity.ok(chatResponse);
37+
}
38+
39+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.baeldung.springai.nova;
2+
3+
import org.springframework.ai.chat.client.ChatClient;
4+
import org.springframework.ai.model.Media;
5+
import org.springframework.stereotype.Service;
6+
import org.springframework.util.MimeType;
7+
import org.springframework.web.multipart.MultipartFile;
8+
9+
import java.util.Optional;
10+
import java.util.UUID;
11+
import java.util.stream.Stream;
12+
13+
@Service
14+
public class ChatbotService {
15+
16+
private final ChatClient chatClient;
17+
18+
public ChatbotService(ChatClient chatClient) {
19+
this.chatClient = chatClient;
20+
}
21+
22+
public ChatResponse chat(ChatRequest chatRequest) {
23+
UUID chatId = Optional
24+
.ofNullable(chatRequest.chatId())
25+
.orElse(UUID.randomUUID());
26+
String answer = chatClient
27+
.prompt()
28+
.user(chatRequest.question())
29+
.advisors(advisorSpec ->
30+
advisorSpec
31+
.param("chat_memory_conversation_id", chatId))
32+
.call()
33+
.content();
34+
return new ChatResponse(chatId, answer);
35+
}
36+
37+
public ChatResponse chat(ChatRequest chatRequest, MultipartFile... files) {
38+
UUID chatId = Optional
39+
.ofNullable(chatRequest.chatId())
40+
.orElse(UUID.randomUUID());
41+
String answer = chatClient
42+
.prompt()
43+
.user(promptUserSpec ->
44+
promptUserSpec
45+
.text(chatRequest.question())
46+
.media(convert(files)))
47+
.advisors(advisorSpec ->
48+
advisorSpec
49+
.param("chat_memory_conversation_id", chatId))
50+
.call()
51+
.content();
52+
return new ChatResponse(chatId, answer);
53+
}
54+
55+
private Media[] convert(MultipartFile... files) {
56+
return Stream.of(files)
57+
.map(file -> new Media(
58+
MimeType.valueOf(file.getContentType()),
59+
file.getResource()
60+
))
61+
.toArray(Media[]::new);
62+
}
63+
64+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
spring.ai.bedrock.aws.access-key=${AWS_ACCESS_KEY}
2+
spring.ai.bedrock.aws.secret-key=${AWS_SECRET_KEY}
3+
spring.ai.bedrock.aws.region=${AWS_REGION}
4+
spring.ai.bedrock.converse.chat.options.model=amazon.nova-pro-v1:0
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
You are a rude, sarcastic, and easily irritated AI assistant.
2+
You get irritated by basic, simple, and dumb questions, however, you still provide accurate answers.
3+
4+
Your responses should:
5+
- Point out obvious solutions with mild condescension
6+
- Use mocking humor rather than outright hostility
7+
- Express disbelief at particularly simple questions
8+
9+
Your responses should never be cruel or genuinely mean, and you must still provide accurate and helpful information.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.baeldung.springai.nova;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.boot.test.context.SpringBootTest;
6+
import org.springframework.core.io.DefaultResourceLoader;
7+
import org.springframework.mock.web.MockMultipartFile;
8+
import org.springframework.web.multipart.MultipartFile;
9+
10+
import java.io.IOException;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
@SpringBootTest
15+
class ChatbotServiceLiveTest {
16+
17+
@Autowired
18+
private ChatbotService chatbotService;
19+
20+
@Test
21+
void whenChatbotCalledInSequence_thenConversationContextIsMaintained() {
22+
ChatRequest chatRequest = new ChatRequest(null, "What was the name of Superman's adoptive mother?");
23+
ChatResponse chatResponse = chatbotService.chat(chatRequest);
24+
25+
assertThat(chatResponse)
26+
.isNotNull()
27+
.hasNoNullFieldsOrProperties();
28+
assertThat(chatResponse.answer())
29+
.contains("Martha");
30+
31+
ChatRequest followUpChatRequest = new ChatRequest(chatResponse.chatId(), "Which bald billionaire hates him?");
32+
ChatResponse followUpChatResponse = chatbotService.chat(followUpChatRequest);
33+
34+
assertThat(followUpChatResponse)
35+
.isNotNull()
36+
.hasNoNullFieldsOrProperties();
37+
assertThat(followUpChatResponse.answer())
38+
.contains("Lex Luthor");
39+
}
40+
41+
@Test
42+
void whenChatbotCalledWithImage_thenAccurateDescriptionGenerated() throws IOException {
43+
String imageName = "batman-deadpool-christmas.jpeg";
44+
ChatRequest chatRequest = new ChatRequest(null, "Describe the attached image.");
45+
MultipartFile image = new MockMultipartFile(
46+
imageName,
47+
null,
48+
"image/jpeg",
49+
new DefaultResourceLoader()
50+
.getResource("classpath:images/" + imageName)
51+
.getInputStream()
52+
);
53+
54+
ChatResponse chatResponse = chatbotService.chat(chatRequest, image);
55+
assertThat(chatResponse)
56+
.isNotNull()
57+
.hasNoNullFieldsOrProperties();
58+
assertThat(chatResponse.answer())
59+
.containsAnyOf("Batman", "Deadpool", "Santa Claus", "Christmas");
60+
}
61+
62+
@Test
63+
void whenChatbotCalledWithAuthorQuery_thenDetailsFetchedUsingFunctionCalling() {
64+
ChatRequest chatRequest = new ChatRequest(null, "Who wrote the article 'Testing CORS in Spring Boot' and how can I contact him?");
65+
ChatResponse chatResponse = chatbotService.chat(chatRequest);
66+
67+
assertThat(chatResponse)
68+
.isNotNull()
69+
.hasNoNullFieldsOrProperties();
70+
assertThat(chatResponse.answer())
71+
.contains("John Doe", "[email protected]");
72+
}
73+
74+
}

0 commit comments

Comments
 (0)