...
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.engine.cio.CIOEngineConfig
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.DEFAULT
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.http.ContentType.Application.Json
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import model.NaverNewsResponse
|
||||
|
||||
object NewsService {
|
||||
private val client = HttpClient<CIOEngineConfig>(CIO) {
|
||||
|
||||
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true })
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchAndIngestNews(query: String) {
|
||||
val clientId = "CqXQXHO3h0kqtYsXkePY" // 설정에서 가져오도록 수정 필요
|
||||
val clientSecret = "DODCxb1M4Z"
|
||||
|
||||
try {
|
||||
val response: NaverNewsResponse = client.get("https://openapi.naver.com/v1/search/news.json") {
|
||||
parameter("query", query)
|
||||
parameter("display", 10) // 최근 10개 뉴스
|
||||
parameter("sort", "sim") // 유사도 순 (또는 date 발간순)
|
||||
header("X-Naver-Client-Id", clientId)
|
||||
header("X-Naver-Client-Secret", clientSecret)
|
||||
}.body()
|
||||
|
||||
response.items.forEach { item ->
|
||||
// HTML 태그 제거 및 텍스트 정제
|
||||
val cleanTitle = item.title.replace(Regex("<[^>]*>"), "")
|
||||
val cleanDesc = item.description.replace(Regex("<[^>]*>"), "")
|
||||
val fullText = "[$cleanTitle] $cleanDesc"
|
||||
println(fullText)
|
||||
// RAG 서비스에 학습(Ingest) 시키기
|
||||
RagService.ingest(
|
||||
text = fullText,
|
||||
meta = "{\"link\": \"${item.originallink}\", \"date\": \"${item.pubDate}\"}"
|
||||
)
|
||||
}
|
||||
println("📰 '${query}' 관련 뉴스 10개 학습 완료")
|
||||
} catch (e: Exception) {
|
||||
println("❌ 뉴스 가져오기 실패: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/main/kotlin/network/RagService.kt
|
||||
|
||||
import VectorStoreTable.metadata
|
||||
import dev.langchain4j.data.segment.TextSegment
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel
|
||||
import dev.langchain4j.model.openai.OpenAiEmbeddingModel
|
||||
@@ -25,14 +26,13 @@ object RagService {
|
||||
* 텍스트를 임베딩하여 H2 DB에 저장합니다.
|
||||
*/
|
||||
fun ingest(text: String, meta: String = "") {
|
||||
val embedding = embeddingModel.embed(text).content().vector()
|
||||
|
||||
val embeddingVector: DoubleArray = embeddingModel.embed(text).content().vector().map { it.toDouble() }.toDoubleArray()
|
||||
transaction {
|
||||
VectorStoreTable.insert {
|
||||
it[content] = text
|
||||
it[metadata] = meta
|
||||
// 벡터 데이터를 문자열 형태로 저장 (H2 포맷)
|
||||
it[VectorStoreTable.embedding] = embedding.joinToString(",", "[", "]")
|
||||
// [수정] 문자열 변환 없이 객체 그대로 전달
|
||||
it[embedding] = embeddingVector
|
||||
}
|
||||
}
|
||||
println("💾 H2 벡터 저장 완료: ${text.take(15)}...")
|
||||
@@ -41,35 +41,41 @@ object RagService {
|
||||
/**
|
||||
* 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다.
|
||||
*/
|
||||
fun ask(question: String): String {
|
||||
fun askWithContext(question: String): String {
|
||||
val queryVector = embeddingModel.embed(question).content().vector()
|
||||
val vectorStr = queryVector.joinToString(",", "[", "]")
|
||||
// H2 ARRAY 포맷에 맞춰 (v1, v2, ...) 형태로 변환
|
||||
val vectorStr = queryVector.joinToString(",", "(", ")")
|
||||
|
||||
// H2의 VECTOR_COSINE_SIMILARITY 함수를 사용하여 검색
|
||||
val context = transaction {
|
||||
val query = "SELECT content FROM VECTOR_STORE " +
|
||||
"ORDER BY VECTOR_COSINE_SIMILARITY(embedding, '$vectorStr') DESC " +
|
||||
"LIMIT 3"
|
||||
// 코사인 유사도 기준 상위 5개 뉴스 추출
|
||||
val query = """
|
||||
SELECT CONTENT FROM VECTOR_STORE
|
||||
ORDER BY VECTOR_COSINE_SIMILARITY(EMBEDDING, CAST('$vectorStr' AS FLOAT8 ARRAY)) DESC
|
||||
LIMIT 5
|
||||
""".trimIndent()
|
||||
|
||||
val results = mutableListOf<String>()
|
||||
exec(query) { rs ->
|
||||
while (rs.next()) {
|
||||
results.add(rs.getString("content"))
|
||||
results.add(rs.getString("CONTENT"))
|
||||
}
|
||||
}
|
||||
results.joinToString("\n\n")
|
||||
}
|
||||
|
||||
val prompt = """
|
||||
[참고 정보]
|
||||
$context
|
||||
|
||||
[질문]
|
||||
$question
|
||||
|
||||
위 정보를 참고하여 분석 결과를 말해주세요.
|
||||
""".trimIndent()
|
||||
val finalPrompt = """
|
||||
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
|
||||
당신은 실시간 뉴스 분석에 능통한 20년 경력의 주식 전문가입니다.
|
||||
제공된 [참고 자료]를 바탕으로 사용자의 질문에 전문적이고 단호하게 답하세요.<|eot_id|>
|
||||
<|start_header_id|>user<|end_header_id|>
|
||||
[참고 자료]
|
||||
$context
|
||||
|
||||
return chatModel.generate(prompt)
|
||||
[질문]
|
||||
$question
|
||||
<|eot_id|><|start_header_id|>assistant<|end_header_id|>
|
||||
""".trimIndent()
|
||||
|
||||
return chatModel.generate(finalPrompt)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user