(selectedSeat || null);
16 |
17 | // Update local selected seat if prop changes
18 | useEffect(() => {
19 | setCurrentSelectedSeat(selectedSeat || null);
20 | }, [selectedSeat]);
21 |
22 | // Handler for seat selection
23 | const handleSeatClick = (seatId: string) => {
24 | setCurrentSelectedSeat(seatId);
25 | if (onSeatSelect) {
26 | onSeatSelect(seatId);
27 | }
28 | };
29 |
30 | // Render the seats
31 | const renderSeats = () => {
32 | const seatRows = [];
33 | for (let row = 1; row <= rows; row++) {
34 | const seatRow = [];
35 | for (let seatIndex = 0; seatIndex < seatsPerRow; seatIndex++) {
36 | const seatLetter = seatLetters[seatIndex];
37 | const seatId = `${row}${seatLetter}`;
38 | const isSelected = currentSelectedSeat === seatId;
39 | seatRow.push(
40 | handleSeatClick(seatId)}
43 | style={{
44 | width: 30,
45 | height: 30,
46 | margin: 2,
47 | backgroundColor: isSelected ? 'blue' : 'lightgray',
48 | color: isSelected ? 'white' : 'black',
49 | textAlign: 'center',
50 | lineHeight: '30px',
51 | cursor: 'pointer',
52 | }}
53 | >
54 | {seatLetter}
55 |
56 | );
57 | // Insert aisle after seat C
58 | if (seatLetter === 'C') {
59 | seatRow.push(
60 |
61 | );
62 | }
63 | }
64 | seatRows.push(
65 |
66 |
{row}
67 | {seatRow}
68 |
69 | );
70 | }
71 | return seatRows;
72 | };
73 |
74 | return (
75 |
76 | {renderSeats()}
77 |
78 | );
79 | };
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Funnair customer support
7 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/frontend/themes/customer-support-agent/styles.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/playground-flight-booking/frontend/themes/customer-support-agent/styles.css
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/frontend/themes/customer-support-agent/theme.json:
--------------------------------------------------------------------------------
1 | {
2 | "lumoImports" : [ "typography", "color", "spacing", "badge", "utility" ]
3 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/frontend/views/@index.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from "react";
2 | import {AssistantService, BookingService} from "../generated/endpoints";
3 | import BookingDetails from "../generated/ai/spring/demo/ai/playground/services/BookingTools/BookingDetails";
4 | import {GridColumn} from "@vaadin/react-components/GridColumn";
5 | import {Grid} from "@vaadin/react-components/Grid";
6 | import {MessageInput} from "@vaadin/react-components/MessageInput";
7 | import {nanoid} from "nanoid";
8 | import {SplitLayout} from "@vaadin/react-components/SplitLayout";
9 | import Message, {MessageItem} from "../components/Message";
10 | import MessageList from "../components/MessageList";
11 | import {Dialog} from "@vaadin/react-components";
12 | import SeatSelection from "../components/SeatSelection";
13 |
14 | export default function Index() {
15 | const [chatId, setChatId] = useState(nanoid());
16 | const [seatSelectionOpen, setSeatSelectionOpen] = useState(false);
17 | const [seatSelectionRequestId, setSeatSelectionRequestId] = useState('');
18 | const [working, setWorking] = useState(false);
19 | const [bookings, setBookings] = useState([]);
20 | const [messages, setMessages] = useState([{
21 | role: 'assistant',
22 | content: 'Welcome to Funnair! How can I help you?'
23 | }]);
24 |
25 | useEffect(() => {
26 | AssistantService.seatChangeRequests(chatId).onNext(request => {
27 | setSeatSelectionRequestId(request.requestId);
28 | setSeatSelectionOpen(true);
29 | })
30 | }, []);
31 |
32 | useEffect(() => {
33 | // Update bookings when we have received the full response
34 | if (!working) {
35 | BookingService.getBookings().then(setBookings);
36 | }
37 | }, [working]);
38 |
39 | function addMessage(message: MessageItem) {
40 | setMessages(messages => [...messages, message]);
41 | }
42 |
43 | function appendToLatestMessage(chunk: string) {
44 | setMessages(messages => {
45 | const latestMessage = messages[messages.length - 1];
46 | latestMessage.content += chunk;
47 | return [...messages.slice(0, -1), latestMessage];
48 | });
49 | }
50 |
51 | async function sendMessage(message: string) {
52 | setWorking(true);
53 | addMessage({
54 | role: 'user',
55 | content: message
56 | });
57 | let first = true;
58 | AssistantService.chat(chatId, message)
59 | .onNext(token => {
60 | if (first && token) {
61 | addMessage({
62 | role: 'assistant',
63 | content: token
64 | });
65 |
66 | first = false;
67 | } else {
68 | appendToLatestMessage(token);
69 | }
70 | })
71 | .onError(() => setWorking(false))
72 | .onComplete(() => setWorking(false));
73 | }
74 |
75 | return (
76 | <>
77 |
78 |
79 |
Funnair support
80 |
81 | sendMessage(e.detail.value)} className="px-0" disabled={working}/>
82 |
83 |
84 |
Bookings database
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {({item}) => item.bookingStatus === "CONFIRMED" ? "✅" : "❌"}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
109 | >
110 | );
111 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/playground-flight-booking/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "no-name",
3 | "license": "UNLICENSED",
4 | "type": "module",
5 | "dependencies": {
6 | "@polymer/polymer": "3.5.2",
7 | "@vaadin/bundles": "24.6.4",
8 | "@vaadin/common-frontend": "0.0.19",
9 | "@vaadin/hilla-file-router": "24.6.5",
10 | "@vaadin/hilla-frontend": "24.6.5",
11 | "@vaadin/hilla-lit-form": "24.6.5",
12 | "@vaadin/hilla-react-auth": "24.6.5",
13 | "@vaadin/hilla-react-crud": "24.6.5",
14 | "@vaadin/hilla-react-form": "24.6.5",
15 | "@vaadin/hilla-react-i18n": "24.6.5",
16 | "@vaadin/hilla-react-signals": "24.6.5",
17 | "@vaadin/polymer-legacy-adapter": "24.6.4",
18 | "@vaadin/react-components": "24.6.4",
19 | "@vaadin/vaadin-development-mode-detector": "2.0.7",
20 | "@vaadin/vaadin-lumo-styles": "24.6.4",
21 | "@vaadin/vaadin-material-styles": "24.6.4",
22 | "@vaadin/vaadin-themable-mixin": "24.6.4",
23 | "@vaadin/vaadin-usage-statistics": "2.1.3",
24 | "construct-style-sheets-polyfill": "3.1.0",
25 | "date-fns": "2.29.3",
26 | "lit": "3.2.1",
27 | "nanoid": "^5.0.6",
28 | "react": "18.3.1",
29 | "react-dom": "18.3.1",
30 | "react-markdown": "^9.0.1",
31 | "react-router-dom": "6.29.0"
32 | },
33 | "devDependencies": {
34 | "@babel/preset-react": "7.26.3",
35 | "@preact/signals-react-transform": "0.5.1",
36 | "@rollup/plugin-replace": "6.0.2",
37 | "@rollup/pluginutils": "5.1.4",
38 | "@types/react": "18.3.18",
39 | "@types/react-dom": "18.3.5",
40 | "@vaadin/hilla-generator-cli": "24.6.5",
41 | "@vaadin/hilla-generator-core": "24.6.5",
42 | "@vaadin/hilla-generator-plugin-backbone": "24.6.5",
43 | "@vaadin/hilla-generator-plugin-barrel": "24.6.5",
44 | "@vaadin/hilla-generator-plugin-client": "24.6.5",
45 | "@vaadin/hilla-generator-plugin-model": "24.6.5",
46 | "@vaadin/hilla-generator-plugin-push": "24.6.5",
47 | "@vaadin/hilla-generator-plugin-signals": "24.6.5",
48 | "@vaadin/hilla-generator-plugin-subtypes": "24.6.5",
49 | "@vaadin/hilla-generator-utils": "24.6.5",
50 | "@vitejs/plugin-react": "4.3.4",
51 | "async": "3.2.6",
52 | "glob": "10.4.5",
53 | "rollup-plugin-brotli": "3.1.0",
54 | "rollup-plugin-visualizer": "5.14.0",
55 | "strip-css-comments": "5.0.0",
56 | "transform-ast": "2.4.4",
57 | "typescript": "5.7.3",
58 | "vite": "6.0.11",
59 | "vite-plugin-checker": "0.8.0",
60 | "workbox-build": "7.3.0",
61 | "workbox-core": "7.3.0",
62 | "workbox-precaching": "7.3.0"
63 | },
64 | "vaadin": {
65 | "dependencies": {
66 | "@polymer/polymer": "3.5.2",
67 | "@vaadin/bundles": "24.6.4",
68 | "@vaadin/common-frontend": "0.0.19",
69 | "@vaadin/hilla-file-router": "24.6.5",
70 | "@vaadin/hilla-frontend": "24.6.5",
71 | "@vaadin/hilla-lit-form": "24.6.5",
72 | "@vaadin/hilla-react-auth": "24.6.5",
73 | "@vaadin/hilla-react-crud": "24.6.5",
74 | "@vaadin/hilla-react-form": "24.6.5",
75 | "@vaadin/hilla-react-i18n": "24.6.5",
76 | "@vaadin/hilla-react-signals": "24.6.5",
77 | "@vaadin/polymer-legacy-adapter": "24.6.4",
78 | "@vaadin/react-components": "24.6.4",
79 | "@vaadin/vaadin-development-mode-detector": "2.0.7",
80 | "@vaadin/vaadin-lumo-styles": "24.6.4",
81 | "@vaadin/vaadin-material-styles": "24.6.4",
82 | "@vaadin/vaadin-themable-mixin": "24.6.4",
83 | "@vaadin/vaadin-usage-statistics": "2.1.3",
84 | "construct-style-sheets-polyfill": "3.1.0",
85 | "date-fns": "2.29.3",
86 | "lit": "3.2.1",
87 | "react": "18.3.1",
88 | "react-dom": "18.3.1",
89 | "react-router-dom": "6.29.0"
90 | },
91 | "devDependencies": {
92 | "@babel/preset-react": "7.26.3",
93 | "@preact/signals-react-transform": "0.5.1",
94 | "@rollup/plugin-replace": "6.0.2",
95 | "@rollup/pluginutils": "5.1.4",
96 | "@types/react": "18.3.18",
97 | "@types/react-dom": "18.3.5",
98 | "@vaadin/hilla-generator-cli": "24.6.5",
99 | "@vaadin/hilla-generator-core": "24.6.5",
100 | "@vaadin/hilla-generator-plugin-backbone": "24.6.5",
101 | "@vaadin/hilla-generator-plugin-barrel": "24.6.5",
102 | "@vaadin/hilla-generator-plugin-client": "24.6.5",
103 | "@vaadin/hilla-generator-plugin-model": "24.6.5",
104 | "@vaadin/hilla-generator-plugin-push": "24.6.5",
105 | "@vaadin/hilla-generator-plugin-signals": "24.6.5",
106 | "@vaadin/hilla-generator-plugin-subtypes": "24.6.5",
107 | "@vaadin/hilla-generator-utils": "24.6.5",
108 | "@vitejs/plugin-react": "4.3.4",
109 | "async": "3.2.6",
110 | "glob": "10.4.5",
111 | "rollup-plugin-brotli": "3.1.0",
112 | "rollup-plugin-visualizer": "5.14.0",
113 | "strip-css-comments": "5.0.0",
114 | "transform-ast": "2.4.4",
115 | "typescript": "5.7.3",
116 | "vite": "6.0.11",
117 | "vite-plugin-checker": "0.8.0",
118 | "workbox-build": "7.3.0",
119 | "workbox-core": "7.3.0",
120 | "workbox-precaching": "7.3.0"
121 | },
122 | "hash": "7f6a652bf4cddeee68c910556ab002143fef15b1b150e19f572c8d40f7faefea"
123 | },
124 | "overrides": {
125 | "@vaadin/bundles": "$@vaadin/bundles",
126 | "@vaadin/common-frontend": "$@vaadin/common-frontend",
127 | "construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
128 | "lit": "$lit",
129 | "@polymer/polymer": "$@polymer/polymer",
130 | "nanoid": "$nanoid",
131 | "react-markdown": "$react-markdown",
132 | "@vaadin/polymer-legacy-adapter": "$@vaadin/polymer-legacy-adapter",
133 | "@vaadin/vaadin-development-mode-detector": "$@vaadin/vaadin-development-mode-detector",
134 | "@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
135 | "@vaadin/react-components": "$@vaadin/react-components",
136 | "react-dom": "$react-dom",
137 | "@vaadin/hilla-frontend": "$@vaadin/hilla-frontend",
138 | "@vaadin/hilla-react-auth": "$@vaadin/hilla-react-auth",
139 | "react": "$react",
140 | "@vaadin/hilla-react-crud": "$@vaadin/hilla-react-crud",
141 | "@vaadin/hilla-file-router": "$@vaadin/hilla-file-router",
142 | "react-router-dom": "$react-router-dom",
143 | "@vaadin/hilla-react-i18n": "$@vaadin/hilla-react-i18n",
144 | "@vaadin/hilla-lit-form": "$@vaadin/hilla-lit-form",
145 | "@vaadin/hilla-react-form": "$@vaadin/hilla-react-form",
146 | "@vaadin/hilla-react-signals": "$@vaadin/hilla-react-signals",
147 | "date-fns": "$date-fns",
148 | "@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
149 | "@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
150 | "@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles"
151 | }
152 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "playground-flight-booking"
2 |
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/Application.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground
2 |
3 | import com.vaadin.flow.component.page.AppShellConfigurator
4 | import com.vaadin.flow.theme.Theme
5 | import io.micrometer.observation.ObservationPredicate
6 | import org.springframework.ai.chat.memory.ChatMemory
7 | import org.springframework.ai.chat.memory.MessageWindowChatMemory
8 | import org.springframework.ai.reader.TextReader
9 | import org.springframework.ai.transformer.splitter.TokenTextSplitter
10 | import org.springframework.ai.vectorstore.VectorStore
11 | import org.springframework.beans.factory.annotation.Value
12 | import org.springframework.boot.CommandLineRunner
13 | import org.springframework.boot.autoconfigure.SpringBootApplication
14 | import org.springframework.boot.runApplication
15 | import org.springframework.context.annotation.Bean
16 | import org.springframework.core.io.Resource
17 | import org.springframework.http.server.observation.ServerRequestObservationContext
18 |
19 | @SpringBootApplication
20 | @Theme(value = "customer-support-agent")
21 | class Application : AppShellConfigurator {
22 | /** Index the Terms-of-Service document into the vector store. */
23 | @Bean
24 | fun ingestTermOfServiceToVectorStore(
25 | vectorStore: VectorStore,
26 | @Value("classpath:rag/terms-of-service.txt") termsOfServiceDocs: Resource
27 | ): CommandLineRunner = CommandLineRunner {
28 | vectorStore.write(
29 | TokenTextSplitter().transform(
30 | TextReader(termsOfServiceDocs).read()
31 | )
32 | )
33 | }
34 |
35 | /** Default in-memory chat window. */
36 | @Bean
37 | fun chatMemory(): ChatMemory =
38 | MessageWindowChatMemory.builder().build()
39 |
40 | /**
41 | * Optionally suppress actuator and static-asset endpoints from micrometer
42 | * `http.server.requests` observations
43 | */
44 | @Bean
45 | fun noActuatorServerObservations(): ObservationPredicate =
46 | ObservationPredicate { name, context ->
47 | if (name == "http.server.requests" && context is ServerRequestObservationContext) {
48 | val uri = context.carrier.requestURI
49 | uri.run {
50 | !startsWith("/actuator") &&
51 | !startsWith("/VAADIN") &&
52 | !startsWith("/HILLA") &&
53 | !startsWith("/connect") &&
54 | !startsWith("/**") &&
55 | !equals("/", ignoreCase = true)
56 | }
57 | } else {
58 | true
59 | }
60 | }
61 | }
62 |
63 | fun main(args: Array) {
64 | runApplication(*args)
65 | }
66 |
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/client/AssistantService.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.client
2 |
3 | import com.vaadin.flow.server.auth.AnonymousAllowed
4 | import com.vaadin.hilla.BrowserCallable
5 | import io.github.devcrocod.example.playground.services.CustomerSupportAssistant
6 | import io.github.devcrocod.example.playground.services.SeatChangeQueue
7 | import io.github.devcrocod.example.playground.services.SeatChangeQueue.SeatChangeRequest
8 | import reactor.core.publisher.Flux
9 | import reactor.core.publisher.Sinks
10 |
11 |
12 | @BrowserCallable
13 | @AnonymousAllowed
14 | class AssistantService(
15 | private val assistant: CustomerSupportAssistant,
16 | private val seatChangeQueue: SeatChangeQueue
17 | ) {
18 |
19 | /** Send a user message to the chat assistant and stream its response. */
20 | fun chat(chatId: String, userMessage: String): Flux =
21 | assistant.chat(chatId, userMessage)
22 |
23 | /** Reactive stream of seat-change requests for the given chat session. */
24 | fun seatChangeRequests(chatId: String): Flux =
25 | seatChangeQueue.seatChangeRequests
26 | .computeIfAbsent(chatId) { Sinks.many().unicast().onBackpressureBuffer() }
27 | .asFlux()
28 |
29 | /** Completes a pending seat-change request with the chosen seat. */
30 | fun completeSeatChangeRequest(requestId: String, seat: String) {
31 | seatChangeQueue.pendingRequests
32 | .remove(requestId)
33 | ?.complete(seat)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/client/BookingService.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.client
2 |
3 | import com.vaadin.flow.server.auth.AnonymousAllowed
4 | import com.vaadin.hilla.BrowserCallable
5 | import io.github.devcrocod.example.playground.data.BookingDetails
6 | import io.github.devcrocod.example.playground.services.FlightBookingService
7 |
8 | @BrowserCallable
9 | @AnonymousAllowed
10 | class BookingService(private val flightBookingService: FlightBookingService) {
11 |
12 | fun getBookings(): List =
13 | flightBookingService.getBookings()
14 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/data/Booking.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.data
2 |
3 | import java.time.LocalDate
4 |
5 | data class Booking(
6 | var bookingNumber: String,
7 | var date: LocalDate,
8 | var bookingTo: LocalDate? = null,
9 | var customer: Customer,
10 | var from: String,
11 | var to: String,
12 | var bookingStatus: BookingStatus,
13 | var seatNumber: String,
14 | var bookingClass: BookingClass,
15 | )
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/data/BookingClass.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.data
2 |
3 | enum class BookingClass {
4 | ECONOMY, PREMIUM_ECONOMY, BUSINESS
5 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/data/BookingData.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.data
2 |
3 | data class BookingData(
4 | var customers: List = emptyList(),
5 | var bookings: List = emptyList()
6 | )
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/data/BookingDetails.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.data
2 |
3 | import java.time.LocalDate
4 |
5 | data class BookingDetails(
6 | val bookingNumber: String,
7 | val firstName: String,
8 | val lastName: String,
9 | val date: LocalDate?,
10 | val bookingStatus: BookingStatus?,
11 | val from: String?,
12 | val to: String?,
13 | val seatNumber: String?,
14 | val bookingClass: String?
15 | )
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/data/BookingStatus.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.data
2 |
3 | enum class BookingStatus {
4 | CONFIRMED, COMPLETED, CANCELLED
5 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/data/Customer.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.data
2 |
3 | data class Customer(
4 | var firstName: String,
5 | var lastName: String,
6 | var bookings: MutableList = mutableListOf()
7 | )
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/services/BookingTools.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.services
2 |
3 | import io.github.devcrocod.example.playground.data.BookingDetails
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.ai.chat.model.ToolContext
6 | import org.springframework.ai.tool.annotation.Tool
7 | import org.springframework.core.NestedExceptionUtils
8 | import org.springframework.stereotype.Service
9 | import java.util.concurrent.CompletableFuture
10 |
11 | @Service
12 | class BookingTools(
13 | private val flightBookingService: FlightBookingService,
14 | private val seatChangeQueue: SeatChangeQueue
15 | ) {
16 | companion object {
17 | private val logger = LoggerFactory.getLogger(BookingTools::class.java)
18 | }
19 |
20 | /** Get booking details, falling back to a “blank” record on error. */
21 | @Tool(description = "Get booking details")
22 | fun getBookingDetails(
23 | bookingNumber: String,
24 | firstName: String,
25 | lastName: String,
26 | toolContext: ToolContext
27 | ): BookingDetails =
28 | try {
29 | flightBookingService.getBookingDetails(bookingNumber, firstName, lastName)
30 | } catch (e: Exception) {
31 | logger.warn("Booking details: ${NestedExceptionUtils.getMostSpecificCause(e).message}")
32 | BookingDetails(bookingNumber, firstName, lastName, null, null, null, null, null, null)
33 | }
34 |
35 | /** Change a booking’s travel dates. */
36 | @Tool(description = "change booking dates")
37 | fun changeBooking(
38 | bookingNumber: String,
39 | firstName: String,
40 | lastName: String,
41 | newDate: String,
42 | from: String,
43 | to: String,
44 | toolContext: ToolContext
45 | ) {
46 | flightBookingService.changeBooking(bookingNumber, firstName, lastName, newDate, from, to)
47 | }
48 |
49 | /** Cancel an existing booking. */
50 | @Tool(description = "Cancel booking")
51 | fun cancelBooking(bookingNumber: String, firstName: String, lastName: String, toolContext: ToolContext) {
52 | flightBookingService.cancelBooking(bookingNumber, firstName, lastName)
53 | }
54 |
55 | /**
56 | * Initiate an interactive seat-change flow.
57 | * Blocks until the asynchronous seat selection is completed elsewhere.
58 | */
59 | @Tool(description = "Change seat")
60 | fun changeSeat(
61 | bookingNumber: String,
62 | firstName: String,
63 | lastName: String,
64 | seat: String,
65 | toolContext: ToolContext
66 | ) {
67 | logger.info("Changing seat for $bookingNumber to a $seat")
68 |
69 | val chatId = toolContext.context["chat_id"].toString()
70 | val future = CompletableFuture()
71 |
72 | // Ask every sink to emit a request for this chat session
73 | seatChangeQueue.seatChangeRequests.values.forEach { sink ->
74 | sink.tryEmitNext(SeatChangeQueue.SeatChangeRequest(chatId))
75 | }
76 |
77 | val seat = try {
78 | future.get() // blocks until completeSeatChangeRequest() supplies the seat
79 | } catch (e: Exception) {
80 | throw RuntimeException("Seat selection interrupted", e)
81 | }
82 |
83 | flightBookingService.changeSeat(bookingNumber, firstName, lastName, seat)
84 | }
85 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/services/CustomerSupportAssistant.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.services
2 |
3 | import org.springframework.ai.chat.client.ChatClient
4 | import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor
5 | import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor
6 | import org.springframework.ai.chat.memory.ChatMemory
7 | import org.springframework.ai.vectorstore.VectorStore
8 | import org.springframework.stereotype.Service
9 | import reactor.core.publisher.Flux
10 |
11 |
12 | @Service
13 | class CustomerSupportAssistant(
14 | chatClientBuilder: ChatClient.Builder,
15 | bookingTools: BookingTools,
16 | vectorStore: VectorStore,
17 | chatMemory: ChatMemory
18 | ) {
19 | private val chatClient: ChatClient = chatClientBuilder
20 | .defaultSystem(
21 | """
22 | You are a customer chat support agent of an airline named "Funnair".
23 | Respond in a friendly, helpful, and joyful manner.
24 | You are interacting with customers through an online chat system.
25 | Before answering a question about a booking or cancelling a booking, you MUST always
26 | get the following information from the user: booking number, customer first name and last name.
27 | If you can not retrieve the status of my flight, please just say "I am sorry, I can not find the booking details".
28 | Check the message history for booking details before asking the user.
29 | Before changing a booking you MUST ensure it is permitted by the terms.
30 | If there is a charge for the change, you MUST ask the user to consent before proceeding.
31 | Use the provided functions to fetch booking details, change bookings, and cancel bookings.
32 | """.trimIndent()
33 | )
34 | .defaultAdvisors(
35 | MessageChatMemoryAdvisor.builder(chatMemory).build(),
36 | QuestionAnswerAdvisor.builder(vectorStore).build()
37 | )
38 | .defaultTools(bookingTools)
39 | .build()
40 |
41 | fun chat(chatId: String, userMessage: String): Flux {
42 | return chatClient.prompt()
43 | .user(userMessage)
44 | .toolContext(mapOf("chat_id" to chatId))
45 | .advisors { it.param(ChatMemory.CONVERSATION_ID, chatId) }
46 | .stream()
47 | .content()
48 | // .asFlow() // Check flow
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/services/FlightBookingService.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.services
2 |
3 | import io.github.devcrocod.example.playground.data.Booking
4 | import io.github.devcrocod.example.playground.data.BookingClass
5 | import io.github.devcrocod.example.playground.data.BookingData
6 | import io.github.devcrocod.example.playground.data.BookingDetails
7 | import io.github.devcrocod.example.playground.data.BookingStatus
8 | import io.github.devcrocod.example.playground.data.Customer
9 | import org.springframework.stereotype.Service
10 | import java.time.LocalDate
11 | import kotlin.random.Random
12 |
13 |
14 | /**
15 | * In-memory flight-booking “backend” with demo data and simple business rules.
16 | */
17 | @Service
18 | class FlightBookingService {
19 | private val db: BookingData = BookingData()
20 |
21 | init {
22 | initDemoData()
23 | }
24 |
25 | /** Populate the database with five random demo bookings. */
26 | private fun initDemoData() {
27 | val firstNames = listOf("John", "Jane", "Michael", "Sarah", "Robert")
28 | val lastNames = listOf("Doe", "Smith", "Johnson", "Williams", "Taylor")
29 | val airportCodes =
30 | listOf("LAX", "SFO", "JFK", "LHR", "CDG", "ARN", "HEL", "TXL", "MUC", "FRA", "MAD", "FUN", "SJC")
31 | val random: Random = Random
32 |
33 | val customers = mutableListOf()
34 | val bookings = mutableListOf()
35 |
36 | repeat(5) {
37 | val customer = Customer(firstName = firstNames[it], lastName = lastNames[it])
38 |
39 | val booking = Booking(
40 | bookingNumber = "10${it + 1}",
41 | date = LocalDate.now().plusDays(2L * it),
42 | customer = customer,
43 | bookingStatus = BookingStatus.CONFIRMED,
44 | from = airportCodes.random(random),
45 | to = airportCodes.random(random),
46 | seatNumber = "${random.nextInt(19) + 1}A",
47 | bookingClass = BookingClass.entries.random(random)
48 | )
49 |
50 | customer.bookings.add(booking)
51 | customers += customer
52 | bookings += booking
53 | }
54 |
55 | // replace the database contents on every startup
56 | db.customers = customers
57 | db.bookings = bookings
58 | }
59 |
60 | fun getBookings(): List =
61 | db.bookings.map(::toBookingDetails)
62 |
63 | fun getBookingDetails(bookingNumber: String, firstName: String, lastName: String): BookingDetails =
64 | toBookingDetails(findBooking(bookingNumber, firstName, lastName))
65 |
66 | fun changeBooking(
67 | bookingNumber: String, firstName: String, lastName: String,
68 | newDate: String, from: String, to: String
69 | ) {
70 | val booking = findBooking(bookingNumber, firstName, lastName)
71 | require(booking.date.isAfter(LocalDate.now().plusDays(1))) {
72 | "Booking cannot be changed within 24 hours of the start date."
73 | }
74 |
75 | booking.apply {
76 | date = LocalDate.parse(newDate)
77 | this.from = from
78 | this.to = to
79 | }
80 | }
81 |
82 | fun cancelBooking(bookingNumber: String, firstName: String, lastName: String) {
83 | val booking = findBooking(bookingNumber, firstName, lastName)
84 |
85 | require(booking.date.isAfter(LocalDate.now().plusDays(2))) {
86 | "Booking cannot be cancelled within 48 hours of the start date."
87 | }
88 |
89 | booking.bookingStatus = BookingStatus.CANCELLED
90 | }
91 |
92 | fun changeSeat(bookingNumber: String, firstName: String, lastName: String, seatNumber: String) {
93 | findBooking(bookingNumber, firstName, lastName).seatNumber = seatNumber
94 | }
95 |
96 | private fun findBooking(bookingNumber: String, firstName: String, lastName: String): Booking =
97 | db.bookings.firstOrNull {
98 | it.bookingNumber.equals(bookingNumber, ignoreCase = true) &&
99 | it.customer.firstName.equals(firstName, ignoreCase = true) &&
100 | it.customer.lastName.equals(lastName, ignoreCase = true)
101 | } ?: throw IllegalArgumentException("Booking not found")
102 |
103 | private fun toBookingDetails(booking: Booking): BookingDetails =
104 | BookingDetails(
105 | booking.bookingNumber,
106 | booking.customer.firstName,
107 | booking.customer.lastName,
108 | booking.date,
109 | booking.bookingStatus,
110 | booking.from,
111 | booking.to,
112 | booking.seatNumber,
113 | booking.bookingClass.toString()
114 | )
115 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/kotlin/io/github/devcrocod/example/playground/services/SeatChangeQueue.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.playground.services
2 |
3 | import org.springframework.stereotype.Service
4 | import reactor.core.publisher.Sinks
5 | import java.util.concurrent.CompletableFuture
6 | import java.util.concurrent.ConcurrentHashMap
7 |
8 |
9 | /**
10 | * Manages seat-change requests flowing between the chat agent and the UI.
11 | */
12 | @Service
13 | class SeatChangeQueue {
14 |
15 | /** Simple “envelope” for a seat-change request. */
16 | data class SeatChangeRequest(val requestId: String)
17 |
18 | /** Futures that are awaiting a seat choice keyed by chat/session ID. */
19 | val pendingRequests = ConcurrentHashMap>()
20 |
21 | /** Reactive sinks that broadcast seat-change requests to subscribers, keyed by chat/session ID. */
22 | val seatChangeRequests = ConcurrentHashMap>()
23 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=flight-booking-assistant
2 |
3 | # spring.ai.chat.client.enabled=false
4 |
5 | spring.threads.virtual.enabled=true
6 |
7 |
8 | ###################
9 | # Anthropic Claude 3
10 | ###################
11 |
12 | #spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
13 | #spring.ai.anthropic.chat.options.model=claude-3-7-sonnet-20250219
14 |
15 |
16 | ###################
17 | # Groq
18 | ###################
19 |
20 | # spring.ai.openai.api-key=${GROQ_API_KEY}
21 | # spring.ai.openai.base-url=https://api.groq.com/openai
22 | # spring.ai.openai.chat.options.model=llama3-70b-8192
23 |
24 |
25 | ###################
26 | # OpenAI
27 | ###################
28 | spring.ai.openai.api-key=${OPENAI_API_KEY}
29 | spring.ai.openai.chat.options.model=gpt-4o
30 |
31 | # spring.ai.openai.chat.options.functions=getBookingDetails,changeBooking,cancelBooking
32 |
33 | ###################
34 | # Chroma
35 | ###################
36 | spring.ai.vectorstore.chroma.client.host=http://localhost
37 | spring.ai.vectorstore.chroma.client.port=8000
38 | spring.ai.vectorstore.chroma.initialize-schema=true
39 |
40 |
41 | # Disable the OpenAI embedding when the local huggingface embedding (e.g. spring-ai-transformers-spring-boot-starter) is used.
42 | # spring.ai.openai.embedding.enabled=false
43 |
44 | ###################
45 | # Azure OpenAI
46 | ###################
47 | # spring.ai.azure.openai.api-key=${AZURE_OPENAI_API_KEY}
48 | # spring.ai.azure.openai.endpoint=${AZURE_OPENAI_ENDPOINT}
49 | # spring.ai.azure.openai.chat.options.deployment-name=gpt-4o
50 |
51 | ###################
52 | # Mistral AI
53 | ###################
54 |
55 | # spring.ai.mistralai.api-key=${MISTRAL_AI_API_KEY}
56 | # spring.ai.mistralai.chat.options.model=mistral-small-latest
57 |
58 | # spring.ai.mistralai.chat.options.model=mistral-small-latest
59 | # spring.ai.mistralai.chat.options.functions=getBookingDetails,changeBooking,cancelBooking
60 | # # spring.ai.retry.on-client-errors=true
61 | # # spring.ai.retry.exclude-on-http-codes=429
62 |
63 | ###################
64 | # Vertex AI Gemini
65 | ###################
66 |
67 | # spring.ai.vertex.ai.gemini.project-id=${VERTEX_AI_GEMINI_PROJECT_ID}
68 | # spring.ai.vertex.ai.gemini.location=${VERTEX_AI_GEMINI_LOCATION}
69 | # spring.ai.vertex.ai.gemini.chat.options.model=gemini-1.5-pro-001
70 | # # spring.ai.vertex.ai.gemini.chat.options.model=gemini-1.5-flash-001
71 | # spring.ai.vertex.ai.gemini.chat.options.transport-type=REST
72 |
73 | # spring.ai.vertex.ai.gemini.chat.options.functions=getBookingDetails,changeBooking,cancelBooking
74 |
75 | ###################
76 | # Milvus Vector Store
77 | ###################
78 | # Change the dimentions to 384 if the local huggingface embedding (e.g. spring-ai-transformers-spring-boot-starter) is used.
79 | # spring.ai.vectorstore.milvus.embedding-dimension=384
80 |
81 | ###################
82 | # PGVector
83 | ###################
84 | # spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
85 | # spring.datasource.username=postgres
86 | # spring.datasource.password=postgres
87 | # spring.ai.vectorstore.pgvector.initialize-schema=true
88 |
89 | ###################
90 | # QDrant
91 | ###################
92 | # spring.ai.vectorstore.qdrant.host=localhost
93 | # spring.ai.vectorstore.qdrant.port=6334
94 |
95 |
96 |
97 | # Enable context propagation for Reactor (required for Observability with streaming)
98 | spring.reactor.context-propagation=auto
99 |
100 |
101 | ## metrics
102 | management.endpoints.web.exposure.include=health, info, metrics, prometheus
103 | management.metrics.distribution.percentiles-histogram.http.server.requests=true
104 | management.observations.key-values.application=flight-booking-assistant
105 |
106 | ## percentiles histogram
107 | management.metrics.distribution.percentiles-histogram.gen_ai.client.operation=true
108 | management.metrics.distribution.percentiles-histogram.db.vector.client.operation=true
109 | management.metrics.distribution.percentiles-histogram.spring.ai.chat.client=true
110 | management.metrics.distribution.percentiles-histogram.spring.ai.tool=true
111 |
112 | ## logging
113 | # logging.pattern.correlation=[${spring.application.name:},%X{traceId:-},%X{spanId:-}]
114 |
115 | ## tracing
116 | management.tracing.sampling.probability=1.0
117 | management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans
118 |
119 |
120 | # disable PDF reader logs
121 | logging.level.com.zaxxer.hikari=ERROR
122 | logging.level.org.springframework.ai=ERROR
123 | logging.level.org.apache.pdfbox.pdmodel.font=OFF
124 | logging.level.org.apache.fontbox.ttf=OFF
125 | logging.level.org.atmosphere=OFF
126 |
127 |
128 | ######################################
129 | # Spring AI observability settings
130 | ######################################
131 |
132 | spring.ai.tools.observations.include-content=true
133 |
134 | ## Include the Chatclient input in observations
135 | spring.ai.chat.client.observation.log-input=true
136 |
137 | ## Include the VectorStore query and response in observations
138 | spring.ai.vectorstore.observations.log-query-response=true
139 |
140 | ## Include prompt and completion contents in observations
141 | spring.ai.chat.observations.log-prompt=true
142 | spring.ai.chat.observations.log-completion=true
143 |
144 | ## Include error logging in observations (note: not needed for Spring Web apps)
145 | spring.ai.chat.observations.include-error-logging=true
146 |
147 | logging.level.org.springframework.ai.chat.observation=DEBUG
148 |
149 | ##
150 | # spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config2.json
151 | # spring.ai.mcp.client.toolcallback.enabled=true
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/resources/logback-spring.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | http://localhost:3100/loki/api/v1/push
10 |
11 |
12 |
15 |
16 | ${FILE_LOG_PATTERN}
17 |
18 | true
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/resources/mcp-servers-config2.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcpServers": {
3 | "brave-search": {
4 | "command": "npx",
5 | "args": [
6 | "-y",
7 | "@modelcontextprotocol/server-brave-search"
8 | ],
9 | "env": {
10 | }
11 | },
12 | "filesystem": {
13 | "command": "npx",
14 | "args": [
15 | "-y",
16 | "@modelcontextprotocol/server-filesystem",
17 | "/Users/christiantzolov/Desktop/tmp"
18 | ]
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/main/resources/rag/terms-of-service.txt:
--------------------------------------------------------------------------------
1 | These Terms of Service govern your experience with Funnair. By booking a flight, you agree to these terms.
2 |
3 | 1. Booking Flights
4 | - Book via our website or mobile app.
5 | - Full payment required at booking.
6 | - Ensure accuracy of personal information (Name, ID, etc.) as corrections may incur a $25 fee.
7 |
8 | 2. Changing Bookings
9 | - Changes allowed up to 24 hours before flight.
10 | - Change via online or contact our support.
11 | - Change fee: $50 for Economy, $30 for Premium Economy, Free for Business Class.
12 |
13 | 3. Cancelling Bookings
14 | - Cancel up to 48 hours before flight.
15 | - Cancellation fees: $75 for Economy, $50 for Premium Economy, $25 for Business Class.
16 | - Refunds processed within 7 business days.
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/src/test/resources/standalone_embed.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Licensed to the LF AI & Data foundation under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 |
19 | run_embed() {
20 | cat << EOF > embedEtcd.yaml
21 | listen-client-urls: http://0.0.0.0:2379
22 | advertise-client-urls: http://0.0.0.0:2379
23 | EOF
24 |
25 | sudo docker run -d \
26 | --name milvus-standalone \
27 | --security-opt seccomp:unconfined \
28 | -e ETCD_USE_EMBED=true \
29 | -e ETCD_DATA_DIR=/var/lib/milvus/etcd \
30 | -e ETCD_CONFIG_PATH=/milvus/configs/embedEtcd.yaml \
31 | -e COMMON_STORAGETYPE=local \
32 | -v $(pwd)/volumes/milvus:/var/lib/milvus \
33 | -v $(pwd)/embedEtcd.yaml:/milvus/configs/embedEtcd.yaml \
34 | -p 19530:19530 \
35 | -p 9091:9091 \
36 | -p 2379:2379 \
37 | --health-cmd="curl -f http://localhost:9091/healthz" \
38 | --health-interval=30s \
39 | --health-start-period=90s \
40 | --health-timeout=20s \
41 | --health-retries=3 \
42 | milvusdb/milvus:v2.3.9 \
43 | milvus run standalone 1> /dev/null
44 | }
45 |
46 | wait_for_milvus_running() {
47 | echo "Wait for Milvus Starting..."
48 | while true
49 | do
50 | res=`sudo docker ps|grep milvus-standalone|grep healthy|wc -l`
51 | if [ $res -eq 1 ]
52 | then
53 | echo "Start successfully."
54 | break
55 | fi
56 | sleep 1
57 | done
58 | }
59 |
60 | start() {
61 | res=`sudo docker ps|grep milvus-standalone|grep healthy|wc -l`
62 | if [ $res -eq 1 ]
63 | then
64 | echo "Milvus is running."
65 | exit 0
66 | fi
67 |
68 | res=`sudo docker ps -a|grep milvus-standalone|wc -l`
69 | if [ $res -eq 1 ]
70 | then
71 | sudo docker start milvus-standalone 1> /dev/null
72 | else
73 | run_embed
74 | fi
75 |
76 | if [ $? -ne 0 ]
77 | then
78 | echo "Start failed."
79 | exit 1
80 | fi
81 |
82 | wait_for_milvus_running
83 | }
84 |
85 | stop() {
86 | sudo docker stop milvus-standalone 1> /dev/null
87 |
88 | if [ $? -ne 0 ]
89 | then
90 | echo "Stop failed."
91 | exit 1
92 | fi
93 | echo "Stop successfully."
94 |
95 | }
96 |
97 | delete() {
98 | res=`sudo docker ps|grep milvus-standalone|wc -l`
99 | if [ $res -eq 1 ]
100 | then
101 | echo "Please stop Milvus service before delete."
102 | exit 1
103 | fi
104 | sudo docker rm milvus-standalone 1> /dev/null
105 | if [ $? -ne 0 ]
106 | then
107 | echo "Delete failed."
108 | exit 1
109 | fi
110 | sudo rm -rf $(pwd)/volumes
111 | sudo rm -rf $(pwd)/embedEtcd.yaml
112 | echo "Delete successfully."
113 | }
114 |
115 |
116 | case $1 in
117 | start)
118 | start
119 | ;;
120 | stop)
121 | stop
122 | ;;
123 | delete)
124 | delete
125 | ;;
126 | *)
127 | echo "please use bash standalone_embed.sh start|stop|delete"
128 | ;;
129 | esac
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/tsconfig.json:
--------------------------------------------------------------------------------
1 | // This TypeScript configuration file is generated by vaadin-maven-plugin.
2 | // This is needed for TypeScript compiler to compile your TypeScript code in the project.
3 | // It is recommended to commit this file to the VCS.
4 | // You might want to change the configurations to fit your preferences
5 | // For more information about the configurations, please refer to http://www.typescriptlang.org/docs/handbook/tsconfig-json.html
6 | {
7 | "_version": "9.1",
8 | "compilerOptions": {
9 | "sourceMap": true,
10 | "jsx": "react-jsx",
11 | "inlineSources": true,
12 | "module": "esNext",
13 | "target": "es2020",
14 | "moduleResolution": "bundler",
15 | "strict": true,
16 | "skipLibCheck": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "noImplicitReturns": true,
19 | "noImplicitAny": true,
20 | "noImplicitThis": true,
21 | "noUnusedLocals": false,
22 | "noUnusedParameters": false,
23 | "experimentalDecorators": true,
24 | "useDefineForClassFields": false,
25 | "baseUrl": "src/main/frontend",
26 | "paths": {
27 | "@vaadin/flow-frontend": ["generated/jar-resources"],
28 | "@vaadin/flow-frontend/*": ["generated/jar-resources/*"],
29 | "Frontend/*": ["*"]
30 | }
31 | },
32 | "include": [
33 | "src/main/frontend/**/*",
34 | "types.d.ts"
35 | ],
36 | "exclude": [
37 | "src/main/frontend/generated/jar-resources/**"
38 | ]
39 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.module.css' {
2 | declare const styles: Record;
3 | export default styles;
4 | }
5 | declare module '*.module.sass' {
6 | declare const styles: Record;
7 | export default styles;
8 | }
9 | declare module '*.module.scss' {
10 | declare const styles: Record;
11 | export default styles;
12 | }
13 | declare module '*.module.less' {
14 | declare const classes: Record;
15 | export default classes;
16 | }
17 | declare module '*.module.styl' {
18 | declare const classes: Record;
19 | export default classes;
20 | }
21 |
22 | /* CSS FILES */
23 | declare module '*.css';
24 | declare module '*.sass';
25 | declare module '*.scss';
26 | declare module '*.less';
27 | declare module '*.styl';
28 |
29 | /* IMAGES */
30 | declare module '*.svg' {
31 | const ref: string;
32 | export default ref;
33 | }
34 | declare module '*.bmp' {
35 | const ref: string;
36 | export default ref;
37 | }
38 | declare module '*.gif' {
39 | const ref: string;
40 | export default ref;
41 | }
42 | declare module '*.jpg' {
43 | const ref: string;
44 | export default ref;
45 | }
46 | declare module '*.jpeg' {
47 | const ref: string;
48 | export default ref;
49 | }
50 | declare module '*.png' {
51 | const ref: string;
52 | export default ref;
53 | }
54 | declare module '*.avif' {
55 | const ref: string;
56 | export default ref;
57 | }
58 | declare module '*.webp' {
59 | const ref: string;
60 | export default ref;
61 | }
62 | declare module '*.css?inline' {
63 | import type { CSSResultGroup } from 'lit';
64 | const content: CSSResultGroup;
65 | export default content;
66 | }
67 |
68 | declare module 'csstype' {
69 | interface Properties {
70 | [index: `--${string}`]: any;
71 | }
72 | }
--------------------------------------------------------------------------------
/projects/spring-ai/playground-flight-booking/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { UserConfigFn } from 'vite';
2 | import { overrideVaadinConfig } from './vite.generated';
3 |
4 | const customConfig: UserConfigFn = (env) => ({
5 | // Here you can add custom Vite parameters
6 | // https://vitejs.dev/config/
7 | });
8 |
9 | export default overrideVaadinConfig(customConfig);
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 | /target/
8 |
9 | ### STS ###
10 | .apt_generated
11 | .classpath
12 | .factorypath
13 | .project
14 | .settings
15 | .springBeans
16 | .sts4-cache
17 | bin/
18 | !**/src/main/**/bin/
19 | !**/src/test/**/bin/
20 |
21 | ### IntelliJ IDEA ###
22 | .idea
23 | *.iws
24 | *.iml
25 | *.ipr
26 | out/
27 | !**/src/main/**/out/
28 | !**/src/test/**/out/
29 |
30 | ### NetBeans ###
31 | /nbproject/private/
32 | /nbbuild/
33 | /dist/
34 | /nbdist/
35 | /.nb-gradle/
36 |
37 | ### VS Code ###
38 | .vscode/
39 |
40 | ### Kotlin ###
41 | .kotlin
42 |
43 | # The following files are automatically generated/updated
44 | node_modules/
45 | frontend/generated/
46 | .npmrc
47 | vite.generated.ts
48 |
49 |
50 | # Eclipse store
51 | storage/
52 |
53 | !**/src/test/resources/volumes
54 |
55 | src/main/bundles
56 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "2.1.21"
3 | kotlin("plugin.spring") version "2.1.21"
4 | id("org.springframework.boot") version "3.5.0"
5 | id("io.spring.dependency-management") version "1.1.7"
6 | }
7 |
8 | group = "io.github.devcrocod.example"
9 | version = "0.0.1-SNAPSHOT"
10 |
11 | java {
12 | toolchain {
13 | languageVersion = JavaLanguageVersion.of(17)
14 | }
15 | }
16 |
17 | repositories {
18 | mavenCentral()
19 | }
20 |
21 | val springAiVersion = "1.0.0"
22 |
23 | dependencyManagement {
24 | imports {
25 | mavenBom("org.springframework.ai:spring-ai-bom:$springAiVersion")
26 | }
27 | }
28 |
29 | dependencies {
30 | implementation("org.springframework.boot:spring-boot-starter-web")
31 | implementation("org.springframework.boot:spring-boot-starter-actuator")
32 | implementation("org.springframework.ai:spring-ai-starter-model-openai")
33 | implementation("org.springframework.ai:spring-ai-advisors-vector-store")
34 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
35 |
36 | testImplementation("org.springframework.boot:spring-boot-starter-test")
37 | }
38 |
39 | kotlin {
40 | compilerOptions {
41 | freeCompilerArgs.addAll("-Xjsr305=strict")
42 | }
43 | }
44 |
45 | tasks.withType {
46 | useJUnitPlatform()
47 | }
48 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/spring-ai-examples/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "spring-ai-examples"
2 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/Application.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class Application
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/Config.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example
2 |
3 | import org.springframework.ai.chat.client.ChatClient
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 |
7 | @Configuration
8 | class Config {
9 |
10 | @Bean
11 | fun chatClient(builder: ChatClient.Builder): ChatClient = builder.build()
12 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/helloworld/SimpleAiController.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.helloworld
2 |
3 | import org.springframework.ai.chat.client.ChatClient
4 | import org.springframework.web.bind.annotation.GetMapping
5 | import org.springframework.web.bind.annotation.RequestParam
6 | import org.springframework.web.bind.annotation.RestController
7 |
8 | @RestController
9 | class SimpleAiController(private val chatClient: ChatClient) {
10 |
11 | @GetMapping("/ai/simple")
12 | fun generation(
13 | @RequestParam(value = "message", defaultValue = "Tell me a joke") message: String
14 | ): Map {
15 | return mapOf("generation" to chatClient.prompt().user(message).call().content()!!)
16 | }
17 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/output/ActorsFilms.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.output
2 |
3 |
4 | data class ActorsFilms constructor(
5 | val actor: String,
6 | val movies: List // works only with jackson annotation
7 | ) {
8 | override fun toString(): String = "ActorsFilms{actor='$actor', movies=$movies}"
9 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/output/OutputParserController.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.output
2 |
3 | import org.springframework.ai.chat.client.ChatClient
4 | import org.springframework.ai.chat.client.entity
5 | import org.springframework.ai.chat.prompt.PromptTemplate
6 | import org.springframework.web.bind.annotation.GetMapping
7 | import org.springframework.web.bind.annotation.RequestParam
8 | import org.springframework.web.bind.annotation.RestController
9 |
10 | @RestController
11 | class OutputParserController(private val chatClient: ChatClient) {
12 | @GetMapping("ai/output")
13 | fun generate(@RequestParam(value = "actor", defaultValue = "Jeff Bridges") actor: String): ActorsFilms {
14 | val userMessage = """
15 | Generate the filmography for the actor $actor.
16 | """.trimIndent()
17 |
18 | val promptTemplate = PromptTemplate.builder()
19 | .template(userMessage)
20 | .variables(mapOf("actor" to actor))
21 | .build()
22 | val prompt = promptTemplate.create()
23 | val generation = chatClient.prompt(prompt).call().entity()
24 | return generation
25 | }
26 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/prompttemplate/PromptTemplateController.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.prompttemplate
2 |
3 | import org.springframework.ai.chat.client.ChatClient
4 | import org.springframework.ai.chat.messages.AssistantMessage
5 | import org.springframework.ai.chat.prompt.PromptTemplate
6 | import org.springframework.beans.factory.annotation.Value
7 | import org.springframework.web.bind.annotation.RestController
8 | import org.springframework.core.io.Resource
9 | import org.springframework.web.bind.annotation.GetMapping
10 | import org.springframework.web.bind.annotation.RequestParam
11 |
12 | @RestController
13 | class PromptTemplateController(private val chatClient: ChatClient) {
14 | @Value("classpath:/prompts/joke-prompt.st")
15 | lateinit var jokeResource: Resource
16 |
17 | @GetMapping("/ai/prompt")
18 | fun completion(
19 | @RequestParam(value = "adjective", defaultValue = "funny") adjective: String,
20 | @RequestParam(value = "topic", defaultValue = "cows") topic: String
21 | ): AssistantMessage {
22 | val promptTemplate = PromptTemplate(jokeResource)
23 | val prompt = promptTemplate.create(mapOf("adjective" to adjective, "topic" to topic))
24 | return chatClient.prompt(prompt).call().chatResponse()!!.result.output
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/rag/RagController.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.rag
2 |
3 | import org.springframework.ai.chat.messages.AssistantMessage
4 | import org.springframework.web.bind.annotation.GetMapping
5 | import org.springframework.web.bind.annotation.RequestParam
6 | import org.springframework.web.bind.annotation.RestController
7 |
8 | @RestController
9 | class RagController(private val ragService: RagService) {
10 |
11 | @GetMapping("/ai/rag")
12 | fun generate(
13 | @RequestParam(value = "message", defaultValue = "What bike is good for city commuting?") message: String
14 | ): AssistantMessage {
15 | return ragService.retrieve(message)
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/rag/RagService.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.rag
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.ai.chat.client.ChatClient
6 | import org.springframework.ai.chat.messages.AssistantMessage
7 | import org.springframework.ai.chat.messages.Message
8 | import org.springframework.ai.chat.messages.UserMessage
9 | import org.springframework.ai.chat.prompt.Prompt
10 | import org.springframework.ai.chat.prompt.SystemPromptTemplate
11 | import org.springframework.ai.document.Document
12 | import org.springframework.ai.embedding.EmbeddingModel
13 | import org.springframework.ai.reader.JsonReader
14 | import org.springframework.ai.vectorstore.SimpleVectorStore
15 | import org.springframework.beans.factory.annotation.Value
16 | import org.springframework.core.io.Resource
17 | import org.springframework.stereotype.Service
18 |
19 | @Service
20 | class RagService(
21 | private val chatClient: ChatClient,
22 | private val embeddingClient: EmbeddingModel
23 | ) {
24 | @Value("classpath:/data/bikes.json")
25 | private lateinit var bikesResource: Resource
26 |
27 | @Value("classpath:/prompts/system-qa.st")
28 | private lateinit var systemBikePrompt: Resource
29 |
30 | companion object {
31 | private val logger: Logger = LoggerFactory.getLogger(RagService::class.java)
32 | }
33 |
34 | fun retrieve(message: String): AssistantMessage {
35 | // Step 1 - Load JSON document as Documents
36 | logger.info("Loading JSON as Documents")
37 | val jsonReader = JsonReader(bikesResource, "name", "price", "shortDescription", "description")
38 | val documents = jsonReader.get()
39 | logger.info("JSON loaded as Documents")
40 |
41 | // Step 2 - Create embeddings and save to vector store
42 | logger.info("Creating Embeddings...")
43 | val vectorStore = SimpleVectorStore.builder(embeddingClient).build()
44 | vectorStore.add(documents)
45 | logger.info("Embeddings created.")
46 |
47 | // Step 3 retrieve related documents to query
48 | logger.info("Retrieving relevant documents")
49 | val similarDocuments = vectorStore.similaritySearch(message)!!
50 | logger.info("Found ${similarDocuments.size} relevant documents.")
51 |
52 | // Step 4 Embed documents into SystemMessage with the `system-qa.st` prompt
53 | // template
54 | val systemMessage = getSystemMessage(similarDocuments)
55 | val userMessage = UserMessage(message)
56 |
57 | // Step 5 - Ask the AI model
58 | logger.info("Asking AI model to reply to question.")
59 | val prompt = Prompt(listOf(systemMessage, userMessage))
60 | logger.info(prompt.toString())
61 |
62 | val chatResponse = chatClient.prompt(prompt).call().chatResponse()!!
63 | logger.info("AI responded.")
64 |
65 | logger.info(chatResponse.result.output.text)
66 | return chatResponse.result.output!!
67 | }
68 |
69 | private fun getSystemMessage(similarDocuments: List): Message {
70 | val documents = similarDocuments.joinToString("\n") { it.text ?: "" }
71 | val systemPromptTemplate = SystemPromptTemplate(systemBikePrompt)
72 | return systemPromptTemplate.createMessage(mapOf("documents" to documents))
73 | }
74 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/rag/config/RagConfiguration.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.rag.config
2 |
3 | import io.github.devcrocod.example.rag.RagService
4 | import org.springframework.ai.chat.client.ChatClient
5 | import org.springframework.ai.embedding.EmbeddingModel
6 | import org.springframework.context.annotation.Bean
7 | import org.springframework.context.annotation.Configuration
8 |
9 | @Configuration
10 | class RagConfiguration {
11 |
12 | @Bean
13 | fun ragService(chatClient: ChatClient, embeddingClient: EmbeddingModel): RagService =
14 | RagService(chatClient, embeddingClient)
15 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/roles/RoleController.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.roles
2 |
3 | import org.springframework.ai.chat.client.ChatClient
4 | import org.springframework.ai.chat.messages.AssistantMessage
5 | import org.springframework.ai.chat.messages.UserMessage
6 | import org.springframework.ai.chat.prompt.Prompt
7 | import org.springframework.ai.chat.prompt.SystemPromptTemplate
8 | import org.springframework.beans.factory.annotation.Value
9 | import org.springframework.core.io.Resource
10 | import org.springframework.web.bind.annotation.GetMapping
11 | import org.springframework.web.bind.annotation.RequestParam
12 | import org.springframework.web.bind.annotation.RestController
13 |
14 | //@RestController
15 | //public class RoleController {
16 | //
17 | // private final ChatClient chatClient;
18 | //
19 | // @Value("classpath:/prompts/system-message.st")
20 | // private Resource systemResource;
21 | //
22 | // @Autowired
23 | // public RoleController(ChatClient chatClient) {
24 | // this.chatClient = chatClient;
25 | // }
26 | //
27 | // @GetMapping("/ai/roles")
28 | // public AssistantMessage generate(@RequestParam(value = "message",
29 | // defaultValue = "Tell me about three famous pirates from the Golden Age of Piracy and why they did. Write at least a sentence for each pirate.") String message,
30 | // @RequestParam(value = "name", defaultValue = "Bob") String name,
31 | // @RequestParam(value = "voice", defaultValue = "pirate") String voice) {
32 | // UserMessage userMessage = new UserMessage(message);
33 | // SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
34 | // Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
35 | // Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
36 | // return chatClient.call(prompt).getResult().getOutput();
37 | // }
38 | //
39 | //}
40 |
41 | @RestController
42 | class RoleController(private val chatClient: ChatClient) {
43 |
44 | @Value("classpath:/prompts/system-message.st")
45 | private lateinit var systemResource: Resource
46 |
47 | @GetMapping("/ai/roles")
48 | fun generate(
49 | @RequestParam(
50 | value = "message",
51 | defaultValue = "Tell me about three famous pirates from the Golden Age of Piracy and why they did. Write at least a sentence for each pirate."
52 | ) message: String,
53 | @RequestParam(value = "name", defaultValue = "Bob") name: String,
54 | @RequestParam(value = "voice", defaultValue = "pirate") voice: String
55 | ): AssistantMessage {
56 | val userMessage = UserMessage(message)
57 | val systemPromptTemplate = SystemPromptTemplate(systemResource)
58 | val systemMessage = systemPromptTemplate.createMessage(mapOf("name" to name, "voice" to voice))
59 | val prompt = Prompt(listOf(userMessage, systemMessage))
60 | return chatClient.prompt(prompt).call().chatResponse()!!.result.output
61 | }
62 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/stuff/Completion.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.stuff
2 |
3 |
4 | data class Completion constructor(
5 | val completion: String?
6 | ) // exception with null
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/kotlin/io/github/devcrocod/example/stuff/StuffController.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.stuff
2 |
3 | import org.springframework.ai.chat.client.ChatClient
4 | import org.springframework.ai.chat.prompt.PromptTemplate
5 | import org.springframework.beans.factory.annotation.Value
6 | import org.springframework.core.io.Resource
7 | import org.springframework.web.bind.annotation.GetMapping
8 | import org.springframework.web.bind.annotation.RequestParam
9 | import org.springframework.web.bind.annotation.RestController
10 |
11 | @RestController
12 | class StuffController(private val chatClient: ChatClient) {
13 |
14 | @Value("classpath:/docs/wikipedia-curling.md")
15 | private lateinit var docsToStuffResource: Resource
16 |
17 | @Value("classpath:/prompts/qa-prompt.st")
18 | private lateinit var qaPromptResource: Resource
19 |
20 | @GetMapping("/ai/stuff")
21 | fun completion(
22 | @RequestParam(
23 | value = "message",
24 | defaultValue = "Which athletes won the mixed doubles gold medal in curling at the 2022 Winter Olympics?"
25 | ) message: String,
26 | @RequestParam(value = "stuffit", defaultValue = "false") stuffit: Boolean
27 | ): Completion {
28 | val promptTemplate = PromptTemplate(qaPromptResource)
29 | val map = mutableMapOf("question" to message)
30 | if (stuffit) {
31 | map["context"] = docsToStuffResource
32 | } else {
33 | map["context"] = ""
34 | }
35 | val prompt = promptTemplate.create(map)
36 | return Completion(chatClient.prompt(prompt).call().content())
37 | // return chatClient.prompt(prompt).call().entity(Completion::class.java)
38 | }
39 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=spring-ai-examples
2 |
3 | spring.ai.openai.api-key=${OPENAI_API_KEY}
4 | spring.ai.openai.chat.options.model=gpt-4o-mini
5 | spring.ai.openai.embedding.options.model=text-embedding-ada-002
6 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/resources/prompts/joke-prompt.st:
--------------------------------------------------------------------------------
1 | Tell me a {adjective} joke about {topic}
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/resources/prompts/qa-prompt.st:
--------------------------------------------------------------------------------
1 | Use the following pieces of context to answer the question at the end.
2 | If you don't know the answer, just say that you don't know, don't try to make up an answer.
3 |
4 | {context}
5 |
6 | Question: {question}
7 | Helpful Answer:
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/resources/prompts/system-message.st:
--------------------------------------------------------------------------------
1 | You are a helpful AI assistant.
2 | You are an AI assistant that helps people find information.
3 | Your name is {name}
4 | You should reply to the user's request with your name and also in the style of a {voice}.
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-examples/src/main/resources/prompts/system-qa.st:
--------------------------------------------------------------------------------
1 | You're assisting with questions about products in a bicycle catalog.
2 | Use the information from the DOCUMENTS section to provide accurate answers.
3 | If the answer involves referring to the price or the dimension of the bicycle, include the bicycle name in the response.
4 | If unsure, simply state that you don't know.
5 |
6 | DOCUMENTS:
7 | {documents}
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-mcp-server-example/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 | /target/
8 |
9 | ### STS ###
10 | .apt_generated
11 | .classpath
12 | .factorypath
13 | .project
14 | .settings
15 | .springBeans
16 | .sts4-cache
17 | bin/
18 | !**/src/main/**/bin/
19 | !**/src/test/**/bin/
20 |
21 | ### IntelliJ IDEA ###
22 | .idea
23 | *.iws
24 | *.iml
25 | *.ipr
26 | out/
27 | !**/src/main/**/out/
28 | !**/src/test/**/out/
29 |
30 | ### NetBeans ###
31 | /nbproject/private/
32 | /nbbuild/
33 | /dist/
34 | /nbdist/
35 | /.nb-gradle/
36 |
37 | ### VS Code ###
38 | .vscode/
39 |
40 | ### Kotlin ###
41 | .kotlin
42 |
43 | # The following files are automatically generated/updated
44 | node_modules/
45 | frontend/generated/
46 | .npmrc
47 | vite.generated.ts
48 |
49 |
50 | # Eclipse store
51 | storage/
52 |
53 | !**/src/test/resources/volumes
54 |
55 | src/main/bundles
56 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-mcp-server-example/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "2.1.21"
3 | kotlin("plugin.spring") version "2.1.21"
4 | id("org.springframework.boot") version "3.5.0"
5 | id("io.spring.dependency-management") version "1.1.7"
6 | application
7 | }
8 |
9 | application {
10 | mainClass.set("MainKt")
11 | }
12 |
13 | group = "io.github.devcrocod.example"
14 | version = "0.1.0"
15 |
16 | repositories {
17 | mavenCentral()
18 | }
19 |
20 | dependencies {
21 | implementation("org.springframework.boot:spring-boot-starter-web")
22 | implementation("org.springframework.ai:spring-ai-mcp-server-spring-boot-starter:1.0.0-M6")
23 |
24 | }
25 |
26 | tasks.test {
27 | useJUnitPlatform()
28 | }
29 |
30 | kotlin {
31 | jvmToolchain(21)
32 | }
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-mcp-server-example/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-mcp-server-example/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/spring-ai-mcp-server-example/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-mcp-server-example/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-mcp-server-example/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-mcp-server-example/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "spring-ai-mcp-server-example"
2 |
--------------------------------------------------------------------------------
/projects/spring-ai/spring-ai-mcp-server-example/src/main/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | package io.github.devcrocod.example.mcpserver
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import io.modelcontextprotocol.server.McpServer
5 | import io.modelcontextprotocol.server.McpServerFeatures
6 | import io.modelcontextprotocol.server.McpSyncServer
7 | import io.modelcontextprotocol.server.transport.HttpServletSseServerTransport
8 | import io.modelcontextprotocol.server.transport.StdioServerTransport
9 | import io.modelcontextprotocol.spec.McpSchema
10 |
11 |
12 | fun main(args: Array) {
13 | val command = args.firstOrNull() ?: "--sse-server"
14 | val port = args.getOrNull(1)?.toIntOrNull() ?: 3001
15 | when (command) {
16 | "--stdio" -> `run mcp server using stdio`()
17 | "--sse-server" -> `run sse mcp server with plain configuration`(port)
18 | else -> {
19 | System.err.println("Unknown command: $command")
20 | }
21 | }
22 | }
23 |
24 | fun McpSyncServer.configureServer(): McpSyncServer {
25 | val kotlinPrompt = McpServerFeatures.SyncPromptRegistration(
26 | McpSchema.Prompt("Kotlin Developer", "Develop small kotlin applications", null)
27 | ) { request ->
28 | McpSchema.GetPromptResult(
29 | "Kotlin project development prompt",
30 | listOf(
31 | McpSchema.PromptMessage(
32 | McpSchema.Role.ASSISTANT,
33 | McpSchema.TextContent("I will help you develop a Kotlin project.")
34 | )
35 | )
36 | )
37 | }
38 |
39 | val calculatorTool = McpServerFeatures.SyncToolRegistration(
40 | McpSchema.Tool("testTool", "A test tool", """{"type":"string"}""")
41 | ) { _ ->
42 | McpSchema.CallToolResult(listOf(McpSchema.TextContent("Hello world!")), false)
43 | }
44 |
45 | val searchResource = McpServerFeatures.SyncResourceRegistration(
46 | McpSchema.Resource("https://search.com/", "Web Search", "Web search engine", "text/html", null)
47 | ) { request ->
48 | McpSchema.ReadResourceResult(
49 | listOf(McpSchema.TextResourceContents("Search results", request.uri, "text/html"))
50 | )
51 | }
52 |
53 | this.addPrompt(kotlinPrompt)
54 | this.addTool(calculatorTool)
55 | this.addResource(searchResource)
56 |
57 | return this
58 | }
59 |
60 | fun `run mcp server using stdio`() {
61 | val server = McpServer.sync(StdioServerTransport())
62 | .serverInfo("mcp test server", "0.1.0")
63 | .capabilities(
64 | McpSchema.ServerCapabilities.builder()
65 | .prompts(true)
66 | .resources(true, true)
67 | .tools(true)
68 | .build()
69 | )
70 | .build()
71 |
72 | server.configureServer()
73 |
74 | println("Server running on stdio")
75 | }
76 |
77 | fun `run sse mcp server with plain configuration`(port: Int) {
78 | println("Starting SSE server on port $port.")
79 | println("Use inspector to connect to the http://localhost:$port/sse")
80 |
81 | val transport = HttpServletSseServerTransport(ObjectMapper(), "/message", "/sse")
82 | val server = McpServer.sync(transport)
83 | .serverInfo("mcp test server", "0.1.0")
84 | .capabilities(
85 | McpSchema.ServerCapabilities.builder()
86 | .prompts(true)
87 | .resources(true, true)
88 | .tools(true)
89 | .build()
90 | )
91 | .build()
92 |
93 | server.configureServer()
94 |
95 | println("SSE server is running on http://localhost:$port/sse")
96 | }
97 |
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | ### Kotlin ###
40 | .kotlin
41 |
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "2.1.21"
3 | kotlin("plugin.spring") version "2.1.21"
4 | id("org.springframework.boot") version "3.5.0"
5 | id("io.spring.dependency-management") version "1.1.7"
6 | }
7 |
8 | group = "com.example"
9 | version = "0.0.1-SNAPSHOT"
10 |
11 | java {
12 | toolchain {
13 | languageVersion = JavaLanguageVersion.of(17)
14 | }
15 | }
16 |
17 | repositories {
18 | mavenCentral()
19 | }
20 |
21 | val springAiVersion = "1.0.0"
22 |
23 | dependencyManagement {
24 | imports {
25 | mavenBom("org.springframework.ai:spring-ai-bom:$springAiVersion")
26 | }
27 | }
28 |
29 | dependencies {
30 | implementation("org.springframework.boot:spring-boot-starter-web")
31 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
32 | implementation("org.jetbrains.kotlin:kotlin-reflect")
33 | implementation("org.springframework.ai:spring-ai-starter-model-openai")
34 | implementation("org.springframework.ai:spring-ai-starter-vector-store-qdrant")
35 | implementation("org.springframework.ai:spring-ai-advisors-vector-store")
36 |
37 | testImplementation("org.springframework.boot:spring-boot-starter-test")
38 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
39 | testRuntimeOnly("org.junit.platform:junit-platform-launcher")
40 | }
41 |
42 | kotlin {
43 | compilerOptions {
44 | freeCompilerArgs.addAll("-Xjsr305=strict")
45 | }
46 | }
47 |
48 | tasks.withType {
49 | useJUnitPlatform()
50 | }
51 |
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/springAI-demo/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/image/qdrant_collections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/springAI-demo/image/qdrant_collections.png
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/image/qdrant_loaded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/springAI-demo/image/qdrant_loaded.png
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/image/qdrant_start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/springAI-demo/image/qdrant_start.png
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/image/start_spring.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/springAI-demo/image/start_spring.png
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/image/welcome_qdrant_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Kotlin-AI-Examples/f2c54e09a989951409ed4fcbc90f9de4dfb807c2/projects/spring-ai/springAI-demo/image/welcome_qdrant_dashboard.png
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "springAI-demo"
2 |
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/src/main/kotlin/com/example/springai/demo/KotlinSTDController.kt:
--------------------------------------------------------------------------------
1 | package com.example.springai.demo
2 |
3 | import org.slf4j.LoggerFactory
4 | import org.springframework.ai.chat.client.ChatClient
5 | import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor
6 | import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor
7 | import org.springframework.ai.chat.prompt.Prompt
8 | import org.springframework.ai.chat.prompt.PromptTemplate
9 | import org.springframework.ai.document.Document
10 | import org.springframework.ai.vectorstore.SearchRequest
11 | import org.springframework.ai.vectorstore.VectorStore
12 | import org.springframework.web.bind.annotation.GetMapping
13 | import org.springframework.web.bind.annotation.PostMapping
14 | import org.springframework.web.bind.annotation.RequestBody
15 | import org.springframework.web.bind.annotation.RequestMapping
16 | import org.springframework.web.bind.annotation.RequestParam
17 | import org.springframework.web.bind.annotation.RestController
18 | import org.springframework.web.client.RestTemplate
19 | import kotlin.uuid.ExperimentalUuidApi
20 | import kotlin.uuid.Uuid
21 |
22 | // Data class representing the chat request payload.
23 | data class ChatRequest(val query: String, val topK: Int = 3)
24 |
25 | @RestController
26 | @RequestMapping("/kotlin")
27 | class KotlinSTDController(
28 | private val chatClientBuilder: ChatClient.Builder,
29 | private val restTemplate: RestTemplate,
30 | private val vectorStore: VectorStore,
31 | ) {
32 |
33 | // Logger for debugging and tracing actions.
34 | private val logger = LoggerFactory.getLogger(this::class.java)
35 |
36 | // Build the chat client with a simple logging advisor.
37 | private val chatClient = chatClientBuilder.defaultAdvisors(SimpleLoggerAdvisor()).build()
38 |
39 | @OptIn(ExperimentalUuidApi::class)
40 | @PostMapping("/load-docs")
41 | fun load() {
42 | // List of topics to load from the Kotlin website documentation.
43 | val kotlinStdTopics = listOf(
44 | "collections-overview", "constructing-collections", "iterators", "ranges", "sequences",
45 | "collection-operations", "collection-transformations", "collection-filtering", "collection-plus-minus",
46 | "collection-grouping", "collection-parts", "collection-elements", "collection-ordering",
47 | "collection-aggregate", "collection-write", "list-operations", "set-operations",
48 | "map-operations", "read-standard-input", "opt-in-requirements", "scope-functions", "time-measurement",
49 | )
50 | // Base URL for the documents.
51 | val url = "https://raw.githubusercontent.com/JetBrains/kotlin-web-site/refs/heads/master/docs/topics/"
52 | // Retrieve each document from the URL and add it to the vector store.
53 | kotlinStdTopics.forEach { topic ->
54 | val data = restTemplate.getForObject("$url$topic.md", String::class.java)
55 | data?.let { it ->
56 | val doc = Document.builder()
57 | // Build a Document with a random UUID, the text content, and metadata.
58 | .id(Uuid.random().toString())
59 | .text(it)
60 | .metadata("topic", topic)
61 | .build()
62 | vectorStore.add(listOf(doc))
63 | logger.info("Document $topic loaded.")
64 | } ?: logger.warn("Failed to load document for topic: $topic")
65 | }
66 | }
67 |
68 | @GetMapping("docs")
69 | fun query(
70 | @RequestParam query: String = "operations, filtering, and transformations",
71 | @RequestParam topK: Int = 2
72 | ): List? {
73 | val searchRequest = SearchRequest.builder()
74 | .query(query)
75 | .topK(topK)
76 | .build()
77 | val results = vectorStore.similaritySearch(searchRequest)
78 | logger.info("Found ${results?.size ?: 0} documents for query: '$query'")
79 | return results
80 | }
81 |
82 | @PostMapping("/chat/ask")
83 | fun chatAsk(@RequestBody request: ChatRequest): String? {
84 | // Define the prompt template with placeholders {query} and {target}.
85 | val promptTemplate = PromptTemplate(
86 | """
87 | {query}.
88 | Please provide a concise answer based on the "Kotlin standard library" documentation.
89 | """.trimIndent()
90 | )
91 |
92 | // Create the prompt by substituting placeholders with actual values.
93 | val prompt: Prompt =
94 | promptTemplate.create(mapOf("query" to request.query))
95 |
96 | // Configure the retrieval advisor to augment the query with relevant documents.
97 | val retrievalAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
98 | .searchRequest(
99 | SearchRequest.builder()
100 | .similarityThreshold(0.7)
101 | .topK(request.topK)
102 | .build()
103 | )
104 | .promptTemplate(promptTemplate)
105 | .build()
106 |
107 | // Send the prompt to the LLM with the retrieval advisor and get the response.
108 | val response = chatClient.prompt(prompt)
109 | .advisors(retrievalAdvisor)
110 | .call()
111 | .content()
112 | logger.info("Chat response generated for query: '${request.query}'")
113 | return response
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/src/main/kotlin/com/example/springai/demo/SpringAiDemoApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example.springai.demo
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 | import org.springframework.context.annotation.Bean
6 | import org.springframework.web.client.RestTemplate
7 |
8 |
9 | @SpringBootApplication
10 | class SpringAiDemoApplication {
11 |
12 | @Bean
13 | fun restTemplate(): RestTemplate = RestTemplate()
14 | }
15 |
16 | fun main(args: Array) {
17 | runApplication(*args)
18 | }
19 |
--------------------------------------------------------------------------------
/projects/spring-ai/springAI-demo/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=springAI-demo
2 | # OpenAI
3 | spring.ai.openai.api-key=${OPENAI_API_KEY}
4 | spring.ai.openai.chat.options.model=gpt-4o-mini
5 | spring.ai.openai.embedding.options.model=text-embedding-ada-002
6 | # Qdrant
7 | spring.ai.vectorstore.qdrant.host=localhost
8 | spring.ai.vectorstore.qdrant.port=6334
9 | spring.ai.vectorstore.qdrant.collection-name=kotlinDocs
10 | spring.ai.vectorstore.qdrant.initialize-schema=true
--------------------------------------------------------------------------------