From 5d774da850c5102d4c72548cf6a2da796ae832e6 Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Thu, 25 Jul 2024 06:58:30 +0200 Subject: [PATCH] Function calling sample with Ollama --- .../function-calling-ollama/README.md | 45 +++++++++++++++++++ .../function-calling-ollama/build.gradle | 39 ++++++++++++++++ .../thomasvitale/ai/spring/BookService.java | 31 +++++++++++++ .../ai/spring/ChatController.java | 21 +++++++++ .../thomasvitale/ai/spring/ChatService.java | 27 +++++++++++ .../FunctionCallingOllamaApplication.java | 13 ++++++ .../com/thomasvitale/ai/spring/Functions.java | 19 ++++++++ .../src/main/resources/application.yml | 7 +++ ...FunctionCallingOllamaApplicationTests.java | 13 ++++++ .../TestFunctionCallingOllamaApplication.java | 26 +++++++++++ README.md | 1 + settings.gradle | 1 + 12 files changed, 243 insertions(+) create mode 100644 07-function-calling/function-calling-ollama/README.md create mode 100644 07-function-calling/function-calling-ollama/build.gradle create mode 100644 07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/BookService.java create mode 100644 07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java create mode 100644 07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java create mode 100644 07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/FunctionCallingOllamaApplication.java create mode 100644 07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/Functions.java create mode 100644 07-function-calling/function-calling-ollama/src/main/resources/application.yml create mode 100644 07-function-calling/function-calling-ollama/src/test/java/com/thomasvitale/ai/spring/FunctionCallingOllamaApplicationTests.java create mode 100644 07-function-calling/function-calling-ollama/src/test/java/com/thomasvitale/ai/spring/TestFunctionCallingOllamaApplication.java diff --git a/07-function-calling/function-calling-ollama/README.md b/07-function-calling/function-calling-ollama/README.md new file mode 100644 index 0000000..9d37489 --- /dev/null +++ b/07-function-calling/function-calling-ollama/README.md @@ -0,0 +1,45 @@ +# Function Calling: Ollama + +Function calling 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 _mistral_ large language model. That's what we'll use in this example. + +```shell +ollama run mistral +``` + +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 _mistral_ model at startup time. + +```shell +./gradlew bootTestRun +``` + +## Calling the application + +You can now call the application that will use Ollama and _mistral_ to call functions in order to answer questions. +This example uses [httpie](https://httpie.io) to send HTTP requests. + +```shell +http :8080/chat/function +``` + +Try passing your custom prompt and check the result. + +```shell +http :8080/chat/function authorName=="Philip Pullman" +``` diff --git a/07-function-calling/function-calling-ollama/build.gradle b/07-function-calling/function-calling-ollama/build.gradle new file mode 100644 index 0000000..cca9189 --- /dev/null +++ b/07-function-calling/function-calling-ollama/build.gradle @@ -0,0 +1,39 @@ +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(22) + } +} + +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-testcontainers' + testImplementation 'org.springframework.ai:spring-ai-spring-boot-testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:ollama' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/BookService.java b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/BookService.java new file mode 100644 index 0000000..767b6fd --- /dev/null +++ b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/BookService.java @@ -0,0 +1,31 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class BookService { + + private static final Map books = new ConcurrentHashMap<>(); + + static { + books.put(1, new Book("His Dark Materials", "Philip Pullman")); + books.put(2, new Book("Narnia", "C.S. Lewis")); + books.put(3, new Book("The Hobbit", "J.R.R. Tolkien")); + books.put(4, new Book("The Lord of The Rings", "J.R.R. Tolkien")); + books.put(5, new Book("The Silmarillion", "J.R.R. Tolkien")); + } + + List getBooksByAuthor(Author author) { + return books.values().stream() + .filter(book -> author.name().equals(book.author())) + .toList(); + } + + public record Book(String title, String author) {} + public record Author(String name) {} + +} diff --git a/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java new file mode 100644 index 0000000..f74bcc6 --- /dev/null +++ b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java @@ -0,0 +1,21 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +class ChatController { + + private final ChatService chatService; + + ChatController(ChatService chatService) { + this.chatService = chatService; + } + + @GetMapping("/chat/function") + String chat(@RequestParam(defaultValue = "J.R.R. Tolkien") String authorName) { + return chatService.getAvailableBooksBy(authorName); + } + +} diff --git a/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java new file mode 100644 index 0000000..81c671e --- /dev/null +++ b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java @@ -0,0 +1,27 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Service; + +@Service +class ChatService { + + private final ChatClient chatClient; + + ChatService(ChatClient.Builder chatClientBuilder) { + this.chatClient = chatClientBuilder.build(); + } + + String getAvailableBooksBy(String authorName) { + var userPromptTemplate = "What books written by {author} are available to read?"; + return chatClient.prompt() + .user(userSpec -> userSpec + .text(userPromptTemplate) + .param("author", authorName) + ) + .functions("booksByAuthor") + .call() + .content(); + } + +} diff --git a/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/FunctionCallingOllamaApplication.java b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/FunctionCallingOllamaApplication.java new file mode 100644 index 0000000..2b282f6 --- /dev/null +++ b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/FunctionCallingOllamaApplication.java @@ -0,0 +1,13 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FunctionCallingOllamaApplication { + + public static void main(String[] args) { + SpringApplication.run(FunctionCallingOllamaApplication.class, args); + } + +} diff --git a/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/Functions.java b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/Functions.java new file mode 100644 index 0000000..d62b8a6 --- /dev/null +++ b/07-function-calling/function-calling-ollama/src/main/java/com/thomasvitale/ai/spring/Functions.java @@ -0,0 +1,19 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Description; + +import java.util.List; +import java.util.function.Function; + +@Configuration(proxyBeanMethods = false) +public class Functions { + + @Bean + @Description("Get the list of available books written by the given author") + public Function> booksByAuthor(BookService bookService) { + return bookService::getBooksByAuthor; + } + +} diff --git a/07-function-calling/function-calling-ollama/src/main/resources/application.yml b/07-function-calling/function-calling-ollama/src/main/resources/application.yml new file mode 100644 index 0000000..a05c157 --- /dev/null +++ b/07-function-calling/function-calling-ollama/src/main/resources/application.yml @@ -0,0 +1,7 @@ +spring: + ai: + ollama: + chat: + options: + model: mistral + temperature: 0.7 diff --git a/07-function-calling/function-calling-ollama/src/test/java/com/thomasvitale/ai/spring/FunctionCallingOllamaApplicationTests.java b/07-function-calling/function-calling-ollama/src/test/java/com/thomasvitale/ai/spring/FunctionCallingOllamaApplicationTests.java new file mode 100644 index 0000000..a39eaa0 --- /dev/null +++ b/07-function-calling/function-calling-ollama/src/test/java/com/thomasvitale/ai/spring/FunctionCallingOllamaApplicationTests.java @@ -0,0 +1,13 @@ +package com.thomasvitale.ai.spring; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class FunctionCallingOllamaApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/07-function-calling/function-calling-ollama/src/test/java/com/thomasvitale/ai/spring/TestFunctionCallingOllamaApplication.java b/07-function-calling/function-calling-ollama/src/test/java/com/thomasvitale/ai/spring/TestFunctionCallingOllamaApplication.java new file mode 100644 index 0000000..40327ec --- /dev/null +++ b/07-function-calling/function-calling-ollama/src/test/java/com/thomasvitale/ai/spring/TestFunctionCallingOllamaApplication.java @@ -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 TestFunctionCallingOllamaApplication { + + @Bean + @RestartScope + @ServiceConnection + OllamaContainer ollama() { + return new OllamaContainer(DockerImageName.parse("ghcr.io/thomasvitale/ollama-mistral") + .asCompatibleSubstituteFor("ollama/ollama")); + } + + public static void main(String[] args) { + SpringApplication.from(FunctionCallingOllamaApplication::main).with(TestFunctionCallingOllamaApplication.class).run(args); + } + +} diff --git a/README.md b/README.md index bad3435..4f0126e 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ _Coming soon_ | Project | Description | |--------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------| | [function-calling-mistral-ai](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/07-function-calling/function-calling-mistral-ai) | Function calling with LLMs via Mistral AI. | +| [function-calling-ollama](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/07-function-calling/function-calling-ollama) | Function calling with LLMs via Ollama. | | [function-calling-openai](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/07-function-calling/function-calling-openai) | Function calling with LLMs via OpenAI. | ### 8. Image Models diff --git a/settings.gradle b/settings.gradle index 1d753bb..a368654 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,6 +39,7 @@ include '05-etl-pipeline:document-transformers-metadata-ollama' include '05-etl-pipeline:document-transformers-splitters-ollama' include '07-function-calling:function-calling-mistral-ai' +include '07-function-calling:function-calling-ollama' include '07-function-calling:function-calling-openai' include '08-image-models:image-models-openai'