Skip to content

Commit

Permalink
release: 0.0.2
Browse files Browse the repository at this point in the history
  • Loading branch information
inpink authored Feb 6, 2025
1 parent 2e11141 commit 54d0cd2
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 12 deletions.
2 changes: 1 addition & 1 deletion docs/api/OCR_파싱_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ key:value는 요청 text에 따라 다르게 응답합니다.

## Request

### HTTP METHOD : `GET`
### HTTP METHOD : `POST`

### url : `https://api.misik.me/reviews/ocr-parsing`
### Http Headers
Expand Down
15 changes: 12 additions & 3 deletions src/main/kotlin/me/misik/api/api/ReviewController.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package me.misik.api.api

import me.misik.api.api.response.ReviewResponse
import me.misik.api.domain.request.CreateReviewRequest
import me.misik.api.api.response.ParsedOcrResponse
import me.misik.api.app.CreateReviewFacade
import me.misik.api.app.GetReviewFacade
import me.misik.api.app.ReCreateReviewFacade
import me.misik.api.app.GetReviewFacade
import me.misik.api.domain.request.CreateReviewRequest
import me.misik.api.domain.request.OcrTextRequest
import me.misik.api.domain.ReviewStyle
import me.misik.api.domain.response.ReviewStylesResponse
import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class ReviewController(
Expand Down Expand Up @@ -42,4 +47,8 @@ class ReviewController(
@PathVariable("id") id: Long,
): ReviewResponse = ReviewResponse.of(getReviewFacade.getReview(id))

@PostMapping("reviews/ocr-parsing")
fun parseOcrText(
@RequestBody ocrText: OcrTextRequest,
) : ParsedOcrResponse = createReviewFacade.parseOcrText(ocrText)
}
10 changes: 10 additions & 0 deletions src/main/kotlin/me/misik/api/api/response/ParsedOcrResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package me.misik.api.api.response

data class ParsedOcrResponse(
val parsed: List<KeyValuePair>
) {
data class KeyValuePair(
val key: String,
val value: String,
)
}
28 changes: 24 additions & 4 deletions src/main/kotlin/me/misik/api/app/CreateReviewFacade.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
package me.misik.api.app;

import com.fasterxml.jackson.databind.ObjectMapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.launch
import me.misik.api.domain.request.CreateReviewRequest
import me.misik.api.core.Chatbot
import me.misik.api.core.GracefulShutdownDispatcher
import me.misik.api.api.response.ParsedOcrResponse
import me.misik.api.domain.CreateReviewCache
import me.misik.api.domain.request.CreateReviewRequest
import me.misik.api.domain.request.OcrTextRequest
import me.misik.api.domain.Review
import me.misik.api.domain.ReviewService
import me.misik.api.domain.prompt.PromptService
import me.misik.api.core.Chatbot
import me.misik.api.core.OcrParser
import me.misik.api.core.GracefulShutdownDispatcher
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service


@Service
class CreateReviewFacade(
private val chatbot:Chatbot,
private val ocrParser: OcrParser,
private val reviewService:ReviewService,
private val promptService: PromptService,
private val createReviewCache: CreateReviewCache
private val createReviewCache: CreateReviewCache,
private val objectMapper: ObjectMapper,
) {

private val logger = LoggerFactory.getLogger(this::class.simpleName)
Expand Down Expand Up @@ -64,6 +70,20 @@ class CreateReviewFacade(
}
}

fun parseOcrText(ocrText: OcrTextRequest): ParsedOcrResponse {
val response = ocrParser.createParsedOcr(OcrParser.Request.from(ocrText.text))
val responseContent = response.result?.message?.content ?: ""

val parsedOcr = objectMapper.readValue(responseContent, ParsedOcrResponse::class.java)
?: throw IllegalStateException("Invalid OCR text format")

if (parsedOcr.parsed.isEmpty()) {
throw IllegalArgumentException("Parsed OCR content is empty")
}

return parsedOcr
}

private companion object {
private const val MAX_RETRY_COUNT = 3
private const val ALREADY_COMPLETED = "stop_before"
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/me/misik/api/core/Chatbot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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 maxTokens: Int = 100,
Expand Down Expand Up @@ -57,3 +58,4 @@ fun interface Chatbot {
)
}
}

11 changes: 11 additions & 0 deletions src/main/kotlin/me/misik/api/core/ErrorResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package me.misik.api.core

data class ErrorResponse(
val message: String,
) {

companion object {
fun from(exception: Exception): ErrorResponse =
ErrorResponse(exception.message ?: exception.localizedMessage)
}
}
85 changes: 85 additions & 0 deletions src/main/kotlin/me/misik/api/core/OcrParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package me.misik.api.core

import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.service.annotation.PostExchange

