Skip to content

Commit b6e9863

Browse files
authored
release: 1.6.4 (#178)
2 parents 7eb0b78 + e7968a2 commit b6e9863

12 files changed

+315
-4
lines changed

src/main/kotlin/org/gitanimals/core/redis/RedisPubSubChannel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ object RedisPubSubChannel {
44

55
const val NEW_QUIZ_CREATED = "NEW_QUIZ_CREATED"
66
const val NOT_APPROVED_QUIZ_CREATED = "NOT_APPROVED_QUIZ_CREATED"
7+
const val NOT_DEVELOPER_QUIZ_CREATED = "NOT_DEVELOPER_QUIZ_CREATED"
78

89
const val SLACK_INTERACTED = "SLACK_INTERACTED"
910
const val SLACK_REPLIED = "SLACK_REPLIED"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.gitanimals.notification.app
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import org.gitanimals.core.redis.TraceableMessageListener
5+
import org.gitanimals.notification.app.event.NotDeveloperQuizCreateRequestedNotification
6+
import org.gitanimals.notification.domain.Notification
7+
import org.gitanimals.notification.domain.Notification.ActionRequest
8+
import org.gitanimals.notification.domain.Notification.ActionRequest.Style.DANGER
9+
import org.slf4j.LoggerFactory
10+
import org.springframework.beans.factory.annotation.Qualifier
11+
import org.springframework.beans.factory.annotation.Value
12+
import org.springframework.data.redis.connection.Message
13+
import org.springframework.data.redis.core.StringRedisTemplate
14+
import org.springframework.stereotype.Component
15+
16+
@Component
17+
class NotDeveloperQuizCreateRequestedMessageListener(
18+
private val redisTemplate: StringRedisTemplate,
19+
private val objectMapper: ObjectMapper,
20+
@Qualifier("gitAnimalsNewQuizCreatedSlackNotification") private val newQuizCreatedNotification: Notification,
21+
@Value("\${quiz.approve.token}") private val approveToken: String,
22+
) : TraceableMessageListener(redisTemplate, objectMapper) {
23+
24+
private val logger = LoggerFactory.getLogger(this::class.simpleName)
25+
26+
override fun onMessage(message: Message) {
27+
runCatching {
28+
logger.info("NotDeveloperQuizCreateRequestedMessageListener message: $message")
29+
val notDeveloperQuizCreateRequested = objectMapper.readValue(
30+
redisTemplate.stringSerializer.deserialize(message.body),
31+
NotDeveloperQuizCreateRequestedNotification::class.java,
32+
)
33+
34+
val payloadWhenApprovedButtonClicked = mapOf(
35+
"clicked" to "TUNE IS_DEVELOPER_QUIZ",
36+
"sourceKey" to "NOT_DEVELOPER_QUIZ_REQUESTED",
37+
"approveToken" to approveToken,
38+
"category" to notDeveloperQuizCreateRequested.category,
39+
"language" to notDeveloperQuizCreateRequested.language,
40+
"problem" to notDeveloperQuizCreateRequested.problem,
41+
)
42+
43+
this.newQuizCreatedNotification.notifyWithActions(
44+
message = """
45+
:warning::warning::warning:
46+
개발 관련 내용이 아닌 퀴즈가 생성 요청되었어요.
47+
개발 관련 퀴즈라면, 버튼을 눌러 학습 시켜주세요.
48+
---생성 요청된 퀴즈 :point_down:---
49+
language: ${notDeveloperQuizCreateRequested.language}
50+
category: ${notDeveloperQuizCreateRequested.category}
51+
problem: ${notDeveloperQuizCreateRequested.problem}
52+
""".trimIndent(),
53+
actions = listOf(
54+
ActionRequest(
55+
id = "approve_action",
56+
style = DANGER,
57+
name = "Approve",
58+
interaction = payloadWhenApprovedButtonClicked,
59+
),
60+
)
61+
)
62+
}.onFailure {
63+
logger.error("Fail to publish not developer quiz create event.", it)
64+
}
65+
}
66+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.gitanimals.notification.app.event
2+
3+
data class NotDeveloperQuizCreateRequestedNotification(
4+
val language: String,
5+
val category: String,
6+
val problem: String,
7+
val traceId: Long,
8+
)

src/main/kotlin/org/gitanimals/notification/infra/RedisMessageListenerConfiguration.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class RedisMessageListenerConfiguration(
1616
private val slackRepliedMessageListener: SlackRepliedMessageListener,
1717
private val slackDeadLetterMessageListener: SlackDeadLetterMessageListener,
1818
private val newPetDropRateDistributionMessageListener: NewPetDropRateDistributionMessageListener,
19+
private val notDeveloperQuizCreateRequestedMessageListener: NotDeveloperQuizCreateRequestedMessageListener,
1920
) {
2021

2122
@Bean
@@ -42,6 +43,10 @@ class RedisMessageListenerConfiguration(
4243
newPetDropRateDistributionMessageListener,
4344
ChannelTopic(RedisPubSubChannel.NEW_PET_DROP_RATE_DISTRIBUTION)
4445
)
46+
this.addMessageListener(
47+
notDeveloperQuizCreateRequestedMessageListener,
48+
ChannelTopic(RedisPubSubChannel.NOT_DEVELOPER_QUIZ_CREATED)
49+
)
4550
}
4651
}
4752
}

src/main/kotlin/org/gitanimals/quiz/app/AIApi.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ class AIApi(
1010
private val openAI: OpenAI,
1111
) {
1212

13-
fun isDevelopmentQuiz(text: String): Boolean {
14-
val request = OpenAI.Request(messages = listOf(Message(role = "user", content = text)))
13+
fun isDevelopmentQuiz(prompt: String, text: String): Boolean {
14+
val request = OpenAI.Request(
15+
messages = listOf(
16+
Message(role = "system", content = prompt),
17+
Message(role = "user", content = text)
18+
)
19+
)
1520
val aiResponseContent = openAI.invoke(request).choices.first().message.content
1621
return when (aiResponseContent.trim().lowercase()) {
1722
"true" -> true
@@ -27,7 +32,7 @@ fun interface OpenAI {
2732
fun invoke(@RequestBody request: Request): Response
2833

2934
data class Request(
30-
val model: String = "gpt-4o",
35+
val model: String = "o4-mini",
3136
val messages: List<Message>,
3237
) {
3338

src/main/kotlin/org/gitanimals/quiz/app/CreateQuizFacade.kt

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ import org.gitanimals.core.filter.MDCFilter.Companion.TRACE_ID
88
import org.gitanimals.core.filter.MDCFilter.Companion.USER_ENTRY_POINT
99
import org.gitanimals.core.filter.MDCFilter.Companion.USER_ID
1010
import org.gitanimals.quiz.app.event.NotApprovedQuizCreated
11+
import org.gitanimals.quiz.app.event.NotDeveloperQuizCreateRequested
1112
import org.gitanimals.quiz.app.request.CreateQuizRequest
1213
import org.gitanimals.quiz.app.response.CreateQuizResponse
1314
import org.gitanimals.quiz.domain.approved.QuizService
15+
import org.gitanimals.quiz.domain.core.Language
16+
import org.gitanimals.quiz.domain.core.Language.Companion.containsKorean
1417
import org.gitanimals.quiz.domain.not_approved.NotApprovedQuizService
1518
import org.gitanimals.quiz.domain.prompt.QuizCreatePromptService
19+
import org.gitanimals.quiz.domain.prompt.rag.QuizCreateRag
20+
import org.gitanimals.quiz.domain.prompt.rag.QuizCreateRagService
1621
import org.rooftop.netx.api.Orchestrator
1722
import org.rooftop.netx.api.OrchestratorFactory
1823
import org.slf4j.LoggerFactory
@@ -30,6 +35,7 @@ class CreateQuizFacade(
3035
private val textSimilarityChecker: TextSimilarityChecker,
3136
private val eventPublisher: ApplicationEventPublisher,
3237
private val quizCreatePromptService: QuizCreatePromptService,
38+
private val quizCreateRagService: QuizCreateRagService,
3339
orchestratorFactory: OrchestratorFactory,
3440
) {
3541

@@ -41,15 +47,34 @@ class CreateQuizFacade(
4147
val userId = internalAuth.getUserId()
4248

4349
val quizCreatePrompt = quizCreatePromptService.getFirstPrompt()
50+
val language = when {
51+
createQuizRequest.problem.containsKorean() -> Language.KOREA
52+
else -> Language.ENGLISH
53+
}
54+
val quizCreateRags = quizCreateRagService.findAllByLanguageAndCategory(
55+
language = language,
56+
category = createQuizRequest.category,
57+
)
58+
4459
val isDevelopmentQuiz = runCatching {
45-
aiApi.isDevelopmentQuiz(quizCreatePrompt.getRequestTextWithPrompt(text = createQuizRequest.problem))
60+
aiApi.isDevelopmentQuiz(
61+
prompt = quizCreateRags.toPrompt(),
62+
text = quizCreatePrompt.getRequestTextWithPrompt(text = createQuizRequest.problem)
63+
)
4664
}.getOrElse {
4765
logger.error("Validation fail on isDevelopmentQuiz cause ${it.message}", it)
4866
throw it
4967
}
5068

5169
require(isDevelopmentQuiz) {
5270
logger.warn("Only development quiz allow request: $createQuizRequest")
71+
eventPublisher.publishEvent(
72+
NotDeveloperQuizCreateRequested(
73+
category = createQuizRequest.category,
74+
problem = createQuizRequest.problem,
75+
language = language,
76+
)
77+
)
5378
"Only development quiz allow request: $createQuizRequest"
5479
}
5580

@@ -98,6 +123,19 @@ class CreateQuizFacade(
98123
).decodeResultOrThrow(CreateQuizResponse::class)
99124
}
100125

126+
private fun List<QuizCreateRag>.toPrompt(): String {
127+
val quizCreateRag = this.map {
128+
"problem: ${it.problem}. is developer quiz?: ${it.isDevelopQuiz}"
129+
}.joinToString { "\n" }
130+
131+
return """
132+
Here are the quiz questions that have been identified as development-related so far.
133+
Please refer to these when determining whether a quiz is related to development.
134+
135+
$quizCreateRag
136+
""".trimIndent()
137+
}
138+
101139
init {
102140
this.createQuizOrchestrator = orchestratorFactory
103141
.create<CreateQuizRequest>("create quiz orchestrator")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.gitanimals.quiz.app.event
2+
3+
import org.gitanimals.core.IdGenerator
4+
import org.gitanimals.core.filter.MDCFilter
5+
import org.gitanimals.core.redis.AsyncRedisPubSubEvent
6+
import org.gitanimals.core.redis.RedisPubSubChannel
7+
import org.gitanimals.quiz.domain.core.Category
8+
import org.gitanimals.quiz.domain.core.Language
9+
import org.slf4j.MDC
10+
11+
data class NotDeveloperQuizCreateRequested(
12+
val language: Language,
13+
val category: Category,
14+
val problem: String,
15+
) : AsyncRedisPubSubEvent(
16+
channel = RedisPubSubChannel.NOT_DEVELOPER_QUIZ_CREATED,
17+
traceId = runCatching { MDC.get(MDCFilter.TRACE_ID) }
18+
.getOrElse { IdGenerator.generate().toString() },
19+
)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.gitanimals.quiz.domain.prompt.rag
2+
3+
import jakarta.persistence.*
4+
import org.gitanimals.core.IdGenerator
5+
import org.gitanimals.quiz.domain.core.Category
6+
import org.gitanimals.quiz.domain.core.Language
7+
8+
@Entity
9+
@Table(
10+
name = "quiz_create_rag",
11+
indexes = [
12+
Index(name = "quiz_idx_language", columnList = "language"),
13+
Index(name = "quiz_idx_category", columnList = "category"),
14+
]
15+
)
16+
class QuizCreateRag(
17+
@Id
18+
@Column(name = "id")
19+
val id: Long,
20+
21+
@Enumerated(EnumType.STRING)
22+
@Column(name = "language", nullable = false)
23+
val language: Language,
24+
25+
@Enumerated(EnumType.STRING)
26+
@Column(name = "category", nullable = false)
27+
val category: Category,
28+
29+
@Column(name = "is_develop_quiz", nullable = false)
30+
val isDevelopQuiz: Boolean,
31+
32+
@Column(name = "problem", length = 1000, columnDefinition = "VARCHAR(1000)", nullable = false)
33+
val problem: String,
34+
) {
35+
36+
companion object {
37+
fun create(language: Language, category: Category, isDevelopQuiz: Boolean, problem: String): QuizCreateRag {
38+
return QuizCreateRag(
39+
id = IdGenerator.generate(),
40+
language = language,
41+
category = category,
42+
isDevelopQuiz = isDevelopQuiz,
43+
problem = problem,
44+
)
45+
}
46+
}
47+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.gitanimals.quiz.domain.prompt.rag
2+
3+
import org.gitanimals.quiz.domain.core.Category
4+
import org.gitanimals.quiz.domain.core.Language
5+
import org.springframework.data.jpa.repository.JpaRepository
6+
7+
interface QuizCreateRagRepository : JpaRepository<QuizCreateRag, Long> {
8+
9+
fun findAllByLanguageAndCategory(language: Language, category: Category): List<QuizCreateRag>
10+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.gitanimals.quiz.domain.prompt.rag
2+
3+
import org.gitanimals.quiz.domain.core.Category
4+
import org.gitanimals.quiz.domain.core.Language
5+
import org.springframework.stereotype.Service
6+
7+
@Service
8+
class QuizCreateRagService(
9+
private val quizCreateRagRepository: QuizCreateRagRepository,
10+
) {
11+
12+
fun findAllByLanguageAndCategory(language: Language, category: Category): List<QuizCreateRag> {
13+
return quizCreateRagRepository.findAllByLanguageAndCategory(
14+
language = language,
15+
category = category,
16+
)
17+
}
18+
19+
fun createQuizCreateRag(
20+
language: Language,
21+
category: Category,
22+
isDevelopQuiz: Boolean,
23+
problem: String,
24+
): QuizCreateRag {
25+
return quizCreateRagRepository.save(
26+
QuizCreateRag.create(
27+
language = language,
28+
category = category,
29+
isDevelopQuiz = isDevelopQuiz,
30+
problem = problem,
31+
)
32+
)
33+
}
34+
}

0 commit comments

Comments
 (0)