This commit is contained in:
lunaticbum 2025-03-06 17:53:16 +09:00
parent c85996e453
commit 8c16275c01
7 changed files with 374 additions and 59 deletions

View File

@ -64,7 +64,8 @@ dependencies {
// implementation("org.springframework.ai:spring-ai-ollama-spring-boot-starter:1.0.0-SNAPSHOT")
implementation(platform("org.springframework.ai:spring-ai-bom:1.0.0-M6"))
implementation("org.springframework.ai:spring-ai-ollama-spring-boot-starter:1.0.0-M6")
implementation("org.springframework.ai:spring-ai-redis-store")
implementation ("org.springframework.ai:spring-ai-qdrant-store-spring-boot-starter")
// implementation ("io.qdrant:client:1.13.0")
implementation ("org.slf4j:slf4j-simple:1.7.25")

View File

@ -1,7 +1,7 @@
package kr.lunaticbum.back.lun.configs
import io.qdrant.client.QdrantClient
import org.springframework.ai.ollama.api.OllamaApi
import org.springframework.ai.ollama.api.OllamaOptions
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@ -30,6 +30,12 @@ class AppConfig : WebMvcConfigurer {
super.addInterceptors(registry)
}
// @Bean
// fun qdrantClient(): QdrantClient {
// return QdrantClient("https://ollama.lunaticbum.kr:6334")
// }
@Bean
fun chatClient(): OllamaApi {
return OllamaApi("https://lama.lunaticbum.kr")

View File

@ -12,6 +12,7 @@ import com.google.maps.model.RankBy
import jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kr.lunaticbum.back.lun.configs.GlobalEnvironment
import kr.lunaticbum.back.lun.model.*
@ -437,26 +438,40 @@ class Telegram {
@Autowired
lateinit var lama : Lama
@ResponseBody
@GetMapping("query/{path}")
fun googleQueryTest(@PathVariable path: String): String {
var originalQuery = path
val gSearch = "https://psn.lunaticbum.kr/search?q=${originalQuery?.replace("오늘", SimpleDateFormat("yyyMMdd").format(Date()))}&language=auto&time_range=month&safesearch=0&categories=general&format=json"
println("gSearch >>> ${gSearch}")
var additionalInfo = StringBuffer()
additionalInfo.append("참고자료")
var idx = 0
WebClient.create().get()
.uri(gSearch)
.retrieve()
.bodyToMono(SearXng::class.java).timeout(Duration.ofMinutes(20L)).block()?.let { gsResult ->
gsResult.results?.filter { it.score > 0.5}?.forEach {
additionalInfo.append(idx).append(":").append(Gson().toJson(it))
idx += 1
}
}
// POST /collections
//
// Content-Type: application/json
//
// {
// "name": "movies",
// "vector_size": 3072,
// "distance": "Cosine"
// }
// println(lama.makeCollection())
// val gSearch = "https://psn.lunaticbum.kr/search?q=${originalQuery?.replace("오늘", SimpleDateFormat("yyyMMdd").format(Date()))}&language=auto&time_range=month&safesearch=0&categories=general&format=json"
// println("gSearch >>> ${gSearch}")
// var additionalInfo = StringBuffer()
// additionalInfo.append("참고자료")
// var idx = 0
// WebClient.create().get()
// .uri(gSearch)
// .retrieve()
// .bodyToMono(SearXng::class.java).timeout(Duration.ofMinutes(20L)).block()?.let { gsResult ->
// gsResult.results?.filter { it.score > 0.5}?.forEach {
// additionalInfo.append(idx).append(":").append(Gson().toJson(it))
// idx += 1
// }
// }
CoroutineScope(Dispatchers.IO).async {
lama.generateResponse(query = originalQuery)
}
return "TEST"
}

View File

@ -0,0 +1,115 @@
package kr.lunaticbum.back.lun.model
class QCollection {
var result: QResult? = null
var status: String? = null
var time: Double = 0.0
}
class QConfig {
var params: QParams? = null
var hnsw_config: QHnswConfig? = null
var optimizer_config: QOptimizerConfig? = null
var wal_config: QWalConfig? = null
var quantization_config: Any? = null
var strict_mode_config: QStrictModeConfig? = null
}
class QHnswConfig {
var m: Int = 0
var ef_construct: Int = 0
var full_scan_threshold: Int = 0
var max_indexing_threads: Int = 0
var on_disk: Boolean = false
}
class QOptimizerConfig {
var deleted_threshold: Double = 0.0
var vacuum_min_vector_number: Int = 0
var default_segment_number: Int = 0
var max_segment_size: Any? = null
var memmap_threshold: Any? = null
var indexing_threshold: Int = 0
var flush_interval_sec: Int = 0
var max_optimization_threads: Any? = null
}
class QParams {
var vectors: QVectors? = null
var shard_number: Int = 0
var replication_factor: Int = 0
var write_consistency_factor: Int = 0
var on_disk_payload: Boolean = false
}
class QPayloadSchema
class QResult {
var status: String? = null
var optimizer_status: String? = null
var indexed_vectors_count: Int = 0
var points_count: Long = 0
var segments_count: Int = 0
var config: QConfig? = null
var payload_schema: QPayloadSchema? = null
}
class QStrictModeConfig {
var enabled: Boolean = false
}
class QVectors {
var size: Int = 0
var distance: String? = null
}
class QWalConfig {
var wal_capacity_mb: Int = 0
var wal_segments_ahead: Int = 0
}
class QSearchResult {
var id: Int = 0
var version: Int = 0
var score: Double = 0.0
}
class QSearch {
var result: ArrayList<QSearchResult>? = null
var status: String? = null
var time: Double = 0.0
}
// import com.fasterxml.jackson.databind.ObjectMapper; // version 2.11.1
// import com.fasterxml.jackson.annotation.JsonProperty; // version 2.11.1
/* ObjectMapper om = new ObjectMapper();
Root root = om.readValue(myJsonString, Root.class); */
class QContentsPayload {
var url: String? = null
var title: String? = null
var content: String? = null
var engine: String? = null
var template: String? = null
var parsed_url: ArrayList<String>? = null
var engines: ArrayList<String>? = null
var positions: ArrayList<Int>? = null
var score: Double = 0.0
var category: String? = null
var originQuery: String? = null
var pageData: String? = null
}
class QContentsResult {
var id: Int = 0
var payload: QContentsPayload? = null
}
class QContents {
var result: ArrayList<QContentsResult>? = null
var status: String? = null
var time: Double = 0.0
}

View File

@ -12,6 +12,7 @@ class SearXng {
var unresponsive_engines: ArrayList<ArrayList<String>>? = null
}
class SearXngResult {
var originQuery : String? = null
var url: String? = null
var title: String? = null
var content: String? = null
@ -23,4 +24,5 @@ class SearXngResult {
var positions: ArrayList<Int>? = null
var score: Double = 0.0
var category: String? = null
var pageData : String? = null
}

View File

@ -1,61 +1,222 @@
package kr.lunaticbum.back.lun.service
import org.springframework.ai.chat.messages.UserMessage
import org.springframework.ai.chat.prompt.Prompt
import org.springframework.ai.document.Document
import org.springframework.ai.ollama.OllamaChatModel
import com.google.gson.Gson
import com.knuddels.jtokkit.api.IntArrayList
import io.micrometer.observation.ObservationRegistry
import kr.lunaticbum.back.lun.configs.GlobalEnvironment
import kr.lunaticbum.back.lun.model.*
import org.jsoup.Jsoup
import org.springframework.ai.embedding.EmbeddingRequest
import org.springframework.ai.ollama.OllamaEmbeddingModel
import org.springframework.ai.ollama.api.OllamaApi
import org.springframework.ai.vectorstore.SearchRequest
import org.springframework.ai.vectorstore.VectorStore
import org.springframework.ai.ollama.api.OllamaOptions
import org.springframework.ai.ollama.management.ModelManagementOptions
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.util.stream.Collectors
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import java.net.URLEncoder
import java.text.SimpleDateFormat
import java.time.Duration
import java.util.*
import kotlin.collections.ArrayList
@Service
class Lama {
@Qualifier("chatClient")
@Autowired
private lateinit var chatClient: OllamaApi
@Autowired
private lateinit var vectorStore: VectorStore
//, val date : String = SimpleDateFormat("yyyyMMddHHmmss").format(Date())
// data class QSearchData(val query : FloatArray,val limit : Int)
fun generateResponse(query: String?): String {
println("On generateResponse :: find something")
// 1. 유사 문서 검색
data class QSearchData(val vector : FloatArray,val limit : Int)
data class QPut(val points : ArrayList<QData>)
data class QData(val id : Long, val vector : FloatArray, val payload : SearXngResult)
val relevantDocs = vectorStore.similaritySearch(
SearchRequest.builder().query(query!!).topK(3).build()
data class QContentsList(var ids : ArrayList<Int> = ArrayList(), var with_payload : Boolean = true, var with_vector : Boolean = false)
// fun makeCollection() : String{
//
// class CollectionPut {
// val name = "blama_vectors"
// val vector_size = 3072
// val distance = "Cosine"
// }
// val qUrl = "https://ollama.lunaticbum.kr/collections"
// val client = WebClient.create()
// return client.post()
// .uri(qUrl)
// .header("api-key","blama-admin-key-gb")
// .body(BodyInserters.fromValue(Gson().toJson(CollectionPut())))
// .retrieve()
// .bodyToMono(String::class.java).timeout(Duration.ofMinutes(20L)).block() ?: ""
// }
var qPointsCount : Long = 0
private fun checkCollection() : Long {
val qUrl = "https://ollama.lunaticbum.kr/collections/blama_vectors"
val client = WebClient.create()
return client.get()
.uri(qUrl)
.header("api-key", "blama-admin-key-gb")
.retrieve()
.bodyToMono(QCollection::class.java).timeout(Duration.ofMinutes(20L)).block()?.result?.points_count ?: 0L
}
private fun addDocuments(query : String) {
val embeddingModel = OllamaEmbeddingModel(
chatClient,
OllamaOptions.builder().build(),
ObservationRegistry.create(),
ModelManagementOptions.defaults()
)
val gSearch = "https://psn.lunaticbum.kr/search?q=${query?.replace("오늘", SimpleDateFormat("yyyMMdd").format(Date()))}&language=auto&time_range=month&safesearch=0&categories=general&format=json"
println("gSearch >>> ${gSearch}")
val sdss = QPut(arrayListOf())
WebClient.create().get()
.uri(gSearch)
.retrieve()
.bodyToMono(SearXng::class.java).timeout(Duration.ofMinutes(20L)).block()?.let { gsResult ->
gsResult.results?.filter { it.score > 0.5}?.forEach {
qPointsCount += 1
println("in filter")
it.originQuery = query
val data = Gson().toJson(it)
println(it.title)
Jsoup.connect(it.url).get().body().text()?.let { text ->
try {
println("text >>>>> $text")
it.pageData = chatClient.chat(OllamaApi.ChatRequest.Builder("phi4:14b").stream(false).format("json").messages(
listOf(OllamaApi.Message.Builder(OllamaApi.Message.Role.USER).content("'${text}' 웹 페이지 모든 내욜을 복사 한건데 본문 내용만 정리해줘").build())
).build()).message.content
println("summary result >>>>> ${it.pageData}")
val embeddingResponse = embeddingModel.call(
EmbeddingRequest(
listOf(data),
OllamaOptions.builder()
.model("nomic-embed-text")
.truncate(false)
.build()
)
)
sdss.points.add(QData(id = qPointsCount,embeddingResponse.result.output,it))
}catch (e : Exception) {
// 2. 프롬프트 구성
val context = relevantDocs?.map { it.text }?.joinToString(separator = "\n")
val prompt = """
Context information is below.
----
$context
----
Given the context information and not prior knowledge, answer the query: $query
\n한국어로 대답해줘
""".trimIndent()
}
}
}
}
println("out filter")
if (sdss.points.size > 0) {
println("sdss.points.size ${sdss.points.size} ${Gson().toJson(sdss)}")
val qUrl = "https://ollama.lunaticbum.kr/collections/blama_vectors".plus("/points")
val client = WebClient.create()
client.put()
.uri(qUrl)
.header("api-key", "blama-admin-key-gb")
.body(BodyInserters.fromValue(Gson().toJson(sdss)))
.retrieve()
.bodyToMono(String::class.java).timeout(Duration.ofMinutes(20L)).block() ?: ""
// 3. Ollama를 사용하여 응답 생성
val response: OllamaApi.ChatResponse = chatClient.chat(OllamaApi.ChatRequest.Builder("phi4:14b").stream(false).format("json").messages(
}
println("end of search")
}
private fun embedQuery(embedFlots : FloatArray) : QContents?{
val qUrl = "https://ollama.lunaticbum.kr/collections/blama_vectors".plus("/points/search")
val client = WebClient.create()
var lists = client.post()
.uri(qUrl)
.header("api-key","blama-admin-key-gb")
.body(BodyInserters.fromValue(Gson().toJson(QSearchData(embedFlots,5))))
.retrieve()
.bodyToMono(QSearch::class.java).timeout(Duration.ofMinutes(20L)).block()
return if (lists?.result?.size ?: 0 > 0) {
val qContents = QContentsList()
lists?.result?.forEach {
qContents.ids.add(it.id)
}
val qCUrl = "https://ollama.lunaticbum.kr/collections/blama_vectors".plus("/points")
val client2 = WebClient.create()
client.post()
.uri(qCUrl)
.header("api-key", "blama-admin-key-gb")
.body(BodyInserters.fromValue(Gson().toJson(qContents)))
.retrieve()
.bodyToMono(QContents::class.java).timeout(Duration.ofMinutes(20L)).block()
} else {
null
}
}
@Autowired
lateinit var globalEvv : GlobalEnvironment
suspend fun generateResponse(query: String?, targetId: String? = globalEvv.telegramMyId) {
val embeddingModel = OllamaEmbeddingModel(
chatClient,
OllamaOptions.builder().build(),
ObservationRegistry.create(),
ModelManagementOptions.defaults()
)
println("On generateResponse :: find something ${query}")
query?.let {
var embeddingResponse = embeddingModel.call(
EmbeddingRequest(
listOf(query),
OllamaOptions.builder()
.model("nomic-embed-text")
.truncate(false)
.build()
)
)
println("points size ${embeddingResponse.result.output.size}")
qPointsCount = checkCollection()
addDocuments(it)
embeddingResponse = embeddingModel.call(
EmbeddingRequest(
listOf(query),
OllamaOptions.builder()
.model("nomic-embed-text")
.truncate(false)
.build()
)
)
println(embeddingResponse.result.output)
var context : String? = ""
try {
embedQuery(embeddingResponse.result.output)?.result?.forEach { result ->
context += "참고자료:".plus(if (result.payload?.pageData?.length ?: 0 > 10) {
result.payload?.pageData
} else {
result.payload?.content
})
context +="\n"
}
}catch (e:Exception){
e.printStackTrace()
}
val response: OllamaApi.ChatResponse = if (context?.length ?: 0 > 10) {
val prompt = "Context information is below.\n$context\nGiven the context information and not prior knowledge, answer the query: $query\n한국어로 대답해줘".trimIndent()
chatClient.chat(OllamaApi.ChatRequest.Builder("phi4:14b").stream(false).format("json").messages(
listOf(OllamaApi.Message.Builder(OllamaApi.Message.Role.USER).content(prompt).build())
).build())
} else {
chatClient.chat(OllamaApi.ChatRequest.Builder("phi4:14b").stream(false).format("json").messages(
listOf(OllamaApi.Message.Builder(OllamaApi.Message.Role.USER).content(query).build())
).build())
}
println(response.message.content)
println("On generateResponse :: END OF Answer")
return response.message.content
}
}
// 문서 추가 메소드
fun addDocuments(documents: List<Document?>?) {
documents?.let {
vectorStore.add(it)
}
// vectorStore!!.add(documents)
}
}

View File

@ -65,10 +65,25 @@ spring.data.mongodb.option.local-threshold=15
spring.ai.ollama.base-url=https://lama.lunaticbum.kr
spring.ai.ollama.chat.options.model=phi4:14b
spring.data.redis.host=ollama.lunaticbum.kr
spring.ai.vectorstore.redis.initialize-schema=true
spring.ai.vectorstore.redis.index=spring-ai-redis-index
spring.ai.vectorstore.redis.prefix=spring-ai-redis-embedding
##spring.data.redis.url=ollama.lunaticbum.kr
#spring.data.redis.host=lunaticbum.kr
#spring.data.redis.port=6379
#
##spring.ai.vectorstore.redis.uri="redis://lunaticbum.kr:6379"
#
#
#spring.ai.vectorstore.redis.initialize-schema=true
#spring.ai.vectorstore.redis.index=spring-ai-redis-index
#spring.ai.vectorstore.redis.prefix=spring-ai-redis-embedding
#https://ollama.lunaticbum.kr/collections/blama_vectors
spring.ai.vectorstore.qdrant.host=ollama.lunaticbum.kr
spring.ai.vectorstore.qdrant.port=443
#spring.ai.vectorstore.qdrant.initialize-schema=true
spring.ai.vectorstore.qdrant.api-key=blama-admin-key-gb
spring.ai.vectorstore.qdrant.collection-name=blama_vectors
spring.ai.ollama.embedding.model=nomic-embed-text
spring.ai.ollama.embedding.enabled=true
#>>>>>>> ab915d0a416c69708f1df1ad76d7a14c779c1f59