fun interface OcrParser {

@PostExchange("/testapp/v1/chat-completions/HCX-003")
fun createParsedOcr(@RequestBody request: Request): Response

data class Request(
val messages: List<Message>,
val maxTokens: Int = 100,
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 {
val cachedParsingSystemMessage = Message.createSystem(
"""
리뷰에 쓸만한 정보를 추출해줘. key에는 방문 장소명, 품명 등이 포함될 수 있어. key는 최대 3개만 뽑아줘.
응답 형식은 반드시 다음과 같은 JSON이야. 응답에는 해당 JSON만 있어야해.
{
"parsed": [
{
"key": "품명",
"value": "카야토스트+음료세트"
},
{
"key": "가격",
"value": "3000"
},
...
]
}
응답의 총 길이는 300자를 넘으면 안돼.
"""
)

fun from(ocrText: String): Request {
return Request(
messages = listOf(
cachedParsingSystemMessage,
Message.createUser(ocrText)
)
)
}
}
}

data class Response(
val status: Status?,
val result: Result?
) {
data class Status(
val code: String,
val message: String
)
data class Result(
val message: Message?
) {
data class Message(
val role: String,
val content: String
)
}
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/me/misik/api/core/advice/GlobalExceptionHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package me.misik.api.core.advice

import me.misik.api.core.ErrorResponse
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class GlobalExceptionHandler {

private val logger = LoggerFactory.getLogger(this::class.simpleName)

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException::class)
fun handleIllegalArgumentException(exception: IllegalArgumentException): ErrorResponse {
logger.error(exception.message, exception)
return ErrorResponse.from(exception)
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(IllegalStateException::class)
fun handleIllegalStateException(exception: IllegalStateException): ErrorResponse {
logger.error(exception.message, exception)
return ErrorResponse.from(exception)
}
}
1 change: 0 additions & 1 deletion src/main/kotlin/me/misik/api/domain/ReviewService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class ReviewService(
fun getById(id: Long): Review = reviewRepository.findByIdOrNull(id)
?: throw IllegalArgumentException("Cannot find review by id \"$id\"")


fun getReview(id: Long) = reviewRepository.findById(id)
?: throw IllegalArgumentException("Cannot find review by id \"$id\"")
}
5 changes: 5 additions & 0 deletions src/main/kotlin/me/misik/api/domain/request/OcrTextRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package me.misik.api.domain.request

data class OcrTextRequest(
val text: String,
)
20 changes: 20 additions & 0 deletions src/main/kotlin/me/misik/api/infra/ClovaChatbotConfiguration.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package me.misik.api.infra

import me.misik.api.core.Chatbot
import me.misik.api.core.OcrParser
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.client.RestClient
import org.springframework.web.client.support.RestClientAdapter
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.support.WebClientAdapter
import org.springframework.web.service.invoker.HttpServiceProxyFactory
Expand Down Expand Up @@ -33,4 +36,21 @@ class ClovaChatbotConfiguration(

return httpServiceProxyFactory.createClient(Chatbot::class.java)
}

@Bean
fun ocrParser(): OcrParser {
val restClient = RestClient.builder()
.baseUrl(chatbotUrl)
.defaultHeaders { headers ->
headers.add(HttpHeaders.AUTHORIZATION, "Bearer $authorization")
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
}
.build()

val httpServiceProxyFactory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()

return httpServiceProxyFactory.createClient(OcrParser::class.java)
}
}
9 changes: 7 additions & 2 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ server.port=8080

spring.datasource.url=jdbc:mysql://localhost:3306/misikapi
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.password=cardme@
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.ddl-auto=create
spring.jpa.open-in-view=false
spring.jpa.show-sql=true

logging.level.me.misik.api.infra=DEBUG

me.misik.chatbot.clova.authorization=nv-e44d6c703b0049adb0db06ac1bcb85362x02

netx.mode=redis
netx.host=localhost
Expand Down
14 changes: 13 additions & 1 deletion src/main/resources/ddl/prompt.ddl
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
INSERT INTO prompt (id, style, command, created_at, modified_at) VALUES
(1, 'PROFESSIONAL', '자연스러운, 전문적인 말투, 존대말, 과거형으로 리뷰를 만들어줘. 응답 형식은 {“review”:”리뷰 내용”}이어야 해. 응답에는 해당 JSON만 있어야해. 또한, 리뷰는 공백을 포함해서 300자가 넘어야 해. 리뷰에는 다음 의견도 자연스럽게 넣어줘.', NOW(), NOW()),
(2, 'FRIENDLY', '~다, ~요를 적절히 섞은 높임말로 리뷰를 만들어줘. 한국 중년처럼 보이도록 가끔 문장 끝에 ..같은 특수문자를 넣어줘. 응답 형식은 {“review”:”리뷰 내용”}이어야 해. 응답에는 해당 JSON만 있어야해. 또한, 리뷰는 공백을 포함해서 100자 이상 200자 이하여야해. 리뷰에는 다음 의견도 자연스럽게 넣어줘.', NOW(), NOW()),
(3, 'CUTE', '~다, ~요를 적절히 섞은 높임말로 리뷰를 만들어줘. 깜찍한 10대처럼 보이도록 가끔 어미 뒤에 랜덤하게 😘, ㅎㅎ 같은 특수문자, "당, 용" 같이 끝나는 어미를 넣어줘. 응답 형식은 {"review":"리뷰 내용"}이어야 해. 응답에는 해당 JSON만 있어야 해. 또한, 리뷰는 공백을 포함해서 100자 이상 200자 이하여야 해. 리뷰에는 다음 의견도 자연스럽게 넣어줘.', NOW(), NOW());
(3, 'CUTE', '~다, ~요를 적절히 섞은 높임말로 리뷰를 만들어줘. 깜찍한 10대처럼 보이도록 가끔 어미 뒤에 랜덤하게 😘, ㅎㅎ 같은 특수문자, "당, 용" 같이 끝나는 어미를 넣어줘. 응답 형식은 {"review":"리뷰 내용"}이어야 해. 응답에는 해당 JSON만 있어야 해. 또한, 리뷰는 공백을 포함해서 100자 이상 200자 이하여야 해. 리뷰에는 다음 의견도 자연스럽게 넣어줘.', NOW(), NOW());

create table if not exists misikapi.prompt
(
created_at datetime(6) null,
id bigint not null
primary key,
modified_at datetime(6) null,
command text not null,
style varchar(20) not null,
constraint UKn1rq77g5p76xoejtkn8xgj207
unique (style)
);

0 comments on commit 54d0cd2

Please sign in to comment.