Skip to content

Commit

Permalink
feat: 뉴스 그룹화 로직 추가 (1차 데모 구현)
Browse files Browse the repository at this point in the history
  • Loading branch information
hun-ca committed Dec 17, 2024
1 parent 64e9021 commit fc95402
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ import org.springframework.web.bind.annotation.RequestBody
)
interface OpenAiClient {
@PostMapping
fun summarizeNews(@RequestBody request: OpenAiRequest): OpenAiResponse
fun send(@RequestBody request: OpenAiRequest): OpenAiResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.few.domain.generator.core
import com.few.domain.generator.client.OpenAiClient
import com.few.domain.generator.client.request.OpenAiRequest
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import org.springframework.stereotype.Component

Expand All @@ -11,7 +12,7 @@ class ChatGpt(
private val openAiClient: OpenAiClient,
private val fewGson: Gson,
) {
fun makeSummaryPrompt(news: NewsModel): List<Map<String, String>> {
private fun makeSummaryPrompt(news: NewsModel): List<Map<String, String>> {
val command = """
다음 뉴스 기사를 분석하고 요약해주세요:
Expand Down Expand Up @@ -55,10 +56,64 @@ class ChatGpt(
messages = prompt
)

val response = openAiClient.summarizeNews(request)
val response = openAiClient.send(request)
val resultContent = response.choices.firstOrNull()?.message?.content
?: throw Exception("요약 결과를 찾을 수 없습니다.")

return fewGson.fromJson(resultContent, object : TypeToken<Map<String, Any>>() {}.type)
}

fun groupNewsWithChatGPT(newsList: List<NewsModel>): JsonObject {
val promptMessages = makeGroupingPrompt(newsList)

val request = OpenAiRequest(
model = "gpt-4",
messages = promptMessages
)

val response = openAiClient.send(request)
val resultContent = response.choices[0].message.content.trim()

// JSON 형태로 변환
return fewGson.fromJson(resultContent, JsonObject::class.java) // TODO 리턴타입 변경
}

private fun makeGroupingPrompt(newsList: List<NewsModel>): List<Map<String, String>> {
val newsSummaries = newsList.joinToString("\n") { "${it.id}: ${it.summary}" }
val command = """
다음은 여러 뉴스 기사의 요약입니다. 이 뉴스들을 비슷한 주제끼리 그룹핑해주세요:
$newsSummaries
# 지침:
1. 뉴스 요약들을 분석하고 비슷한 주제끼리 그룹화하세요.
2. 각 그룹에 적절한 주제를 부여하세요.
3. topic은 구체적인 문장으로 작성해주세요.
4. 응답은 반드시 다음 JSON 형식을 따라야 합니다:
{
"groups": [
{
"topic": "그룹의 주제",
"news_ids": ["id1", "id3", "id5" ...]
},
{
"topic": "다른 그룹의 주제",
"news_ids": ["id2", "id4", "id6" ...]
}
]
}
# 주의사항:
- 응답은 오직 위의 JSON 형식만 포함해야 합니다. 다른 설명이나 내용을 추가하지 마세요.
- 각 그룹의 "news_ids"는 위 목록의 뉴스 ID를 나타냅니다.
- 그룹의 수에는 제한이 없지만, 너무 많은 그룹을 만들지 않도록 주의하세요. 뉴스 총 개수의 10% 이상의 그룹을 만들지 않도록 주의하세요.
- 그룹의 주제는 추상적인 문장을 사용하지 말고 구체적인 문장으로 작성해주세요.
""".trimIndent()

return listOf( // TODO 클래스 정의
mapOf("role" to "system", "content" to "당신은 뉴스 기사를 주제별로 그룹핑하는 전문가입니다. 주어진 뉴스 요약들을 분석하고 비슷한 주제끼리 그룹화해야 합니다."),
mapOf("role" to "user", "content" to command)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.few.domain.generator.core

data class GroupNewsModel(
val topic: String = "",
val news: List<NewsModel> = listOf(),
val section: SectionContentModel = SectionContentModel(),
) {
fun toMap(): Map<String, Any> {
return mapOf(
"topic" to topic,
"news" to news.map { it.toMap() },
"section" to section.toDict()
)
}

companion object {
fun fromMap(data: Map<String, Any>): GroupNewsModel {
val newsList = (data["news"] as List<Map<String, Any>>).map { NewsModel.fromMap(it) }
val sectionData = SectionContentModel.fromDict(data["section"] as Map<String, Any>? ?: emptyMap())
return GroupNewsModel(
topic = data["topic"] as String,
news = newsList,
section = sectionData
)
}
}
}

data class SectionContentModel(
val title: String = "",
val contents: List<ContentModel> = listOf(),
) {
fun toDict(): Map<String, Any> {
return mapOf(
"title" to title,
"contents" to contents.map { it.toDict() }
)
}

companion object {
fun fromDict(data: Map<String, Any>): SectionContentModel {
val contentsList =
(data["contents"] as? List<Map<String, Any>>)?.map { ContentModel.fromDict(it) } ?: emptyList()
return SectionContentModel(
title = data["title"] as? String ?: "",
contents = contentsList
)
}
}
}

data class ContentModel(
val subTitle: String = "",
val body: String = "",
) {
fun toDict(): Map<String, Any> {
return mapOf(
"subTitle" to subTitle,
"body" to body
)
}

companion object {
fun fromDict(data: Map<String, Any>): ContentModel {
return ContentModel(
subTitle = data["subTitle"] as? String ?: "",
body = data["body"] as? String ?: ""
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.few.domain.generator.core

import java.io.File
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component

@Component
class NewsGrouper(
private val chatGpt: ChatGpt,
private val fewGson: Gson,
) {
private val log = KotlinLogging.logger {}

fun groupAndSaveNews(inputFilePath: String, outputFilePath: String) {
val newsList = loadSummarizedNews(inputFilePath)

log.info { "뉴스 그룹화 진행 중..." }

val groupedNews = chatGpt.groupNewsWithChatGPT(newsList)

log.info { "그룹화된 뉴스 저장 중..." }
saveGroupedNewsToJson(groupedNews, newsList, outputFilePath)

log.info { "뉴스 그룹화 완료." }
log.info { "${groupedNews.size()}개의 그룹으로 뉴스가 분류되어 '$outputFilePath' 파일로 저장되었습니다." }
}

private fun loadSummarizedNews(inputFilePath: String): List<NewsModel> {
val fileContent = File(inputFilePath).readText(Charsets.UTF_8)

// JSON 문자열을 List<Map<String, Any>> 형태로 변환
val typeToken = object : TypeToken<List<Map<String, Any>>>() {}.type
val data: List<Map<String, Any>> = fewGson.fromJson(fileContent, typeToken)

// 각 항목을 NewsModel 객체로 변환
return data.map { NewsModel.fromMap(it) }
}

private fun saveGroupedNewsToJson(
groupedNews: JsonObject,
newsList: List<NewsModel>,
outputFilePath: String,
) {
val result = mutableListOf<GroupNewsModel>()

// "groups" 필드를 JsonArray로 추출
val groupElements = groupedNews.getAsJsonArray("groups")

for (groupElement in groupElements) {
val group = groupElement.asJsonObject

// "news_ids"를 JsonArray로 추출하고 String 리스트로 변환
val groupNewsIds = group.getAsJsonArray("news_ids").map { it.asString }

// 뉴스 ID가 그룹에 포함된 뉴스 필터링
val newsInGroup = newsList.filter { it.id in groupNewsIds }

// 뉴스가 3개 이상인 경우만 추가
if (newsInGroup.size >= 3) {
val groupNews = GroupNewsModel(
topic = group.getAsJsonPrimitive("topic").asString,
news = newsInGroup
)
result.add(groupNews)
println(groupNewsIds)
}
}

// JSON 직렬화 및 파일 저장
val groupNewsData = result.map { it.toMap() }
File(outputFilePath).writeText(fewGson.toJson(groupNewsData), Charsets.UTF_8)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ExecuteCrawlerUseCase(
* 없는 경우 크롤링 시작
*/

// 1. 네이버 뉴스 크롤링
log.info { "크롤링이 시작" }
val newsUrls = naverNewsCrawler.getNaverNewsUrls(useCaseIn.sid)

Expand All @@ -41,8 +42,11 @@ class ExecuteCrawlerUseCase(
naverNewsCrawler.saveContentAsJson(results)
log.info { "크롤링이 완료 및 요약 시작" }

// 2. 뉴스 추출 및 요약
extractor.extractAndSaveNews("crawled_news.json", "extracted_news.json")

// 3. 뉴스 그룹화

ExecuteCrawlerUseCaseOut(
useCaseIn.sid,
listOf(UUID.randomUUID().toString()) // TODO: DB 저장 시 크롤링 고유 ID 응답
Expand Down

0 comments on commit fc95402

Please sign in to comment.