|
1 | 1 | package com.rogervinas |
2 | 2 |
|
3 | | -import io.modelcontextprotocol.client.McpClient |
4 | | -import io.modelcontextprotocol.client.McpSyncClient |
5 | | -import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport |
6 | | -import org.springframework.ai.chat.client.ChatClient |
7 | | -import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor |
8 | | -import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor |
9 | | -import org.springframework.ai.chat.memory.InMemoryChatMemory |
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper |
| 4 | +import org.slf4j.LoggerFactory |
10 | 5 | import org.springframework.ai.document.Document |
11 | | -import org.springframework.ai.mcp.SyncMcpToolCallbackProvider |
12 | 6 | import org.springframework.ai.vectorstore.VectorStore |
13 | | -import org.springframework.beans.factory.annotation.Value |
14 | 7 | import org.springframework.boot.ApplicationRunner |
15 | 8 | import org.springframework.boot.autoconfigure.SpringBootApplication |
16 | 9 | import org.springframework.boot.runApplication |
17 | 10 | import org.springframework.context.annotation.Bean |
18 | 11 | import org.springframework.context.annotation.Configuration |
19 | | -import org.springframework.data.annotation.Id |
20 | | -import org.springframework.data.repository.ListCrudRepository |
21 | | -import org.springframework.stereotype.Controller |
22 | | -import org.springframework.web.bind.annotation.PathVariable |
23 | | -import org.springframework.web.bind.annotation.PostMapping |
24 | | -import org.springframework.web.bind.annotation.RequestParam |
25 | | -import org.springframework.web.bind.annotation.ResponseBody |
26 | | -import java.util.concurrent.ConcurrentHashMap |
| 12 | +import org.springframework.core.io.ClassPathResource |
| 13 | +import org.springframework.jdbc.core.JdbcTemplate |
27 | 14 |
|
28 | 15 | @SpringBootApplication |
29 | 16 | class ChatServerApplication |
30 | 17 |
|
31 | 18 | @Configuration |
32 | | -class ConversationalConfiguration { |
33 | | - @Bean |
34 | | - fun mcpClient(@Value("\${mcp-server.url}") url: String) = McpClient |
35 | | - .sync(HttpClientSseClientTransport(url)) |
36 | | - .build().apply { |
37 | | - initialize() |
38 | | - } |
39 | | - |
40 | | - @Bean |
41 | | - fun chatClient( |
42 | | - mcpSyncClient: McpSyncClient, |
43 | | - builder: ChatClient.Builder |
44 | | - ): ChatClient { |
45 | | - val system = """ |
46 | | - You are an AI powered assistant to help people adopt a dog from the adoption |
47 | | - agency named Pooch Palace with locations in Atlanta, Antwerp, Seoul, Tokyo, Singapore, Paris, |
48 | | - Mumbai, New Delhi, Barcelona, San Francisco, and London. Information about the dogs available |
49 | | - will be presented below. If there is no information, then return a polite response suggesting we |
50 | | - don't have any dogs available. |
51 | | - |
52 | | - If the response involves a timestamp, be sure to convert it to something human-readable. |
53 | | - |
54 | | - Do _not_ include any indication of what you're thinking. Nothing should be sent to the client between <thinking> tags. |
55 | | - Just give the answer. |
56 | | - |
57 | | - """.trimIndent() |
58 | | - return builder |
59 | | - .defaultSystem(system) |
60 | | - .defaultTools(SyncMcpToolCallbackProvider(mcpSyncClient)) |
61 | | - .build() |
62 | | - } |
63 | | -} |
64 | | - |
65 | | -interface DogRepository : ListCrudRepository<Dog, Int> |
66 | | - |
67 | | -data class Dog(@Id val id: Int, val name: String, val owner: String?, val description: String) |
| 19 | +class VectorStoreConfiguration { |
68 | 20 |
|
69 | | -@Controller |
70 | | -@ResponseBody |
71 | | -class ConversationalController(vectorStore: VectorStore, private val chatClient: ChatClient) { |
72 | | - private val questionAnswerAdvisor = QuestionAnswerAdvisor(vectorStore) |
73 | | - private val chatMemory = ConcurrentHashMap<String, PromptChatMemoryAdvisor>() |
74 | | - |
75 | | - @PostMapping("/{id}/inquire") |
76 | | - fun inquire(@PathVariable id: String, @RequestParam question: String): String? { |
77 | | - val promptChatMemoryAdvisor = chatMemory |
78 | | - .computeIfAbsent(id) { _: String -> PromptChatMemoryAdvisor.builder(InMemoryChatMemory()).build() } |
79 | | - return chatClient |
80 | | - .prompt() |
81 | | - .user(question) |
82 | | - .advisors(questionAnswerAdvisor, promptChatMemoryAdvisor) |
83 | | - .call() |
84 | | - .content() |
85 | | - } |
86 | | -} |
87 | | - |
88 | | -@Configuration |
89 | | -class DogDataInitializerConfiguration { |
| 21 | + data class City(val name: String, val country: String, val description: String) |
90 | 22 |
|
91 | 23 | @Bean |
92 | | - fun initializerRunner(vectorStore: VectorStore, dogRepository: DogRepository): ApplicationRunner { |
93 | | - return ApplicationRunner { |
94 | | - dogRepository.deleteAll() |
95 | | - if (dogRepository.count() == 0L) { |
96 | | - println("initializing vector store"); |
97 | | - var map = mapOf( |
98 | | - "Rocky" to "A Boxer that needs to always say 'hi' and play with everybody he sees", |
99 | | - "Jasper" to "A grey Shih Tzu known for being protective.", |
100 | | - "Toby" to "A grey Doberman known for being playful.", |
101 | | - "Nala" to "A spotted German Shepherd known for being loyal.", |
102 | | - "Penny" to "A white Great Dane known for being protective.", |
103 | | - "Bella" to "A golden Poodle known for being calm.", |
104 | | - "Willow" to "A brindle Great Dane known for being calm.", |
105 | | - "Daisy" to "A spotted Poodle known for being affectionate.", |
106 | | - "Mia" to "A grey Great Dane known for being loyal.", |
107 | | - "Molly" to "A golden Chihuahua known for being curious.", |
108 | | - "Prancer" to "A demonic, neurotic, man hating, animal hating, children hating dogs that look like gremlins." |
109 | | - ) |
110 | | - map.forEach { name, description -> |
111 | | - var dog = dogRepository.save(Dog(0, name, null, description)); |
112 | | - var dogument = Document("id: ${dog.id}, name: ${dog.name}, description: ${dog.description}") |
113 | | - vectorStore.add(listOf(dogument)); |
114 | | - } |
115 | | - println("finished initializing vector store") |
| 24 | + fun vectorStoreInitializer( |
| 25 | + vectorStore: VectorStore, |
| 26 | + jdbcTemplate: JdbcTemplate, |
| 27 | + objectMapper: ObjectMapper |
| 28 | + ) = ApplicationRunner { |
| 29 | + val logger = LoggerFactory.getLogger(ChatServerApplication::class.java) |
| 30 | + val vectorStoreCount = vectorStoreCount(jdbcTemplate) |
| 31 | + if (vectorStoreCount == 0) { |
| 32 | + logger.info("Initializing vector store ...") |
| 33 | + val cities = ClassPathResource("cities.json").inputStream.use { |
| 34 | + objectMapper.readValue(it, Array<City>::class.java).toList() |
| 35 | + } |
| 36 | + cities.forEach { city -> |
| 37 | + logger.info("Adding ${city.name} to vector store ...") |
| 38 | + val document = Document("name: ${city.name} country: ${city.country} description: ${city.description}") |
| 39 | + vectorStore.add(listOf(document)) |
116 | 40 | } |
117 | | - }; |
| 41 | + logger.info("Vector store initialized with ${cities.size} cities") |
| 42 | + } else { |
| 43 | + logger.info("Vector store already contains $vectorStoreCount cities") |
| 44 | + } |
118 | 45 | } |
| 46 | + |
| 47 | + private fun vectorStoreCount(jdbcTemplate: JdbcTemplate) = |
| 48 | + jdbcTemplate.queryForObject("SELECT COUNT(*) FROM vector_store", Int::class.java) |
119 | 49 | } |
120 | 50 |
|
121 | 51 | fun main(args: Array<String>) { |
|
0 commit comments