ㅎㅎㅎ

This commit is contained in:
2026-01-22 16:21:18 +09:00
parent dfc5de7cdc
commit 99804b892a
19 changed files with 714 additions and 178 deletions
+163 -42
View File
@@ -1,12 +1,25 @@
// src/main/kotlin/network/RagService.kt
import VectorStoreTable.metadata
import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
import dev.langchain4j.data.document.Metadata
import dev.langchain4j.data.message.UserMessage
import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.model.openai.OpenAiChatModel
import dev.langchain4j.model.openai.OpenAiEmbeddingModel
import dev.langchain4j.store.embedding.EmbeddingSearchRequest
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import model.CandleData
import network.DartCodeManager
import network.KisTradeService
import network.NewsService
import org.apache.lucene.store.MMapDirectory
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
import org.jetbrains.exposed.sql.transactions.transaction
import service.TechnicalAnalyzer
import java.nio.file.Paths
import java.time.Duration
object RagService {
@@ -22,60 +35,168 @@ object RagService {
.timeout(Duration.ofSeconds(60))
.build()
private val embeddingStore: LuceneEmbeddingStore by lazy {
val path = Paths.get("db/lucene_idx")
// FSDirectory.open(path)도 가능하지만, 64bit 시스템(Mac)에선 MMapDirectory가 가장 빠릅니다.
val directory = MMapDirectory(path)
// 제공해주신 소스의 Builder 사용
LuceneEmbeddingStore.builder()
.directory(directory)
.build()
}
/**
* 텍스트를 임베딩하여 H2 DB에 저장합니다.
*/
fun ingest(text: String, meta: String = "") {
val embeddingVector: DoubleArray = embeddingModel.embed(text).content().vector().map { it.toDouble() }.toDoubleArray()
transaction {
VectorStoreTable.insert {
it[content] = text
it[metadata] = meta
// [수정] 문자열 변환 없이 객체 그대로 전달
it[embedding] = embeddingVector
}
}
println("💾 H2 벡터 저장 완료: ${text.take(15)}...")
fun ingest(text: String, newsLink: String = "", pubDate: String = "") {
// 소스 코드의 TextSegment 구조에 맞춰 메타데이터 생성
val metadata = Metadata()
metadata.put("link", newsLink)
metadata.put("date", pubDate)
// TextSegment.from(text, metadata) 팩토리 메서드 활용
val segment = TextSegment.from(text, metadata)
val embedding = embeddingModel.embed(segment).content()
// LuceneEmbeddingStore.add(Embedding, TextSegment) 호출
embeddingStore.add(embedding, segment)
println("🔎 [Lucene] 인덱싱 성공: ${text.take(20)}...")
}
suspend fun processStock(stockCode: String,result :(String, Boolean)->Unit,decide : (String,TradingDecision?)->Unit) {
// 1. 10분간의 데이터 가져오기 (API 호출)
coroutineScope {
var tradingDecision : TradingDecision = TradingDecision()
val financialDataDeferred = async { NewsService.fetchFinancialGrowth(DartCodeManager.getCorpCode(stockCode)) }
tradingDecision.financialData = financialDataDeferred.await()
result(tradingDecision.toString(),false)
tradingDecision.techSummary = TechnicalAnalyzer.generateComprehensiveReport()
result(tradingDecision.toString(),false)
val question = "$stockCode 종목의 현재 주가 흐름과 뉴스, 재무 실적을 바탕으로 종합 투자 전략을 세워줘."
val questionEmbedding = embeddingModel.embed(question).content()
val searchResult = embeddingStore.search(
EmbeddingSearchRequest.builder()
.queryEmbedding(questionEmbedding)
.maxResults(3)
.build()
)
tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
result(tradingDecision.toString(),false)
decide(stockCode,decideTrading(stockCode, tradingDecision.techSummary ?: "", tradingDecision.newsContext ?: "",tradingDecision.financialData ?: ""))
}
}
/**
* 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다.
*/
fun askWithContext(question: String): String {
val queryVector = embeddingModel.embed(question).content().vector()
// H2 ARRAY 포맷에 맞춰 (v1, v2, ...) 형태로 변환
val vectorStr = queryVector.joinToString(",", "(", ")")
val context = transaction {
// 코사인 유사도 기준 상위 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.joinToString("\n\n")
}
fun askWithContext(question: String,
corpInfo: String,
financialData: String,
days : List<CandleData>,
weeks : List<CandleData>,
monthly : List<CandleData>): String {
val questionEmbedding = embeddingModel.embed(question).content()
val searchResult = embeddingStore.search(
EmbeddingSearchRequest.builder()
.queryEmbedding(questionEmbedding)
.maxResults(5)
.build()
)
val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
// 2. 종합 분석 프롬프트 구성
val finalPrompt = """
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
당신은 실시간 뉴스 분석에 능통한 20년 경력의 주식 전문가입니다.
제공된 [참고 자료]를 바탕으로 사용자의 질문에 전문적이고 단호하게 답하세요.<|eot_id|>
<|start_header_id|>user<|end_header_id|>
[참고 자료]
$context
당신은 뉴스(심리), 재무(본질), 차트(추세)를 통합 분석하는 'AI 수석 애널리스트'입니다.
제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요.
[질문]
$question
[데이터 세트]
1. 기업 기본 정보: $corpInfo
2. 재무 성장성: $financialData
3. 기술적 추세: ${monthly}, ${weeks}, ${days}
4. 최신 이슈(뉴스): $newsContext
[분석 요청 사항]
1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요.
2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요.
3. **장기/단기 전략**:
- 장기(재무/월봉 기반): 추천 혹은 비추천 사유
- 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유
4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요.
<|eot_id|>
<|start_header_id|>user<|end_header_id|>
질문: $question
<|eot_id|><|start_header_id|>assistant<|end_header_id|>
""".trimIndent()
return chatModel.generate(finalPrompt)
val response = chatModel.chat(UserMessage.from(finalPrompt))
println(response)
return response.aiMessage().text()
}
suspend fun decideTrading(
stockName: String,
techSummary: String,
newsContext: String,
financialData: String
): TradingDecision? {
val prompt = """
당신은 단기 데이트레이딩 전문가입니다. 아래 데이터를 분석하여 '매수', '매도', '관망' 중 하나를 결정하세요.
[종목]: $stockName
$techSummary
[관련 뉴스]: $newsContext
[재무 기초]: $financialData
반드시 아래 JSON 형식으로만 답변하세요:
{
"decision": "BUY" | "SELL" | "HOLD",
"reason": "결정적 근거 한 줄",
"confidence": 0~100
}
""".trimIndent()
val response = chatModel.chat(UserMessage.from(prompt))
val jsonResponse = response.aiMessage().text()
// JSON 파싱 (Kotlinx Serialization 활용)
return try {
println(jsonResponse)
val decision = Json.decodeFromString<TradingDecision>(jsonResponse)
decision.financialData = financialData
decision.newsContext = newsContext
decision.techSummary = techSummary
decision
} catch (e: Exception) {
null
}
}
}
@Serializable
class TradingDecision {
var decision: String? = null
var reason: String? = null
var confidence: Int = 0
var techSummary : String? = null
var newsContext : String? = null
var financialData : String? = null
override fun toString(): String {
return """
decision: $decision
reason: $reason
confidence: $confidence
techSummary: $techSummary
newsContext: $newsContext
financialData: $financialData
""".trimIndent()
}
}