-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 리뷰 재생성 기능을 추가한다 * refactor: 컨트롤러의 url path 오타를 수정한다 * refactor: Qualifer 오타를 -> Value로 수정한다 * fix: 리뷰 스트리밍 생성 기능을 테스트하고, 오류를 수정한다
- Loading branch information
Showing
20 changed files
with
397 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,10 +14,3 @@ | |
### Response | ||
|
||
#### `Response Status 200 OK` | ||
|
||
```json | ||
{ | ||
"id": "123456789", | ||
"review": "가득합니다....." | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# 리뷰 생성 | ||
|
||
리뷰를 생성합니다. | ||
|
||
## Request | ||
|
||
### HTTP METHOD : `GET` | ||
|
||
### url : `https://api.misik.me/reviews/{id}` | ||
### Http Headers | ||
- device-id: 식별할 수 있는 값 | ||
`미래에도 변하지 않아야함 앱을 삭제했다 다시 깔아도 안변하는 값으로 줄 수 있는지` | ||
|
||
### Response | ||
|
||
#### `Response Status 200 OK` | ||
|
||
```json | ||
{ | ||
"isSuccess": true, // 리뷰가 생성되지 않았으면 false, 전체가 성공되면 true | ||
"id": "123456789", | ||
"review": "카야토스는 숨겨져 있는 카야잼과 버터가 확실히 가득합니다. 또한, 가게의 분위기는 아늑하고 편안하고 바깥쪽에 있고 사랑하는 시간을 보낼 수 있는 공간입니다. 무엇보다 가격에 비해 음식의 품질이 정말 훌륭해서, 마음에 들었습니다." | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package me.misik.api.api | ||
|
||
import me.misik.api.app.ReCreateReviewFacade | ||
import org.springframework.web.bind.annotation.PathVariable | ||
import org.springframework.web.bind.annotation.PostMapping | ||
import org.springframework.web.bind.annotation.RestController | ||
|
||
@RestController | ||
class ReviewController( | ||
private val reCreateReviewFacade: ReCreateReviewFacade, | ||
) { | ||
|
||
@PostMapping("reviews/{id}/re-create") | ||
fun reCreateReview( | ||
@PathVariable("id") id: Long, | ||
) = reCreateReviewFacade.reCreateReviewInBackground(id) | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package me.misik.api.app | ||
|
||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.flow.filterNot | ||
import kotlinx.coroutines.launch | ||
import me.misik.api.core.Chatbot | ||
import me.misik.api.core.GracefulShutdownDispatcher | ||
import me.misik.api.domain.ReviewService | ||
import org.slf4j.LoggerFactory | ||
import org.springframework.stereotype.Service | ||
|
||
@Service | ||
class ReCreateReviewFacade( | ||
private val chatbot: Chatbot, | ||
private val reviewService: ReviewService, | ||
) { | ||
|
||
private val logger = LoggerFactory.getLogger(this::class.simpleName) | ||
|
||
fun reCreateReviewInBackground(id: Long) { | ||
reviewService.clearReview(id) | ||
|
||
reCreateReviewWithRetry(id, retryCount = 0) | ||
} | ||
|
||
private fun reCreateReviewWithRetry(id: Long, retryCount: Int) { | ||
CoroutineScope(GracefulShutdownDispatcher.dispatcher).launch { | ||
val review = reviewService.getById(id) | ||
|
||
chatbot.createReviewWithModelName(Chatbot.Request.from(review)) | ||
.filterNot { it.stopReason == ALREADY_COMPLETED } | ||
.collect { | ||
reviewService.updateReview(id, it.message?.content ?: "") | ||
} | ||
}.invokeOnCompletion { | ||
if (it == null) { | ||
return@invokeOnCompletion reviewService.completeReview(id) | ||
} | ||
if (retryCount == MAX_RETRY_COUNT) { | ||
logger.error("Failed to create review.", it) | ||
throw it | ||
} | ||
logger.warn("Failed to create review. retrying... retryCount: \"${retryCount + 1}\"", it) | ||
reCreateReviewWithRetry(id, retryCount + 1) | ||
} | ||
} | ||
|
||
private companion object { | ||
private const val MAX_RETRY_COUNT = 3 | ||
private const val ALREADY_COMPLETED = "stop_before" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package me.misik.api.core | ||
|
||
import kotlinx.coroutines.flow.Flow | ||
import me.misik.api.domain.Review | ||
import org.springframework.web.bind.annotation.RequestBody | ||
import org.springframework.web.service.annotation.PostExchange | ||
|
||
fun interface Chatbot { | ||
|
||
@PostExchange("/testapp/v1/chat-completions/HCX-003") | ||
fun createReviewWithModelName(@RequestBody request: Request): Flow<Response> | ||
|
||
data class Request( | ||
val messages: List<Message>, | ||
val includeAiFilters: Boolean = true, | ||
) { | ||
data class Message( | ||
val role: String, | ||
val content: String, | ||
) { | ||
|
||
companion object { | ||
|
||
fun createSystem(content: String) = Message( | ||
role = "system", | ||
content = content, | ||
) | ||
|
||
fun createUser(content: String) = Message( | ||
role = "user", | ||
content = content, | ||
) | ||
} | ||
} | ||
|
||
companion object { | ||
private val cachedSystemMessage = Message.createSystem( | ||
""" | ||
너는 지금부터 음식에 대한 리뷰를 하는 고독한 미식가야. | ||
답변에는 리뷰에 대한 내용만 포함해. | ||
또한, 응답 메시지는 공백을 포함해서 300자가 넘으면 절대로 안돼. | ||
""" | ||
) | ||
|
||
fun from(review: Review): Request { | ||
return Request( | ||
messages = listOf( | ||
cachedSystemMessage, | ||
Message.createUser(review.requestPrompt.text) | ||
) | ||
) | ||
} | ||
} | ||
} | ||
|
||
data class Response( | ||
val stopReason: String?, | ||
val message: Message?, | ||
) { | ||
data class Message( | ||
val role: String, | ||
val content: String, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package me.misik.api.core | ||
|
||
import jakarta.annotation.PreDestroy | ||
import kotlinx.coroutines.CoroutineDispatcher | ||
import kotlinx.coroutines.asCoroutineDispatcher | ||
import me.misik.api.core.GracefulShutdownDispatcher.executorService | ||
import org.slf4j.LoggerFactory | ||
import org.springframework.stereotype.Component | ||
import java.util.concurrent.Executors | ||
import java.util.concurrent.TimeUnit | ||
|
||
object GracefulShutdownDispatcher { | ||
|
||
val executorService = Executors.newFixedThreadPool(10) { runnable -> | ||
Thread(runnable, "misik-gracefulshutdown").apply { isDaemon = false } | ||
} | ||
|
||
val dispatcher: CoroutineDispatcher = executorService.asCoroutineDispatcher() | ||
} | ||
|
||
@Component | ||
class GracefulShutdownHook { | ||
|
||
private val logger = LoggerFactory.getLogger(this::class.simpleName) | ||
|
||
@PreDestroy | ||
fun tryGracefulShutdown() { | ||
logger.info("Shutting down dispatcher...") | ||
executorService.shutdown() | ||
runCatching { | ||
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { | ||
logger.warn("Forcing shutdown...") | ||
executorService.shutdownNow() | ||
} else { | ||
logger.info("Shutdown completed gracefully.") | ||
} | ||
}.onFailure { | ||
if (it is InterruptedException) { | ||
logger.warn("Shutdown interrupted. Forcing shutdown...") | ||
executorService.shutdownNow() | ||
Thread.currentThread().interrupt() | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,23 @@ | ||
package me.misik.api.domain | ||
|
||
import jakarta.persistence.LockModeType | ||
import org.springframework.data.jpa.repository.JpaRepository | ||
import org.springframework.data.jpa.repository.Lock | ||
import org.springframework.data.jpa.repository.Modifying | ||
import org.springframework.data.jpa.repository.Query | ||
import org.springframework.data.repository.query.Param | ||
|
||
interface ReviewRepository : JpaRepository<Review, Long> | ||
interface ReviewRepository : JpaRepository<Review, Long> { | ||
|
||
@Modifying(flushAutomatically = true, clearAutomatically = true) | ||
@Query("update review as r set r.text = concat(r.text, :text) where r.id = :id") | ||
fun addText(@Param("id") id: Long, @Param("text") text: String) | ||
|
||
@Modifying(flushAutomatically = true, clearAutomatically = true) | ||
@Query("update review as r set r.isCompleted = :completedStatus where r.id = :id") | ||
fun setReviewCompletedStatus(@Param("id") id: Long, @Param("completedStatus") completedStatus: Boolean) | ||
|
||
@Lock(LockModeType.PESSIMISTIC_READ) | ||
@Query("select r from review as r where id = :id") | ||
fun getReviewWithReadLock(@Param("id") id: Long): Review | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
36 changes: 36 additions & 0 deletions
36
src/main/kotlin/me/misik/api/infra/ClovaChatbotConfiguration.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package me.misik.api.infra | ||
|
||
import me.misik.api.core.Chatbot | ||
import org.springframework.beans.factory.annotation.Value | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.http.HttpHeaders | ||
import org.springframework.http.MediaType | ||
import org.springframework.web.reactive.function.client.WebClient | ||
import org.springframework.web.reactive.function.client.support.WebClientAdapter | ||
import org.springframework.web.service.invoker.HttpServiceProxyFactory | ||
|
||
@Configuration | ||
class ClovaChatbotConfiguration( | ||
@Value("\${me.misik.chatbot.clova.url:https://clovastudio.stream.ntruss.com/}") private val chatbotUrl: String, | ||
@Value("\${me.misik.chatbot.clova.authorization}") private val authorization: String, | ||
) { | ||
|
||
@Bean | ||
fun clovaChatbot(): Chatbot { | ||
val webClient = WebClient.builder() | ||
.baseUrl(chatbotUrl) | ||
.defaultHeaders { headers -> | ||
headers.add(HttpHeaders.AUTHORIZATION, "Bearer $authorization") | ||
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | ||
headers.add(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE) | ||
} | ||
.build() | ||
|
||
val httpServiceProxyFactory = HttpServiceProxyFactory | ||
.builderFor(WebClientAdapter.create(webClient)) | ||
.build() | ||
|
||
return httpServiceProxyFactory.createClient(Chatbot::class.java) | ||
} | ||
} |
Oops, something went wrong.