Skip to content

Commit 406a065

Browse files
authored
feat: creating new tutorial for creating a spring ai app (#4852)
* feat: creating new tutorial for creating a spring ai app This is a draft to add a tutorial for creating a Spring AI app using Kotlin * update: adding rest of the guide for a first draft * small change from properties to text tag * modifying TOC * changing the title to something a bit more explicit * fixing code comments * implementing TWr review comments
1 parent c129942 commit 406a065

9 files changed

+347
-0
lines changed
254 KB
Loading
128 KB
Loading
306 KB
Loading
244 KB
Loading
303 KB
Loading
447 KB
Loading
120 KB
Loading

docs/kr.tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
<toc-element toc-title="Use Spring Data CrudRepository" topic="jvm-spring-boot-using-crudrepository.md"/>
161161
</toc-element>
162162
<!-- <toc-element id="jvm-spring-boot-restful.md" accepts-web-file-names="spring-boot-restful.html,spring-boot-restful-db.html,jvm-spring-boot-restful-db.html,jvm-get-started-spring-boot.html,jvm-create-project-with-spring-boot.html,jvm-spring-boot-add-data-class.html,jvm-spring-boot-add-db-support.html,jvm-spring-boot-using-crudrepository.html"/>-->
163+
<toc-element topic="spring-ai-guide.md"/>
163164
<toc-element toc-title="Spring Framework Documentation for Kotlin" href="https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#languages"/>
164165
<toc-element toc-title="Build a web application with Spring Boot and Kotlin – tutorial" href="https://spring.io/guides/tutorials/spring-boot-kotlin/"/>
165166
<toc-element toc-title="Create a chat application with Kotlin Coroutines and RSocket – tutorial" href="https://spring.io/guides/tutorials/spring-webflux-kotlin-rsocket/"/>

docs/topics/spring-ai-guide.md

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
[//]: # (title: Build a Kotlin app that uses Spring AI to answer questions based on documents stored in Qdrant — tutorial)
2+
3+
In this tutorial, you'll learn how to build a Kotlin app that uses [Spring AI](https://spring.io/projects/spring-ai) to connect to an LLM,
4+
store documents in a vector database, and answer questions using context from those documents.
5+
6+
You will use the following tools during this tutorial:
7+
8+
* [Spring Boot](https://spring.io/projects/spring-boot) as the base to configure and run the web application.
9+
* [Spring AI](https://spring.io/projects/spring-ai) to interact with the LLM and perform context-based retrieval.
10+
* [IntelliJ IDEA](https://www.jetbrains.com/idea/) to generate the project and implement the application logic.
11+
* [Qdrant](https://qdrant.tech/) as the vector database for similarity search.
12+
* [Docker](https://www.docker.com/) to run Qdrant locally.
13+
* [OpenAI](https://platform.openai.com) as the LLM provider.
14+
15+
## Before you start
16+
17+
1. Download and install the latest version of [IntelliJ IDEA Ultimate Edition](https://www.jetbrains.com/idea/download/index.html).
18+
19+
> If you use IntelliJ IDEA Community Edition or another IDE, you can generate a Spring Boot project using
20+
> a [web-based project generator](https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin).
21+
>
22+
{style="tip"}
23+
24+
2. Create an OpenAI API key on the [OpenAI platform](https://platform.openai.com/api-keys) to access the API.
25+
3. Install [Docker](https://www.docker.com/) to run the Qdrant vector database locally.
26+
4. After installing Docker, open your terminal and run the following command to start the container:
27+
28+
```bash
29+
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
30+
```
31+
32+
## Create the project
33+
34+
> You can use [Spring Boot web-based project generator](https://start.spring.io/) as an alternative to generate your project.
35+
>
36+
{style="note"}
37+
38+
Create a new Spring Boot project in IntelliJ IDEA Ultimate Edition:
39+
40+
1. In IntelliJ IDEA, select **File** | **New** | **Project**.
41+
2. In the panel on the left, select **New Project** | **Spring Boot**.
42+
3. Specify the following fields and options in the **New Project** window:
43+
44+
* **Name**: springAIDemo
45+
* **Language**: Kotlin
46+
* **Type**: Gradle - Kotlin
47+
48+
> This option specifies the build system and the DSL.
49+
>
50+
{style="tip"}
51+
52+
* **Package name**: com.example.springaidemo
53+
* **JDK**: Java JDK
54+
55+
> This tutorial uses **Oracle OpenJDK version 21.0.1**.
56+
> If you don't have a JDK installed, you can download it from the dropdown list.
57+
>
58+
{style="note"}
59+
60+
* **Java**: 17
61+
62+
![Create Spring Boot project](create-spring-ai-project.png){width=800}
63+
64+
4. Make sure that you have specified all the fields and click **Next**.
65+
5. Select the latest stable Spring Boot version in the **Spring Boot** field.
66+
67+
6. Select the following dependencies required for this tutorial:
68+
69+
* **Web | Spring Web**
70+
* **AI | OpenAI**
71+
* **SQL | Qdrant Vector Database**
72+
73+
![Set up Spring Boot project](spring-ai-dependencies.png){width=800}
74+
75+
7. Click **Create** to generate and set up the project.
76+
77+
> The IDE will generate and open a new project. It may take some time to download and import the project dependencies.
78+
>
79+
{style="tip"}
80+
81+
After this, you can see the following structure in the **Project view**:
82+
83+
![Spring Boot project view](spring-ai-project-view.png){width=400}
84+
85+
The generated Gradle project corresponds to the Maven's standard directory layout:
86+
87+
* There are packages and classes under the `main/kotlin` folder that belong to the application.
88+
* The entry point to the application is the `main()` method of the `SpringAiDemoApplication.kt` file.
89+
90+
91+
## Update the project configuration
92+
93+
1. Update your `build.gradle.kts` Gradle build file with the following:
94+
95+
```kotlin
96+
plugins {
97+
kotlin("jvm") version "%kotlinVersion%"
98+
kotlin("plugin.spring") version "%kotlinVersion%"
99+
// Rest of the plugins
100+
}
101+
```
102+
103+
2. Update your `springAiVersion` to `1.0.0-M6`:
104+
105+
```kotlin
106+
extra["springAiVersion"] = "1.0.0-M6"
107+
```
108+
109+
3. Click the **Sync Gradle Changes** button to synchronize the Gradle files.
110+
4. Update your `src/main/resources/application.properties` file with the following:
111+
112+
```text
113+
# OpenAI
114+
spring.ai.openai.api-key=YOUR_OPENAI_API_KEY
115+
spring.ai.openai.chat.options.model=gpt-4o-mini
116+
spring.ai.openai.embedding.options.model=text-embedding-ada-002
117+
# Qdrant
118+
spring.ai.vectorstore.qdrant.host=localhost
119+
spring.ai.vectorstore.qdrant.port=6334
120+
spring.ai.vectorstore.qdrant.collection-name=kotlinDocs
121+
spring.ai.vectorstore.qdrant.initialize-schema=true
122+
```
123+
124+
> Set your OpenAI API key to the `spring.ai.openai.api-key` property.
125+
>
126+
{style="note"}
127+
128+
5. Run the `SpringAiDemoApplication.kt` file to start the Spring Boot application.
129+
Once it's running, open the [Qdrant collections](http://localhost:6333/dashboard#/collections) page in your browser to see the result:
130+
131+
![Qdrant collections](qdrant-collections.png){width=700}
132+
133+
## Create a controller to load and search documents
134+
135+
Create a Spring `@RestController` to search documents and store them in the Qdrant collection:
136+
137+
1. In the `src/main/kotlin/org/example/springaidemo` directory, create a new file named `KotlinSTDController.kt`, and add the following code:
138+
139+
```kotlin
140+
package org.example.springaidemo
141+
142+
// Imports the required Spring and utility classes
143+
import org.slf4j.LoggerFactory
144+
import org.springframework.ai.document.Document
145+
import org.springframework.ai.vectorstore.SearchRequest
146+
import org.springframework.ai.vectorstore.VectorStore
147+
import org.springframework.web.bind.annotation.GetMapping
148+
import org.springframework.web.bind.annotation.PostMapping
149+
import org.springframework.web.bind.annotation.RequestMapping
150+
import org.springframework.web.bind.annotation.RequestParam
151+
import org.springframework.web.bind.annotation.RestController
152+
import org.springframework.web.client.RestTemplate
153+
import kotlin.uuid.ExperimentalUuidApi
154+
import kotlin.uuid.Uuid
155+
156+
// Data class representing the chat request payload
157+
data class ChatRequest(val query: String, val topK: Int = 3)
158+
159+
@RestController
160+
@RequestMapping("/kotlin")
161+
class KotlinSTDController(
162+
private val restTemplate: RestTemplate,
163+
private val vectorStore: VectorStore,
164+
) {
165+
private val logger = LoggerFactory.getLogger(this::class.java)
166+
167+
@OptIn(ExperimentalUuidApi::class)
168+
@PostMapping("/load-docs")
169+
fun load() {
170+
// Loads a list of documents from the Kotlin documentation
171+
val kotlinStdTopics = listOf(
172+
"collections-overview", "constructing-collections", "iterators", "ranges", "sequences",
173+
"collection-operations", "collection-transformations", "collection-filtering", "collection-plus-minus",
174+
"collection-grouping", "collection-parts", "collection-elements", "collection-ordering",
175+
"collection-aggregate", "collection-write", "list-operations", "set-operations",
176+
"map-operations", "read-standard-input", "opt-in-requirements", "scope-functions", "time-measurement",
177+
)
178+
// Base URL for the documents
179+
val url = "https://raw.githubusercontent.com/JetBrains/kotlin-web-site/refs/heads/master/docs/topics/"
180+
// Retrieves each document from the URL and adds it to the vector store
181+
kotlinStdTopics.forEach { topic ->
182+
val data = restTemplate.getForObject("$url$topic.md", String::class.java)
183+
data?.let { it ->
184+
val doc = Document.builder()
185+
// Builds a document with a random UUID
186+
.id(Uuid.random().toString())
187+
.text(it)
188+
.metadata("topic", topic)
189+
.build()
190+
vectorStore.add(listOf(doc))
191+
logger.info("Document $topic loaded.")
192+
} ?: logger.warn("Failed to load document for topic: $topic")
193+
}
194+
}
195+
196+
@GetMapping("docs")
197+
fun query(
198+
@RequestParam query: String = "operations, filtering, and transformations",
199+
@RequestParam topK: Int = 2
200+
): List<Document>? {
201+
val searchRequest = SearchRequest.builder()
202+
.query(query)
203+
.topK(topK)
204+
.build()
205+
val results = vectorStore.similaritySearch(searchRequest)
206+
logger.info("Found ${results?.size ?: 0} documents for query: '$query'")
207+
return results
208+
}
209+
}
210+
```
211+
{collapsible="true"}
212+
213+
2. Update the `SpringAiDemoApplication.kt` file to declare a `RestTemplate` bean:
214+
215+
```kotlin
216+
package org.example.springaidemo
217+
218+
import org.springframework.boot.autoconfigure.SpringBootApplication
219+
import org.springframework.boot.runApplication
220+
import org.springframework.context.annotation.Bean
221+
import org.springframework.web.client.RestTemplate
222+
223+
@SpringBootApplication
224+
class SpringAiDemoApplication {
225+
226+
@Bean
227+
fun restTemplate(): RestTemplate = RestTemplate()
228+
}
229+
230+
fun main(args: Array<String>) {
231+
runApplication<SpringAiDemoApplication>(*args)
232+
}
233+
```
234+
{collapsible="true"}
235+
236+
3. Run the application.
237+
4. In the terminal, send a POST request to the `/kotlin/load-docs` endpoint to load the documents:
238+
239+
```bash
240+
curl -X POST http://localhost:8080/kotlin/load-docs
241+
```
242+
243+
5. Once the documents are loaded, you can search for them with a GET request:
244+
245+
```Bash
246+
curl -X GET http://localhost:8080/kotlin/docs
247+
```
248+
249+
![GET request results](spring-ai-get-results.png){width="700"}
250+
251+
> You can also view the results on the [Qdrant collections](http://localhost:6333/dashboard#/collections) page.
252+
>
253+
{style="tip"}
254+
255+
## Implement an AI chat endpoint
256+
257+
Once the documents are loaded, the final step is to add an endpoint that answers questions using the documents in Qdrant through Spring AI's Retrieval-Augmented Generation (RAG) support:
258+
259+
1. Open the `KotlinSTDController.kt` file, and import the following classes:
260+
261+
```kotlin
262+
import org.springframework.ai.chat.client.ChatClient
263+
import org.springframework.ai.chat.client.advisor.RetrievalAugmentationAdvisor
264+
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor
265+
import org.springframework.ai.chat.prompt.Prompt
266+
import org.springframework.ai.chat.prompt.PromptTemplate
267+
import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer
268+
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever
269+
import org.springframework.web.bind.annotation.RequestBody
270+
```
271+
272+
2. Add `ChatClient.Builder` to the controller's constructor parameters:
273+
274+
```kotlin
275+
class KotlinSTDController(
276+
private val chatClientBuilder: ChatClient.Builder,
277+
private val restTemplate: RestTemplate,
278+
private val vectorStore: VectorStore,
279+
)
280+
```
281+
282+
3. Inside the controller class, create a `ChatClient` instance and a query transformer:
283+
284+
```kotlin
285+
// Builds the chat client with a simple logging advisor
286+
private val chatClient = chatClientBuilder.defaultAdvisors(SimpleLoggerAdvisor()).build()
287+
// Builds the query transformer used to rewrite the input query
288+
private val rqtBuilder = RewriteQueryTransformer.builder().chatClientBuilder(chatClientBuilder)
289+
```
290+
291+
4. At the bottom of your `KotlinSTDController.kt` file, add a new `chatAsk()` endpoint, with the following logic:
292+
293+
```kotlin
294+
@PostMapping("/chat/ask")
295+
fun chatAsk(@RequestBody request: ChatRequest): String? {
296+
// Defines the prompt template with placeholders
297+
val promptTemplate = PromptTemplate(
298+
"""
299+
{query}.
300+
Please provide a concise answer based on the {target} documentation.
301+
""".trimIndent()
302+
)
303+
304+
// Creates the prompt by substituting placeholders with actual values
305+
val prompt: Prompt =
306+
promptTemplate.create(mapOf("query" to request.query, "target" to "Kotlin standard library"))
307+
308+
// Configures the retrieval advisor to augment the query with relevant documents
309+
val retrievalAdvisor = RetrievalAugmentationAdvisor.builder()
310+
.documentRetriever(
311+
VectorStoreDocumentRetriever.builder()
312+
.similarityThreshold(0.7)
313+
.topK(request.topK)
314+
.vectorStore(vectorStore)
315+
.build()
316+
)
317+
.queryTransformers(rqtBuilder.promptTemplate(promptTemplate).build())
318+
.build()
319+
320+
// Sends the prompt to the LLM with the retrieval advisor and get the response
321+
val response = chatClient.prompt(prompt)
322+
.advisors(retrievalAdvisor)
323+
.call()
324+
.content()
325+
logger.info("Chat response generated for query: '${request.query}'")
326+
return response
327+
}
328+
```
329+
330+
5. Run the application.
331+
6. In the terminal, send a POST request to the new endpoint to see the results:
332+
333+
```bash
334+
curl -X POST "http://localhost:8080/kotlin/chat/ask" \
335+
-H "Content-Type: application/json" \
336+
-d '{"query": "What are the performance implications of using lazy sequences in Kotlin for large datasets?", "topK": 3}'
337+
```
338+
339+
![OpenAI answer to chat request](open-ai-chat-endpoint.png){width="700"}
340+
341+
Congratulations! You now have a Kotlin app that connects to OpenAI and answers questions using context retrieved from
342+
documentation stored in Qdrant.
343+
Try experimenting with different queries or importing other documents to explore more possibilities.
344+
345+
You can view the completed project in the [Spring AI demo GitHub repository](https://github.com/Kotlin/Kotlin-AI-Examples/tree/master/projects/spring-ai/springAI-demo),
346+
or explore other Spring AI examples in [Kotlin AI Examples](https://github.com/Kotlin/Kotlin-AI-Examples/tree/master).

0 commit comments

Comments
 (0)