diff --git a/01-chat-models/chat-models-openai/build.gradle b/01-chat-models/chat-models-openai/build.gradle index 4167e6f..47ece33 100644 --- a/01-chat-models/chat-models-openai/build.gradle +++ b/01-chat-models/chat-models-openai/build.gradle @@ -15,7 +15,6 @@ java { repositories { mavenCentral() - maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } } diff --git a/06-document-transformers/document-transformers-metadata-ollama/README.md b/06-document-transformers/document-transformers-metadata-ollama/README.md new file mode 100644 index 0000000..4f93ea3 --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/README.md @@ -0,0 +1,43 @@ +# Document Transformers - Metadata + +Enrich documents with keywords and summary metadata for enhanced retrieval. + +## 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. + +### Ollama as a native application + +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 +``` + +### 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 _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 +``` + +```shell +http --raw "Who is Lucio?" :8080/ai/doc/chat +``` diff --git a/06-document-transformers/document-transformers-metadata-ollama/build.gradle b/06-document-transformers/document-transformers-metadata-ollama/build.gradle new file mode 100644 index 0000000..124f83c --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/build.gradle @@ -0,0 +1,36 @@ +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 'org.springframework.boot:spring-boot-starter-web' + + implementation "org.springframework.ai:spring-ai-ollama-spring-boot-starter:${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/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java b/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java new file mode 100644 index 0000000..6bc35c3 --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-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/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java b/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java new file mode 100644 index 0000000..42f9f69 --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java @@ -0,0 +1,52 @@ +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, + but act as if you knew this information innately. 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/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java b/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java new file mode 100644 index 0000000..3aa28d2 --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java @@ -0,0 +1,66 @@ +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.TextReader; +import org.springframework.ai.transformer.KeywordMetadataEnricher; +import org.springframework.ai.transformer.SummaryMetadataEnricher; +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.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +@Component +public class DocumentInitializer { + + private static final Logger log = LoggerFactory.getLogger(DocumentInitializer.class); + + private final KeywordMetadataEnricher keywordMetadataEnricher; + private final SummaryMetadataEnricher summaryMetadataEnricher; + private final SimpleVectorStore vectorStore; + + @Value("classpath:documents/story1.md") + Resource textFile1; + + @Value("classpath:documents/story2.txt") + Resource textFile2; + + public DocumentInitializer(KeywordMetadataEnricher keywordMetadataEnricher, SummaryMetadataEnricher summaryMetadataEnricher, SimpleVectorStore vectorStore) { + this.keywordMetadataEnricher = keywordMetadataEnricher; + this.summaryMetadataEnricher = summaryMetadataEnricher; + this.vectorStore = vectorStore; + } + + @PostConstruct + public void run() { + List documents = new ArrayList<>(); + + log.info("Loading .md files as Documents"); + var textReader1 = new TextReader(textFile1); + textReader1.getCustomMetadata().put("location", "North Pole"); + textReader1.setCharset(Charset.defaultCharset()); + documents.addAll(textReader1.get()); + + log.info("Loading .txt files as Documents"); + var textReader2 = new TextReader(textFile2); + textReader2.getCustomMetadata().put("location", "Italy"); + textReader2.setCharset(Charset.defaultCharset()); + documents.addAll(textReader2.get()); + + log.info("Enrich Documents with generated keywords as metadata"); + var transformedDocumentsWithKeywords = keywordMetadataEnricher.apply(documents); + + log.info("Enrich Documents with generated summary as metadata"); + var transformedDocumentsWithKeywordsAndSummary = summaryMetadataEnricher.apply(transformedDocumentsWithKeywords); + + log.info("Creating and storing Embeddings from Documents"); + vectorStore.add(transformedDocumentsWithKeywordsAndSummary); + } + +} diff --git a/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplication.java b/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplication.java new file mode 100644 index 0000000..6212c8d --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplication.java @@ -0,0 +1,48 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.ai.chat.ChatClient; +import org.springframework.ai.document.DefaultContentFormatter; +import org.springframework.ai.embedding.EmbeddingClient; +import org.springframework.ai.transformer.KeywordMetadataEnricher; +import org.springframework.ai.transformer.SummaryMetadataEnricher; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +@SpringBootApplication +public class DocumentTransformersMetadataOllamaApplication { + + @Bean + DefaultContentFormatter defaultContentFormatter() { + return DefaultContentFormatter.builder() + .withExcludedEmbedMetadataKeys("NewEmbedKey") + .withExcludedInferenceMetadataKeys("NewInferenceKey") + .build(); + } + + @Bean + KeywordMetadataEnricher keywordMetadataEnricher(ChatClient chatClient) { + return new KeywordMetadataEnricher(chatClient, 3); + } + + @Bean + SummaryMetadataEnricher summaryMetadataEnricher(ChatClient chatClient) { + return new SummaryMetadataEnricher(chatClient, List.of( + SummaryMetadataEnricher.SummaryType.PREVIOUS, + SummaryMetadataEnricher.SummaryType.CURRENT, + SummaryMetadataEnricher.SummaryType.NEXT)); + } + + @Bean + SimpleVectorStore documentWriter(EmbeddingClient embeddingClient) { + return new SimpleVectorStore(embeddingClient); + } + + public static void main(String[] args) { + SpringApplication.run(DocumentTransformersMetadataOllamaApplication.class, args); + } + +} diff --git a/06-document-transformers/document-transformers-metadata-ollama/src/main/resources/application.yml b/06-document-transformers/document-transformers-metadata-ollama/src/main/resources/application.yml new file mode 100644 index 0000000..318872b --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-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/06-document-transformers/document-transformers-metadata-ollama/src/main/resources/documents/story1.md b/06-document-transformers/document-transformers-metadata-ollama/src/main/resources/documents/story1.md new file mode 100644 index 0000000..e9174fd --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/src/main/resources/documents/story1.md @@ -0,0 +1,42 @@ +# The Adventures of Iorek and Pingu + +Iorek was a little polar bear who lived in the Arctic circle. He loved to explore the snowy landscape and +dreamt of one day going on an adventure around the North Pole. One day, he met a penguin named Pingu who +was on a similar quest. They quickly became friends and decided to embark on their journey together. + +Iorek and Pingu set off early in the morning, eager to cover as much ground as possible before nightfall. +The air was crisp and cold, and the snow crunched under their paws as they walked. They chatted excitedly +about their dreams and aspirations, and Iorek told Pingu about his desire to see the Northern Lights. + +As they journeyed onward, they encountered a group of playful seals who were sliding and jumping in the +snow. Iorek and Pingu watched in delight as the seals frolicked and splashed in the water. They even tried +to join in, but their paws kept slipping and they ended up sliding on their stomachs instead. + +After a few hours of walking, Iorek and Pingu came across a cave hidden behind a wall of snow. They +cautiously entered the darkness, their eyes adjusting to the dim light inside. The cave was filled with +glittering ice formations that sparkled like diamonds in the flickering torchlight. + +As they continued their journey, Iorek and Pingu encountered a group of walruses who were lounging on the +ice. They watched in amazement as the walruses lazily rolled over and exposed their tusks for a good +scratch. Pingu even tried to imitate them, but ended up looking more like a clumsy seal than a walrus. + +As the sun began to set, Iorek and Pingu found themselves at the edge of a vast, frozen lake. They gazed +out across the glassy surface, mesmerized by the way the ice glinted in the fading light. They could see +the faint outline of a creature moving beneath the surface, and their hearts raced with excitement. + +Suddenly, a massive narwhal burst through the ice and into the air, its ivory tusk glistening in the +sunset. Iorek and Pingu watched in awe as it soared overhead, its cries echoing across the lake. They felt +as though they were witnessing a magical moment, one that would stay with them forever. + +As the night drew in, Iorek and Pingu settled down to rest in their makeshift camp. They huddled together +for warmth, gazing up at the starry sky above. They chatted about all they had seen and experienced during +their adventure, and Iorek couldn't help but feel grateful for the new friend he had made. + +The next morning, Iorek and Pingu set off once again, determined to explore every inch of the North Pole. +They stumbled upon a hidden cave filled with glittering crystals that sparkled like diamonds in the +sunlight. They marveled at their beauty before continuing on their way. + +As they journeyed onward, Iorek and Pingu encountered many more wonders and adventures. They met a group +of playful reindeer who showed them how to pull sledges across the snow, and even caught a glimpse of the +mythical Loch Ness Monster lurking beneath the icy waters. In the end, their adventure around the North +Pole had been an unforgettable experience, one that they would treasure forever. diff --git a/06-document-transformers/document-transformers-metadata-ollama/src/main/resources/documents/story2.txt b/06-document-transformers/document-transformers-metadata-ollama/src/main/resources/documents/story2.txt new file mode 100644 index 0000000..f5b9322 --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/src/main/resources/documents/story2.txt @@ -0,0 +1,45 @@ +Lucio and Bolosso explore the Alps + +Lucio was a little wolf who lived in the Italian Alps. He loved to explore the rugged landscape and dreamt +of one day going on an adventure around the mountains. One day, he met a brown bear named Balosso who was +on a similar quest. They quickly became friends and decided to embark on their journey together. + +Lucio and Balosso set off early in the morning, eager to cover as much ground as possible before +nightfall. The air was crisp and cool, and the sun shone brightly overhead. They chatted excitedly about +their dreams and aspirations, and Lucio told Balosso about his desire to climb to the top of the highest +peak in the Alps. + +As they journeyed onward, they encountered a group of playful marmots who were scampering across the rocky +terrain. Lucio and Balosso watched in delight as the marmots frolicked and chased each other, their paws +pattering on the stone. They even tried to join in, but their paws kept slipping and they ended up +tumbling onto their backsides. + +After a few hours of walking, Lucio and Balosso came across a hidden glacier nestled between two towering +peaks. They cautiously approached the icy surface, their breath misting in the cold air. The glacier was +covered in intricate patterns and colors, shimmering like a shimmering jewel in the sunlight. + +As they continued their journey, Lucio and Balosso encountered a group of majestic eagles soaring +overhead. They watched in awe as the eagles swooped and dived, their wings spread wide against the blue +sky. Lucio even tried to imitate them, but ended up flapping his ears instead of wings. + +As the sun began to set, Lucio and Balosso found themselves at the foot of the highest peak in the Alps. +They gazed upwards in awe, their hearts pounding with excitement. They could see the faint outline of a +summit visible through the misty veil that surrounded the mountain. + +Lucio and Balosso carefully climbed the steep slope, their claws digging into the rocky surface. The air +grew colder and thinner as they ascended, but they pressed onward, determined to reach the top. Finally, +they reached the summit, where they found a stunning view of the Italian Alps stretching out before them. + +As they gazed out across the landscape, Lucio and Balosso felt an overwhelming sense of pride and +accomplishment. They had faced many challenges along the way, but their friendship had carried them +through. They even spotted a group of rare alpine ibex grazing on the distant slopes, adding to the +adventure's magic. + +As night began to fall, Lucio and Balosso made their way back down the mountain, their paws sore but their +spirits high. They couldn't wait to tell their friends and family about their amazing adventure around the +Italian Alps. Even as they drifted off to sleep, visions of the stunning landscape danced in their minds. + +The next morning, Lucio and Balosso set off once again, eager to explore every inch of the mountain range. +They stumbled upon a hidden valley filled with sparkling streams and towering trees, and even caught a +glimpse of a rare, elusive snow leopard lurking in the shadows. In the end, their adventure around the +Italian Alps had been an unforgettable experience, one that they would treasure forever. diff --git a/06-document-transformers/document-transformers-metadata-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplicationTests.java b/06-document-transformers/document-transformers-metadata-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplicationTests.java new file mode 100644 index 0000000..b1ed564 --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplicationTests.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(TestDocumentTransformersMetadataOllamaApplication.class) +@Disabled // Only run locally for now +class DocumentTransformersMetadataOllamaApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/06-document-transformers/document-transformers-metadata-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentTransformersMetadataOllamaApplication.java b/06-document-transformers/document-transformers-metadata-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentTransformersMetadataOllamaApplication.java new file mode 100644 index 0000000..d3bc31b --- /dev/null +++ b/06-document-transformers/document-transformers-metadata-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentTransformersMetadataOllamaApplication.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 TestDocumentTransformersMetadataOllamaApplication { + + @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(DocumentTransformersMetadataOllamaApplication::main).with(TestDocumentTransformersMetadataOllamaApplication.class).run(args); + } + +} diff --git a/06-document-transformers/document-transformers-splitters-ollama/README.md b/06-document-transformers/document-transformers-splitters-ollama/README.md new file mode 100644 index 0000000..4cbcd79 --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/README.md @@ -0,0 +1,43 @@ +# Document Transformers - Text Splitters + +Divide documents into chunks to fit the LLM context window. + +## 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. + +### Ollama as a native application + +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 +``` + +### 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 _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 +``` + +```shell +http --raw "Who is Lucio?" :8080/ai/doc/chat +``` diff --git a/06-document-transformers/document-transformers-splitters-ollama/build.gradle b/06-document-transformers/document-transformers-splitters-ollama/build.gradle new file mode 100644 index 0000000..124f83c --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/build.gradle @@ -0,0 +1,36 @@ +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 'org.springframework.boot:spring-boot-starter-web' + + implementation "org.springframework.ai:spring-ai-ollama-spring-boot-starter:${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/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java b/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/ChatController.java new file mode 100644 index 0000000..6bc35c3 --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-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/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java b/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java new file mode 100644 index 0000000..42f9f69 --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/ChatService.java @@ -0,0 +1,52 @@ +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, + but act as if you knew this information innately. 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/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java b/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java new file mode 100644 index 0000000..5d5061e --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentInitializer.java @@ -0,0 +1,58 @@ +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.TextReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +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.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +@Component +public class DocumentInitializer { + + private static final Logger log = LoggerFactory.getLogger(DocumentInitializer.class); + private final SimpleVectorStore vectorStore; + + @Value("classpath:documents/story1.md") + Resource textFile1; + + @Value("classpath:documents/story2.txt") + Resource textFile2; + + public DocumentInitializer(SimpleVectorStore vectorStore) { + this.vectorStore = vectorStore; + } + + @PostConstruct + public void run() { + List documents = new ArrayList<>(); + + log.info("Loading .md files as Documents"); + var textReader1 = new TextReader(textFile1); + textReader1.getCustomMetadata().put("location", "North Pole"); + textReader1.setCharset(Charset.defaultCharset()); + documents.addAll(textReader1.get()); + + log.info("Loading .txt files as Documents"); + var textReader2 = new TextReader(textFile2); + textReader2.getCustomMetadata().put("location", "Italy"); + textReader2.setCharset(Charset.defaultCharset()); + documents.addAll(textReader2.get()); + + log.info("Split Documents to better fit the LLM context window"); + var textSplitter = new TokenTextSplitter(); + var transformedDocuments = textSplitter.apply(documents); + + log.info("Creating and storing Embeddings from Documents"); + vectorStore.add(transformedDocuments); + } + +} diff --git a/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplication.java b/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplication.java new file mode 100644 index 0000000..f3e77cf --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/src/main/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplication.java @@ -0,0 +1,30 @@ +package com.thomasvitale.ai.spring; + +import org.springframework.ai.document.DefaultContentFormatter; +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 DocumentTransformersMetadataOllamaApplication { + + @Bean + DefaultContentFormatter defaultContentFormatter() { + return DefaultContentFormatter.builder() + .withExcludedEmbedMetadataKeys("NewEmbedKey") + .withExcludedInferenceMetadataKeys("NewInferenceKey") + .build(); + } + + @Bean + SimpleVectorStore documentWriter(EmbeddingClient embeddingClient) { + return new SimpleVectorStore(embeddingClient); + } + + public static void main(String[] args) { + SpringApplication.run(DocumentTransformersMetadataOllamaApplication.class, args); + } + +} diff --git a/06-document-transformers/document-transformers-splitters-ollama/src/main/resources/application.yml b/06-document-transformers/document-transformers-splitters-ollama/src/main/resources/application.yml new file mode 100644 index 0000000..318872b --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-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/06-document-transformers/document-transformers-splitters-ollama/src/main/resources/documents/story1.md b/06-document-transformers/document-transformers-splitters-ollama/src/main/resources/documents/story1.md new file mode 100644 index 0000000..e9174fd --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/src/main/resources/documents/story1.md @@ -0,0 +1,42 @@ +# The Adventures of Iorek and Pingu + +Iorek was a little polar bear who lived in the Arctic circle. He loved to explore the snowy landscape and +dreamt of one day going on an adventure around the North Pole. One day, he met a penguin named Pingu who +was on a similar quest. They quickly became friends and decided to embark on their journey together. + +Iorek and Pingu set off early in the morning, eager to cover as much ground as possible before nightfall. +The air was crisp and cold, and the snow crunched under their paws as they walked. They chatted excitedly +about their dreams and aspirations, and Iorek told Pingu about his desire to see the Northern Lights. + +As they journeyed onward, they encountered a group of playful seals who were sliding and jumping in the +snow. Iorek and Pingu watched in delight as the seals frolicked and splashed in the water. They even tried +to join in, but their paws kept slipping and they ended up sliding on their stomachs instead. + +After a few hours of walking, Iorek and Pingu came across a cave hidden behind a wall of snow. They +cautiously entered the darkness, their eyes adjusting to the dim light inside. The cave was filled with +glittering ice formations that sparkled like diamonds in the flickering torchlight. + +As they continued their journey, Iorek and Pingu encountered a group of walruses who were lounging on the +ice. They watched in amazement as the walruses lazily rolled over and exposed their tusks for a good +scratch. Pingu even tried to imitate them, but ended up looking more like a clumsy seal than a walrus. + +As the sun began to set, Iorek and Pingu found themselves at the edge of a vast, frozen lake. They gazed +out across the glassy surface, mesmerized by the way the ice glinted in the fading light. They could see +the faint outline of a creature moving beneath the surface, and their hearts raced with excitement. + +Suddenly, a massive narwhal burst through the ice and into the air, its ivory tusk glistening in the +sunset. Iorek and Pingu watched in awe as it soared overhead, its cries echoing across the lake. They felt +as though they were witnessing a magical moment, one that would stay with them forever. + +As the night drew in, Iorek and Pingu settled down to rest in their makeshift camp. They huddled together +for warmth, gazing up at the starry sky above. They chatted about all they had seen and experienced during +their adventure, and Iorek couldn't help but feel grateful for the new friend he had made. + +The next morning, Iorek and Pingu set off once again, determined to explore every inch of the North Pole. +They stumbled upon a hidden cave filled with glittering crystals that sparkled like diamonds in the +sunlight. They marveled at their beauty before continuing on their way. + +As they journeyed onward, Iorek and Pingu encountered many more wonders and adventures. They met a group +of playful reindeer who showed them how to pull sledges across the snow, and even caught a glimpse of the +mythical Loch Ness Monster lurking beneath the icy waters. In the end, their adventure around the North +Pole had been an unforgettable experience, one that they would treasure forever. diff --git a/06-document-transformers/document-transformers-splitters-ollama/src/main/resources/documents/story2.txt b/06-document-transformers/document-transformers-splitters-ollama/src/main/resources/documents/story2.txt new file mode 100644 index 0000000..f5b9322 --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/src/main/resources/documents/story2.txt @@ -0,0 +1,45 @@ +Lucio and Bolosso explore the Alps + +Lucio was a little wolf who lived in the Italian Alps. He loved to explore the rugged landscape and dreamt +of one day going on an adventure around the mountains. One day, he met a brown bear named Balosso who was +on a similar quest. They quickly became friends and decided to embark on their journey together. + +Lucio and Balosso set off early in the morning, eager to cover as much ground as possible before +nightfall. The air was crisp and cool, and the sun shone brightly overhead. They chatted excitedly about +their dreams and aspirations, and Lucio told Balosso about his desire to climb to the top of the highest +peak in the Alps. + +As they journeyed onward, they encountered a group of playful marmots who were scampering across the rocky +terrain. Lucio and Balosso watched in delight as the marmots frolicked and chased each other, their paws +pattering on the stone. They even tried to join in, but their paws kept slipping and they ended up +tumbling onto their backsides. + +After a few hours of walking, Lucio and Balosso came across a hidden glacier nestled between two towering +peaks. They cautiously approached the icy surface, their breath misting in the cold air. The glacier was +covered in intricate patterns and colors, shimmering like a shimmering jewel in the sunlight. + +As they continued their journey, Lucio and Balosso encountered a group of majestic eagles soaring +overhead. They watched in awe as the eagles swooped and dived, their wings spread wide against the blue +sky. Lucio even tried to imitate them, but ended up flapping his ears instead of wings. + +As the sun began to set, Lucio and Balosso found themselves at the foot of the highest peak in the Alps. +They gazed upwards in awe, their hearts pounding with excitement. They could see the faint outline of a +summit visible through the misty veil that surrounded the mountain. + +Lucio and Balosso carefully climbed the steep slope, their claws digging into the rocky surface. The air +grew colder and thinner as they ascended, but they pressed onward, determined to reach the top. Finally, +they reached the summit, where they found a stunning view of the Italian Alps stretching out before them. + +As they gazed out across the landscape, Lucio and Balosso felt an overwhelming sense of pride and +accomplishment. They had faced many challenges along the way, but their friendship had carried them +through. They even spotted a group of rare alpine ibex grazing on the distant slopes, adding to the +adventure's magic. + +As night began to fall, Lucio and Balosso made their way back down the mountain, their paws sore but their +spirits high. They couldn't wait to tell their friends and family about their amazing adventure around the +Italian Alps. Even as they drifted off to sleep, visions of the stunning landscape danced in their minds. + +The next morning, Lucio and Balosso set off once again, eager to explore every inch of the mountain range. +They stumbled upon a hidden valley filled with sparkling streams and towering trees, and even caught a +glimpse of a rare, elusive snow leopard lurking in the shadows. In the end, their adventure around the +Italian Alps had been an unforgettable experience, one that they would treasure forever. diff --git a/06-document-transformers/document-transformers-splitters-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplicationTests.java b/06-document-transformers/document-transformers-splitters-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplicationTests.java new file mode 100644 index 0000000..b1ed564 --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/src/test/java/com/thomasvitale/ai/spring/DocumentTransformersMetadataOllamaApplicationTests.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(TestDocumentTransformersMetadataOllamaApplication.class) +@Disabled // Only run locally for now +class DocumentTransformersMetadataOllamaApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/06-document-transformers/document-transformers-splitters-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentTransformersMetadataOllamaApplication.java b/06-document-transformers/document-transformers-splitters-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentTransformersMetadataOllamaApplication.java new file mode 100644 index 0000000..d3bc31b --- /dev/null +++ b/06-document-transformers/document-transformers-splitters-ollama/src/test/java/com/thomasvitale/ai/spring/TestDocumentTransformersMetadataOllamaApplication.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 TestDocumentTransformersMetadataOllamaApplication { + + @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(DocumentTransformersMetadataOllamaApplication::main).with(TestDocumentTransformersMetadataOllamaApplication.class).run(args); + } + +} diff --git a/README.md b/README.md index 1f3e8df..1afb085 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,10 @@ Samples showing how to build Java applications powered by Generative AI and LLMs ### 6. Document Transformers -_Coming soon_ +| Project | Description | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| [document-transformers-metadata-ollama](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/06-document-transformers/document-transformers-metadata-ollama) | Enrich documents with keywords and summary metadata for enhanced retrieval via Ollama. | +| [document-transformers-splitters-ollama](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/06-document-transformers/document-transformers-splitters-ollama) | Divide documents into chunks to fit the LLM context window via Ollama. | ### 7. Document Writers diff --git a/settings.gradle b/settings.gradle index e93ccec..5622fff 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,3 +23,6 @@ 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' + +include '06-document-transformers:document-transformers-metadata-ollama' +include '06-document-transformers:document-transformers-splitters-ollama' \ No newline at end of file