From 2347203fa9db9de5df1e811c1ea3cba0ff7cbacf Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Fri, 26 Jan 2024 19:50:34 +0100 Subject: [PATCH] Document readers samples for RAG --- .../document-readers-json-ollama/README.md | 16 ++++- .../thomasvitale/ai/spring/ChatService.java | 7 +-- .../document-readers-pdf-ollama/README.md | 43 +++++++++++++ .../document-readers-pdf-ollama/build.gradle | 35 +++++++++++ .../ai/spring/ChatController.java | 21 +++++++ .../thomasvitale/ai/spring/ChatService.java | 51 ++++++++++++++++ .../ai/spring/DocumentInitializer.java | 57 ++++++++++++++++++ .../DocumentReadersPdfOllamaApplication.java | 21 +++++++ .../src/main/resources/application.yml | 10 +++ .../src/main/resources/documents/story1.pdf | Bin 0 -> 22071 bytes .../src/main/resources/documents/story2.pdf | Bin 0 -> 21782 bytes ...umentReadersPdfOllamaApplicationTests.java | 17 ++++++ ...stDocumentReadersPdfOllamaApplication.java | 29 +++++++++ .../document-readers-text-ollama/README.md | 16 ++++- .../thomasvitale/ai/spring/ChatService.java | 20 +++--- README.md | 1 + settings.gradle | 1 + 17 files changed, 328 insertions(+), 17 deletions(-) create mode 100644 05-document-readers/document-readers-pdf-ollama/README.md create mode 100644 05-document-readers/document-readers-pdf-ollama/build.gradle create mode 100644 05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java create mode 100644 05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java create mode 100644 05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java create mode 100644 05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentReadersPdfOllamaApplication.java create mode 100644 05-document-readers/document-readers-pdf-ollama/src/main/resources/application.yml create mode 100644 05-document-readers/document-readers-pdf-ollama/src/main/resources/documents/story1.pdf create mode 100644 05-document-readers/document-readers-pdf-ollama/src/main/resources/documents/story2.pdf create mode 100644 05-document-readers/document-readers-pdf-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentReadersPdfOllamaApplicationTests.java create mode 100644 05-document-readers/document-readers-pdf-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentReadersPdfOllamaApplication.java diff --git a/05-document-readers/document-readers-json-ollama/README.md b/05-document-readers/document-readers-json-ollama/README.md index 82204a1..eb271cf 100644 --- a/05-document-readers/document-readers-json-ollama/README.md +++ b/05-document-readers/document-readers-json-ollama/README.md @@ -1,25 +1,39 @@ # JSON Document Readers: Ollama -## Running the application +Reading and vectorizing JSON documents with LLMs via Ollama. + +# Running the application + +The application relies on Ollama for providing LLMs. You can either run Ollama locally on your laptop (macOS or Linux), or rely on the Testcontainers support in Spring Boot to spin up an Ollama service automatically. ### When using Ollama +First, make sure you have [Ollama](https://ollama.ai) installed on your laptop (macOS or Linux). +Then, use Ollama to run the _llama2_ large language model. + ```shell ollama run llama2 ``` +Finally, run the Spring Boot application. + ```shell ./gradlew bootRun ``` ### When using Docker/Podman +The application relies on the native Testcontainers support in Spring Boot to spin up an Ollama service with a _llama2_ model at startup time. + ```shell ./gradlew bootTestRun ``` ## Calling the application +You can now call the application that will use Ollama and llama2 to load JSON documents as embeddings and generate an answer to your questions based on those documents (RAG pattern). +This example uses [httpie](https://httpie.io) to send HTTP requests. + ```shell http --raw "What bike is good for city commuting?" :8080/ai/doc/chat ``` diff --git a/05-document-readers/document-readers-json-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java b/05-document-readers/document-readers-json-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java index a86b2b3..4dde576 100644 --- a/05-document-readers/document-readers-json-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java +++ b/05-document-readers/document-readers-json-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java @@ -28,10 +28,9 @@ class ChatService { AssistantMessage chatWithDocument(String message) { var systemPromptTemplate = new SystemPromptTemplate(""" You're assisting with questions about products in a bicycle catalog. - Use the information from the DOCUMENTS section to provide accurate answers. - If the answer involves referring to the price or the dimension of the bicycle, - include the bicycle name in the response. - If unsure, simply state that you don't know. + Use the information from the DOCUMENTS section and no prior knowledge. + If unsure or if the answer isn't found in the DOCUMENTS section, simply state + that you don't know the answer. DOCUMENTS: {documents} diff --git a/05-document-readers/document-readers-pdf-ollama/README.md b/05-document-readers/document-readers-pdf-ollama/README.md new file mode 100644 index 0000000..53fed4c --- /dev/null +++ b/05-document-readers/document-readers-pdf-ollama/README.md @@ -0,0 +1,43 @@ +# PDF Document Readers: Ollama + +Reading and vectorizing PDF documents with LLMs via Ollama. + +# Running the application + +The application relies on Ollama for providing LLMs. You can either run Ollama locally on your laptop (macOS or Linux), or rely on the Testcontainers support in Spring Boot to spin up an Ollama service automatically. + +### When using Ollama + +First, make sure you have [Ollama](https://ollama.ai) installed on your laptop (macOS or Linux). +Then, use Ollama to run the _llama2_ large language model. + +```shell +ollama run llama2 +``` + +Finally, run the Spring Boot application. + +```shell +./gradlew bootRun +``` + +### When using Docker/Podman + +The application relies on the native Testcontainers support in Spring Boot to spin up an Ollama service with a _llama2_ model at startup time. + +```shell +./gradlew bootTestRun +``` + +## Calling the application + +You can now call the application that will use Ollama and llama2 to load PDF documents as embeddings and generate an answer to your questions based on those documents (RAG pattern). +This example uses [httpie](https://httpie.io) to send HTTP requests. + +```shell +http --raw "What is Iorek's biggest dream?" :8080/ai/doc/chat +``` + +```shell +http --raw "Who is Lucio?" :8080/ai/doc/chat +``` diff --git a/05-document-readers/document-readers-pdf-ollama/build.gradle b/05-document-readers/document-readers-pdf-ollama/build.gradle new file mode 100644 index 0000000..f2c529e --- /dev/null +++ b/05-document-readers/document-readers-pdf-ollama/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'org.springframework.boot' + id 'io.spring.dependency-management' +} + +group = 'com.thomasvitale' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '21' +} + +repositories { + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + + implementation "org.springframework.ai:spring-ai-ollama-spring-boot-starter:${springAiVersion}" + implementation "org.springframework.ai:spring-ai-pdf-document-reader:${springAiVersion}" + + testAndDevelopmentOnly 'org.springframework.boot:spring-boot-devtools' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java b/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java new file mode 100644 index 0000000..6bc35c3 --- /dev/null +++ b/05-document-readers/document-readers-pdf-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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +class ChatController { + + private final ChatService chatService; + + ChatController(ChatService chatService) { + this.chatService = chatService; + } + + @PostMapping("/ai/doc/chat") + String chatWithDocument(@RequestBody String input) { + return chatService.chatWithDocument(input).getContent(); + } + +} diff --git a/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java b/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java new file mode 100644 index 0000000..6420e4b --- /dev/null +++ b/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java @@ -0,0 +1,51 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.ai.chat.ChatClient; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.SystemPromptTemplate; +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +class ChatService { + + private final ChatClient chatClient; + private final SimpleVectorStore vectorStore; + + ChatService(ChatClient chatClient, SimpleVectorStore vectorStore) { + this.chatClient = chatClient; + this.vectorStore = vectorStore; + } + + AssistantMessage chatWithDocument(String message) { + var systemPromptTemplate = new SystemPromptTemplate(""" + Answer questions given the context information below (DOCUMENTS section) and no prior knowledge. + If the answer is not found in the DOCUMENTS section, simply state that you don't know the answer. + In the answer, include the source file name from which the context information is extracted from. + + DOCUMENTS: + {documents} + """); + + List similarDocuments = vectorStore.similaritySearch(SearchRequest.query(message).withTopK(2)); + String documents = similarDocuments.stream().map(Document::getContent).collect(Collectors.joining(System.lineSeparator())); + + Map model = Map.of("documents", documents); + var systemMessage = systemPromptTemplate.createMessage(model); + + var userMessage = new UserMessage(message); + var prompt = new Prompt(List.of(systemMessage, userMessage)); + + var chatResponse = chatClient.call(prompt); + return chatResponse.getResult().getOutput(); + } + +} diff --git a/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java b/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java new file mode 100644 index 0000000..c948744 --- /dev/null +++ b/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java @@ -0,0 +1,57 @@ +package com.thomasvitale.ai.spring; + +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.ExtractedTextFormatter; +import org.springframework.ai.reader.pdf.PagePdfDocumentReader; +import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class DocumentInitializer { + + private static final Logger log = LoggerFactory.getLogger(DocumentInitializer.class); + private final SimpleVectorStore simpleVectorStore; + + @Value("classpath:documents/story1.pdf") + Resource pdfFile1; + + @Value("classpath:documents/story2.pdf") + Resource pdfFile2; + + public DocumentInitializer(SimpleVectorStore simpleVectorStore) { + this.simpleVectorStore = simpleVectorStore; + } + + @PostConstruct + public void run() { + List documents = new ArrayList<>(); + + log.info("Loading PDF files as Documents"); + var pdfReader1 = new PagePdfDocumentReader(pdfFile1); + documents.addAll(pdfReader1.get()); + + log.info("Loading PDF files as Documents after reformatting"); + var pdfReader2 = new PagePdfDocumentReader(pdfFile2, PdfDocumentReaderConfig.builder() + .withPageExtractedTextFormatter(ExtractedTextFormatter.builder() + .withNumberOfTopPagesToSkipBeforeDelete(0) + .withNumberOfBottomTextLinesToDelete(1) + .withNumberOfTopTextLinesToDelete(1) + .build()) + .withPagesPerDocument(1) + .build()); + documents.addAll(pdfReader2.get()); + + log.info("Creating and storing Embeddings from Documents"); + simpleVectorStore.add(documents); + } + +} diff --git a/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentReadersPdfOllamaApplication.java b/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentReadersPdfOllamaApplication.java new file mode 100644 index 0000000..9673031 --- /dev/null +++ b/05-document-readers/document-readers-pdf-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentReadersPdfOllamaApplication.java @@ -0,0 +1,21 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class DocumentReadersPdfOllamaApplication { + + @Bean + SimpleVectorStore documentWriter(EmbeddingClient embeddingClient) { + return new SimpleVectorStore(embeddingClient); + } + + public static void main(String[] args) { + SpringApplication.run(DocumentReadersPdfOllamaApplication.class, args); + } + +} diff --git a/05-document-readers/document-readers-pdf-ollama/src/main/resources/application.yml b/05-document-readers/document-readers-pdf-ollama/src/main/resources/application.yml new file mode 100644 index 0000000..318872b --- /dev/null +++ b/05-document-readers/document-readers-pdf-ollama/src/main/resources/application.yml @@ -0,0 +1,10 @@ +spring: + ai: + ollama: + chat: + model: llama2 + embedding: + model: llama2 + threads: + virtual: + enabled: true diff --git a/05-document-readers/document-readers-pdf-ollama/src/main/resources/documents/story1.pdf b/05-document-readers/document-readers-pdf-ollama/src/main/resources/documents/story1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7445313052dd4ba8135ae284c50176570958dfa2 GIT binary patch literal 22071 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%0D|Y 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 S=T;*FVCLgeRdw}u;{pKA#Jt)7 literal 0 HcmV?d00001 diff --git a/05-document-readers/document-readers-pdf-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentReadersPdfOllamaApplicationTests.java b/05-document-readers/document-readers-pdf-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentReadersPdfOllamaApplicationTests.java new file mode 100644 index 0000000..ff32149 --- /dev/null +++ b/05-document-readers/document-readers-pdf-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentReadersPdfOllamaApplicationTests.java @@ -0,0 +1,17 @@ +package com.thomasvitale.ai.spring; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +@SpringBootTest +@Import(TestDocumentReadersPdfOllamaApplication.class) +@Disabled // Only run locally for now +class DocumentReadersPdfOllamaApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/05-document-readers/document-readers-pdf-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentReadersPdfOllamaApplication.java b/05-document-readers/document-readers-pdf-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentReadersPdfOllamaApplication.java new file mode 100644 index 0000000..5af9d6f --- /dev/null +++ b/05-document-readers/document-readers-pdf-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentReadersPdfOllamaApplication.java @@ -0,0 +1,29 @@ +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.context.annotation.Bean; +import org.springframework.context.annotation.Scope; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.testcontainers.containers.GenericContainer; + +@TestConfiguration(proxyBeanMethods = false) +public class TestDocumentReadersPdfOllamaApplication { + + @Bean + @RestartScope + @Scope("singleton") // needed because of https://github.com/spring-projects/spring-boot/issues/35786 + GenericContainer ollama(DynamicPropertyRegistry properties) { + var ollama = new GenericContainer<>("ghcr.io/thomasvitale/ollama-llama2") + .withExposedPorts(11434); + properties.add("spring.ai.ollama.base-url", + () -> "http://%s:%s".formatted(ollama.getHost(), ollama.getMappedPort(11434))); + return ollama; + } + + public static void main(String[] args) { + SpringApplication.from(DocumentReadersPdfOllamaApplication::main).with(TestDocumentReadersPdfOllamaApplication.class).run(args); + } + +} diff --git a/05-document-readers/document-readers-text-ollama/README.md b/05-document-readers/document-readers-text-ollama/README.md index 790432d..dfc2943 100644 --- a/05-document-readers/document-readers-text-ollama/README.md +++ b/05-document-readers/document-readers-text-ollama/README.md @@ -1,25 +1,39 @@ # Text Document Readers: Ollama -## Running the application +Reading and vectorizing text documents with LLMs via Ollama. + +# Running the application + +The application relies on Ollama for providing LLMs. You can either run Ollama locally on your laptop (macOS or Linux), or rely on the Testcontainers support in Spring Boot to spin up an Ollama service automatically. ### When using Ollama +First, make sure you have [Ollama](https://ollama.ai) installed on your laptop (macOS or Linux). +Then, use Ollama to run the _llama2_ large language model. + ```shell ollama run llama2 ``` +Finally, run the Spring Boot application. + ```shell ./gradlew bootRun ``` ### When using Docker/Podman +The application relies on the native Testcontainers support in Spring Boot to spin up an Ollama service with a _llama2_ model at startup time. + ```shell ./gradlew bootTestRun ``` ## Calling the application +You can now call the application that will use Ollama and llama2 to load text documents as embeddings and generate an answer to your questions based on those documents (RAG pattern). +This example uses [httpie](https://httpie.io) to send HTTP requests. + ```shell http --raw "What is Iorek's biggest dream?" :8080/ai/doc/chat ``` diff --git a/05-document-readers/document-readers-text-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java b/05-document-readers/document-readers-text-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java index ae54894..6420e4b 100644 --- a/05-document-readers/document-readers-text-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java +++ b/05-document-readers/document-readers-text-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java @@ -2,8 +2,9 @@ import org.springframework.ai.chat.ChatClient; import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.chat.prompt.PromptTemplate; +import org.springframework.ai.chat.prompt.SystemPromptTemplate; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.SimpleVectorStore; @@ -25,26 +26,23 @@ class ChatService { } AssistantMessage chatWithDocument(String message) { - var promptTemplate = new PromptTemplate(""" - Answer questions given the information below (DOCUMENTS section) and no prior knowledge. + var systemPromptTemplate = new SystemPromptTemplate(""" + Answer questions given the context information below (DOCUMENTS section) and no prior knowledge. + If the answer is not found in the DOCUMENTS section, simply state that you don't know the answer. In the answer, include the source file name from which the context information is extracted from. DOCUMENTS: {documents} - - Given the context information and no prior knowledge, answer the question (QUESTION section). - - QUESTION: - {question} """); List similarDocuments = vectorStore.similaritySearch(SearchRequest.query(message).withTopK(2)); String documents = similarDocuments.stream().map(Document::getContent).collect(Collectors.joining(System.lineSeparator())); - Map model = Map.of("documents", documents, "question", message); - var userMessage = promptTemplate.createMessage(model); + Map model = Map.of("documents", documents); + var systemMessage = systemPromptTemplate.createMessage(model); - var prompt = new Prompt(userMessage); + var userMessage = new UserMessage(message); + var prompt = new Prompt(List.of(systemMessage, userMessage)); var chatResponse = chatClient.call(prompt); return chatResponse.getResult().getOutput(); diff --git a/README.md b/README.md index 88bcebf..1f3e8df 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Samples showing how to build Java applications powered by Generative AI and LLMs | Project | Description | |----------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| | [document-readers-json-ollama](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/05-document-readers/document-readers-json-ollama) | Reading and vectorizing JSON documents with LLMs via Ollama. | +| [document-readers-pdf-ollama](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/05-document-readers/document-readers-text-ollama) | Reading and vectorizing PDF documents with LLMs via Ollama. | | [document-readers-text-ollama](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/05-document-readers/document-readers-text-ollama) | Reading and vectorizing text documents with LLMs via Ollama. | ### 6. Document Transformers diff --git a/settings.gradle b/settings.gradle index c2afae7..34b8ac9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,4 +17,5 @@ include '04-embedding-models:embedding-models-ollama' include '04-embedding-models:embedding-models-openai' include '05-document-readers:document-readers-json-ollama' +include '05-document-readers:document-readers-pdf-ollama' include '05-document-readers:document-readers-text-ollama'