Skip to content

Commit

Permalink
Update to Spring Boot 3.3 and early chatbot example
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasVitale committed May 23, 2024
1 parent 775652a commit ddc6213
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 8 deletions.
43 changes: 43 additions & 0 deletions 00-use-cases/chatbot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Chatbot

Chat with LLMs via Ollama.

## Running the application

The application relies on Ollama for providing LLMs. You can either run Ollama locally on your laptop, or rely on the Testcontainers support in Spring Boot to spin up an Ollama service automatically.

### Ollama as a native application

First, make sure you have [Ollama](https://ollama.ai) installed on your laptop.
Then, use Ollama to run the _llama3_ large language model.

```shell
ollama run llama3
```

Finally, run the Spring Boot application.

```shell
./gradlew bootRun
```

### Ollama as a dev service with Testcontainers

The application relies on the native Testcontainers support in Spring Boot to spin up an Ollama service with a _llama3_ model at startup time.

```shell
./gradlew bootTestRun
```

## Calling the application

You can now call the application that will use Ollama and llama3 to answer your questions.
This example uses [httpie](https://httpie.io) to send HTTP requests.

```shell
http --raw "Where did Saruman got the wood to build a weapon factory?" :8080/chat
```

```shell
http --raw "Can a wolf destroy a house?" :8080/chat
```
40 changes: 40 additions & 0 deletions 00-use-cases/chatbot/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
plugins {
id 'java'
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}

group = 'com.thomasvitale'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
implementation platform("org.springframework.ai:spring-ai-bom:${springAiVersion}")

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'

testAndDevelopmentOnly 'org.springframework.boot:spring-boot-devtools'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.springframework.ai:spring-ai-spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:ollama'
}

tasks.named('test') {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.thomasvitale.ai.spring;

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Chatbot {

public static void main(String[] args) {
SpringApplication.run(Chatbot.class, args);
}

@Bean
ChatMemory chatHistory() {
return new InMemoryChatMemory();
}

@Bean
TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.thomasvitale.ai.spring;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ChatbotController {

private final ChatbotService chatbotService;

ChatbotController(ChatbotService chatbotService) {
this.chatbotService = chatbotService;
}

@PostMapping("/chat")
String chat(@RequestBody String input) {
return chatbotService.chat(input);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.thomasvitale.ai.spring;

import org.springframework.ai.chat.ChatModel;
import org.springframework.ai.chat.memory.*;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.service.ChatService;
import org.springframework.ai.chat.service.PromptTransformingChatService;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
class ChatbotService {

private final ChatService chatService;

ChatbotService(ChatModel chatModel, ChatMemory chatMemory, TokenCountEstimator tokenCountEstimator) {
this.chatService = PromptTransformingChatService.builder(chatModel)
.withRetrievers(List.of(new ChatMemoryRetriever(chatMemory)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatMemory)))
.build();
}

String chat(String message) {
var prompt = new Prompt(new UserMessage(message));
var chatServiceResponse = this.chatService.call(new ChatServiceContext(prompt));
return chatServiceResponse.getChatResponse().getResult().getOutput().getContent();
}

}
9 changes: 9 additions & 0 deletions 00-use-cases/chatbot/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
spring:
ai:
ollama:
chat:
options:
model: llama3
threads:
virtual:
enabled: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.thomasvitale.ai.spring;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ChatbotTests {

@Test
void contextLoads() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.thomasvitale.ai.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.ollama.OllamaContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
public class TestChatbot {

@Bean
@RestartScope
@ServiceConnection
OllamaContainer ollama() {
return new OllamaContainer(DockerImageName.parse("ghcr.io/thomasvitale/ollama-llama3")
.asCompatibleSubstituteFor("ollama/ollama"));
}

public static void main(String[] args) {
SpringApplication.from(Chatbot::main).with(TestChatbot.class).run(args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.Media;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.MimeTypeUtils;

import java.io.IOException;
import java.util.List;
import java.net.MalformedURLException;
import java.net.URI;

@Service
class ChatService {
Expand All @@ -33,12 +33,14 @@ String chatFromImageFile(String message) throws IOException {
.content();
}

String chatFromImageUrl(String message) {
String chatFromImageUrl(String message) throws MalformedURLException {
var imageUrl = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";
var userMessage = new UserMessage(message,
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageUrl)));
var url = URI.create(imageUrl).toURL();
return chatClient.prompt()
.messages(userMessage)
.user(userSpec -> userSpec
.text(message)
.media(new Media(MimeTypeUtils.IMAGE_PNG, url))
)
.call()
.content();
}
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ repositories {
}

ext {
set("springBootVersion", '3.2.5')
set("dependencyManagementVersion", '1.1.4')
set("springBootVersion", '3.3.0')
set("dependencyManagementVersion", '1.1.5')
}

dependencies {
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {

rootProject.name = 'llm-apps-java-spring-ai'

include '00-use-cases:chatbot'
include '00-use-cases:question-answering-with-documents'

include '01-chat-models:chat-models-mistral-ai'
Expand Down

0 comments on commit ddc6213

Please sign in to comment.