From dfc5de7cdc77adb6d96f2882e0ba9ba68f22939e Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Wed, 21 Jan 2026 18:59:55 +0900 Subject: [PATCH] ... --- db/autotrade_db.mv.db | Bin 24576 -> 0 bytes src/main/kotlin/Main.kt | 2 +- src/main/kotlin/database/DatabaseFactory.kt | 28 ++++++-- src/main/kotlin/model/NewsModel.kt | 16 +++++ src/main/kotlin/network/NewsService.kt | 61 ++++++++++++++++++ src/main/kotlin/network/RagService.kt | 48 ++++++++------ .../kotlin/service/StockAnalysisManager.kt | 25 +++++++ src/main/kotlin/ui/AiAnalysisView.kt | 30 +++++++-- 8 files changed, 174 insertions(+), 36 deletions(-) delete mode 100644 db/autotrade_db.mv.db create mode 100644 src/main/kotlin/model/NewsModel.kt create mode 100644 src/main/kotlin/network/NewsService.kt create mode 100644 src/main/kotlin/service/StockAnalysisManager.kt diff --git a/db/autotrade_db.mv.db b/db/autotrade_db.mv.db deleted file mode 100644 index edee5ae4099137bce9c66e25f8f7f3849f1f802e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 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 Ci_Dn- diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index f5e9144..b901a6e 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -10,6 +10,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import model.AppConfig import model.KisSession import network.LlamaServerManager +import network.NewsService import org.jetbrains.exposed.sql.selectAll import ui.DashboardScreen import ui.SettingsScreen @@ -47,7 +48,6 @@ fun main() = application { } } - isLoaded = true } diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index 3860edc..8c27fdd 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -62,14 +62,28 @@ object TradeLogTable : Table("trade_logs") { override val primaryKey = PrimaryKey(id) } -class VectorColumnType(private val dimension: Int) : ColumnType() { - // [수정] H2에서 ARRAY는 내부 타입을 명시해야 합니다. - // VECTOR 대신 FLOAT8 ARRAY를 사용하면 벡터 연산 함수와 100% 호환됩니다. +class VectorColumnType(private val dimension: Int) : ColumnType() { override fun sqlType(): String = "FLOAT8 ARRAY" - override fun valueFromDB(value: Any): String = value.toString() + override fun valueFromDB(value: Any): DoubleArray { + return when (value) { + // H2 드라이버에서 반환된 객체를 처리 + is java.sql.Array -> { + val array = value.array as Array<*> + array.map { (it as Number).toDouble() }.toDoubleArray() + } + is Array<*> -> value.map { (it as Number).toDouble() }.toDoubleArray() + is DoubleArray -> value + else -> DoubleArray(0) + } + } - override fun notNullValueToDB(value: String): Any = value + // [핵심 수정 부분] + // primitive double[]을 Object Double[]로 변환하여 반환합니다. + // 이렇게 해야 H2 JDBC 드라이버가 직렬화 대신 SQL ARRAY로 인식합니다. + override fun notNullValueToDB(value: DoubleArray): Any { + return value.toTypedArray() + } } object VectorStoreTable : Table("VECTOR_STORE") { @@ -77,8 +91,8 @@ object VectorStoreTable : Table("VECTOR_STORE") { val content = text("content") val metadata = text("metadata") - // 이제 FLOAT8 ARRAY 타입으로 컬럼이 생성됩니다. - val embedding = registerColumn("embedding", VectorColumnType(1024)) + // 이제 이 컬럼은 DoubleArray 데이터를 직접 주고받습니다. + val embedding = registerColumn("embedding", VectorColumnType(1024)) override val primaryKey = PrimaryKey(id) } diff --git a/src/main/kotlin/model/NewsModel.kt b/src/main/kotlin/model/NewsModel.kt new file mode 100644 index 0000000..0c2d979 --- /dev/null +++ b/src/main/kotlin/model/NewsModel.kt @@ -0,0 +1,16 @@ +package model + +import kotlinx.serialization.Serializable + +@Serializable +data class NaverNewsResponse( + val items: List +) + +@Serializable +data class NewsItem( + val title: String, + val originallink: String, + val description: String, + val pubDate: String +) \ No newline at end of file diff --git a/src/main/kotlin/network/NewsService.kt b/src/main/kotlin/network/NewsService.kt new file mode 100644 index 0000000..954592f --- /dev/null +++ b/src/main/kotlin/network/NewsService.kt @@ -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(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}") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index dae93f4..49443b2 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -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() 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) } } \ No newline at end of file diff --git a/src/main/kotlin/service/StockAnalysisManager.kt b/src/main/kotlin/service/StockAnalysisManager.kt new file mode 100644 index 0000000..5910720 --- /dev/null +++ b/src/main/kotlin/service/StockAnalysisManager.kt @@ -0,0 +1,25 @@ +package service + +import network.NewsService + +object StockAnalysisManager { + + suspend fun analyzeStockWithRealTimeData(stockName: String, currentPrice: String): String { + println("🔍 [1/3] '${stockName}' 실시간 뉴스 수집 및 학습 시작...") + + // 1. 실시간 뉴스 검색 및 DB 저장 (Embedding 서버 8081 활용) + // 키워드를 "종목명 주가 전망"으로 최적화하여 검색 + NewsService.fetchAndIngestNews("$stockName 주가 전망") + + println("🧠 [2/3] 관련 컨텍스트 추출 중...") + + // 2. 방금 저장된 뉴스를 포함하여 DB에서 관련성 높은 정보 추출 + val question = "${stockName}의 현재가 ${currentPrice}원 기준, 최근 뉴스 수급 상황과 향후 단기 전망을 분석해줘." + val context = RagService.askWithContext(question) + + println("🤖 [3/3] AI 분석 생성 중 (Chat 서버 8080)...") + + // 3. 최종 분석 결과 반환 + return context + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/AiAnalysisView.kt b/src/main/kotlin/ui/AiAnalysisView.kt index 2eaa4b2..444af3d 100644 --- a/src/main/kotlin/ui/AiAnalysisView.kt +++ b/src/main/kotlin/ui/AiAnalysisView.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -24,11 +25,12 @@ import kotlinx.coroutines.launch import model.KisSession import model.RealTimeTrade import network.AiService +import service.StockAnalysisManager @Composable fun AiAnalysisView(stockName: String, currentPrice: String, trades: List) { var aiOpinion by remember { mutableStateOf("분석 대기 중...") } - var isLoading by remember { mutableStateOf(false) } + var isAnalyzing by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() // KisSession의 전역 설정을 참조 @@ -53,15 +55,29 @@ fun AiAnalysisView(stockName: String, currentPrice: String, trades: List