diff --git a/api-docs.yml b/api-docs.yml new file mode 100644 index 0000000..0898ea0 --- /dev/null +++ b/api-docs.yml @@ -0,0 +1,468 @@ +openapi: 3.1.0 +info: + description: | + 2024 멋쟁이사자처럼 중앙해커톤 남송리3번지팀 데모 API 명세 + version: 0.1.0 + title: stepper + contact: + name: zionhann + email: hanzion1108@gmail.com +servers: + - url: https://api.zionhann.com/stepper + description: production + - url: http://localhost:8080 + description: local +security: + - X-CSRF-TOKEN: [ ] +tags: + - name: auth + description: 인증 + - name: goals + description: 성장목표 + - name: journals + description: 일지 + - name: chats + description: AI 대화 +paths: + # auth + /v1/csrf: + get: + tags: + - auth + summary: csrf 토큰 발급 + description: " " + operationId: issueCsrfToken + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CsrfToken' + examples: + CsrfTokenExample: + value: + parameterName: _csrf + token: inU6CJ2UNMf + headerName: X-CSRF-TOKEN + /logout: + post: + tags: + - auth + summary: 로그아웃 + description: " " + operationId: logout + responses: + "200": + description: OK + + # goals + /v1/goals: + post: + tags: + - goals + summary: 목표 추가 + description: 목표를 추가한다. + operationId: addGoal + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GoalRequest' + examples: + GoalRequestExample: + value: + title: 중앙해커톤 대상 + startDate: 2024-08-06 + endDate: 2024-08-07 + thumbnail: https://3-namsong-st.s3.ap-northeast-2.amazonaws.com/goals/b46f73d9-fa83-4331-b37d-996897280aa6.jpeg... + responses: + "201": + description: Created + get: + tags: + - goals + summary: 내 목표 목록 조회 + description: 내 목표 목록을 불러온다. + operationId: getGoals + parameters: + - name: sort + in: query + schema: + type: string + enum: + - NEWEST + - ASC + - DESC + description: | + 정렬 순서. + + `NEWEST`: 최신순(기본값, 생성 날짜 기준) + + `ASC`: 오름차순(이름 기준) + + `DESC`: 내림차순(이름 기준) + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GoalResponse' + examples: + GoalResponseExample: + value: + goals: + - goalId: 1 + title: 중앙해커톤 대상 + streak: 3 + status: OPEN + startDate: 24.08.06 + endDate: 24.08.07 + thumbnail: https://3-namsong-st.s3.ap-northeast-2.amazonaws.com/goals/b46f73d9-fa83-4331-b37d-996897280aa6.jpeg... + - goalId: 2 + title: 5kg 다이어트 + streak: 0 + status: CLOSED + + # journals + /v1/goals/{goalId}/journals: + post: + tags: + - journals + summary: 특정 목표의 일지 생성 + description: " " + operationId: addJournal + parameters: + - name: goalId + in: path + description: 목표 번호 + required: true + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/JournalRequest' + examples: + JournalRequestExample: + value: + title: 진행상황 공유 + content: | + 미팅 시간에 돌아가면서 진행상황을 공유했다. 각자의 역할은 존중하면서 하나의 프로덕트를 다같이 만들어간다는 느낌이 들어 기분이 좋았다. + responses: + "201": + description: Created + get: + tags: + - journals + summary: 특정 목표의 일지 목록 조회 + description: " " + operationId: getJournalsOfGoal + parameters: + - name: goalId + in: path + description: 목표 번호 + required: true + schema: + type: number + - name: q + in: query + description: 검색 키워드(제목+내용) + schema: + type: string + - name: sort + in: query + description: | + 정렬순서 + + `NEWEST`: 최신순(기본값, 작성일 기준) + + `OLDEST`: 오래된순(작성일 기준) + schema: + type: string + enum: + - NEWEST + - OLDEST + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + goals: + $ref: '#/components/schemas/GoalResponse' + journals: + type: array + items: + $ref: '#/components/schemas/JournalResponse' + examples: + JournalResponseExample: + value: + goal: + goalId: 1 + title: 중앙해커톤 대상 + streak: 3 + status: OPEN + startDate: 24.08.06 + endDate: 24.08.07 + thumbnail: https://3-namsong-st.s3.ap-northeast-2.amazonaws.com/goals/b46f73d9-fa83-4331-b37d-996897280aa6.jpeg... + journals: + - journalId: 1 + title: 진행상황 공유 + createdDate: 24.07.17 + # thumbnail: https://3-namsong-st.s3.ap-northeast-2.amazonaws.com/goals/b46f73d9-fa83-4331-b37d-996897280aa6.jpeg... + - journalId: 2 + title: 아이디어 디벨롭 + createdDate: 24.07.16 + /v1/journals/{journalId}: + get: + tags: + - journals + summary: 일지 상세정보 조회 + description: " " + operationId: getJournalDetail + parameters: + - name: journalId + in: path + required: true + schema: + type: string + description: 일지 ID + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/JournalDetailResponse' + examples: + JournalDetailResponseExample: + value: + journalId: 1 + title: 진행상황 공유 + content: | + 미팅 시간에 돌아가면서 진행상황을 공유했다. 각자의 역할은 존중하면서 하나의 프로덕트를 다같이 만들어간다는 느낌이 들어 기분이 좋았다. + createdDate: 24.07.17 + + # chat + /v1/goals/{goalId}/chats: + post: + tags: + - chats + summary: 특정 목표에 대해 AI로 일지 작성 채팅방 생성 + description: " " + operationId: initChat + parameters: + - name: goalId + in: path + description: 목표 ID + required: true + schema: + type: number + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/ChatResponse' + examples: + ChatResponseExample: + value: + chatId: thread_StEPpEr + /v1/chats/{chatId}/history: + get: + tags: + - chats + summary: 특정 채팅방의 대화내역 조회 + description: " " + operationId: getChatMessages + parameters: + - name: chatId + in: path + description: 채팅방 ID + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + messages: + type: array + items: + $ref: '#/components/schemas/ChatHistoryResponse' + examples: + ChatHistoryResponseExample: + value: + messages: + - role: CHATBOT + content: "안녕하세요 😊\n저는 당신의 성장 여정을 함께할 AI (챗봇 이름)입니다 :)" + - role: CHATBOT + content: "자신의 생각과 감정을 솔직하게 기록하면서, 매일 조금씩 더 나은 나를 발견해 보세요.\n오늘의 기록이 내일의 당신을 더욱 빛나게 할 거예요.\n자, 이제 시작해 볼까요? 당신의 이야기를 들려주세요!" + - role: CHATBOT + content: "어떤 계기로 ‘중앙해커톤 우승'이라는 목표를 가지게 되었나요?" + - role: USER + content: "한동 멋사에서 아기사자로 활동하는 동안 우승하고 싶어!" + /v1/chats/{chatId}/summary: + post: + tags: + - chats + summary: 특정 채팅방의 대화 내용 요약 (일지 초안 작성) + description: " " + operationId: chatSummary + parameters: + - name: chatId + in: path + description: 채팅방 ID + required: true + schema: + type: number + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/ChatSummaryResponse' + examples: + ChatSummaryResponseExample: + value: + content: "중앙해커톤에 참여하면서 '우승'이라는 목표를 세웠다. 활동 기간 동안 우승하고 싶다는 열망이 커졌고, 이를 위해 매주 두 번씩 모여 서비스에 대한 회의를 진행했다. 지속적으로 피드백을 주고받고, AI 챗봇 기능이 정말 중요하다는 이야기를 듣고 반드시 선보여야겠다는 다짐을 했다.\n\nAI 챗봇 기능의 발전을 위해 기능 플로우에 대한 시퀀스 다이어그램을 만들고 순차적으로 구현해 나갔다." +components: + securitySchemes: + X-CSRF-TOKEN: + type: apiKey + in: header + name: X-CSRF-TOKEN + schemas: + GoalRequest: + type: object + required: + - title + properties: + title: + type: string + description: 목표 이름 + startDate: + type: string + description: 목표 시작일 + endDate: + type: string + description: 목표 종료일 + thumbnail: + type: string + description: 목표 썸네일 이미지 + GoalResponse: + type: object + properties: + goaldId: + type: number + descriptiom: 목표 ID + title: + type: string + description: 목표 이름 + streak: + type: number + description: 연속작성일수 + status: + type: string + enum: + - OPEN + - CLOSED + - OVERDUE + description: 목표 상태 + startDate: + type: string + description: 목표 시작일 + endDate: + type: string + description: 목표 종료일 + thumbnail: + type: string + description: 목표 썸네일 이미지 + JournalRequest: + type: object + required: + - title + - content + properties: + title: + type: string + description: 일지제목 + content: + type: string + description: 일지내용 + JournalResponse: + type: object + properties: + journalId: + type: number + description: 일지 ID + title: + type: string + description: 일지 제목 + createdDate: + type: string + description: 작성일자 + # thumbnail: + # type: string + # description: 썸네일 이미지 + JournalDetailResponse: + type: object + properties: + journalId: + type: number + description: 일지 ID + title: + type: string + description: 일지 제목 + content: + type: string + description: 일지 내용 + createdDate: + type: string + description: 작성일 + CsrfToken: + type: object + properties: + prameterName: + type: string + description: csrf 파라미터 키값 + token: + type: string + description: csrf 토큰 + headerName: + type: string + description: csrf 헤더 키값 + ChatResponse: + type: object + properties: + chatId: + type: string + description: 채팅방 ID + ChatHistoryResponse: + type: object + properties: + role: + type: string + description: 메시지 주체 + content: + type: string + description: 메시지 내용 + ChatSummaryResponse: + type: object + properties: + content: + type: string + description: 요약한 내용 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 104090d..c7db7ef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,9 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.springframework.session:spring-session-data-redis") + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") diff --git a/src/main/kotlin/com/likelionhgu/stepper/chat/ChatController.kt b/src/main/kotlin/com/likelionhgu/stepper/chat/ChatController.kt new file mode 100644 index 0000000..ddd11cc --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/chat/ChatController.kt @@ -0,0 +1,36 @@ +package com.likelionhgu.stepper.chat + +import com.likelionhgu.stepper.chat.response.ChatHistoryResponseWrapper +import com.likelionhgu.stepper.chat.response.ChatResponse +import com.likelionhgu.stepper.chat.response.ChatSummaryResponse +import com.likelionhgu.stepper.goal.GoalService +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class ChatController( + private val goalService: GoalService, + private val chatService: ChatService +) { + @PostMapping("/v1/goals/{goalId}/chats") + fun createChat(@PathVariable goalId: Long): ResponseEntity { + val goal = goalService.goalInfo(goalId) + val responseBody = chatService.initChat(goal) + return ResponseEntity.status(HttpStatus.CREATED).body(responseBody) + } + + @GetMapping("/v1/chats/{chatId}/history") + fun getChatHistory(@PathVariable chatId: String): ChatHistoryResponseWrapper { + return chatService.chatHistoryOf(chatId).let(ChatHistoryResponseWrapper.Companion::of) + } + + @PostMapping("/v1/chats/{chatId}/summary") + fun getChatSummary(@PathVariable chatId: String): ResponseEntity { + val responseBody = chatService.generateSummaryOf(chatId) + return ResponseEntity.status(HttpStatus.CREATED).body(responseBody) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/chat/ChatRole.kt b/src/main/kotlin/com/likelionhgu/stepper/chat/ChatRole.kt new file mode 100644 index 0000000..a0544a0 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/chat/ChatRole.kt @@ -0,0 +1,14 @@ +package com.likelionhgu.stepper.chat + +import com.likelionhgu.stepper.exception.ChatRoleNotSupportedException + +enum class ChatRole( + val alias: String +) { + USER("user"), CHATBOT("assistant"); + + companion object { + fun of(role: String): ChatRole = ChatRole.entries.find { it.alias == role } + ?: throw ChatRoleNotSupportedException("Role $role is not supported") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/chat/ChatService.kt b/src/main/kotlin/com/likelionhgu/stepper/chat/ChatService.kt new file mode 100644 index 0000000..fcf608e --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/chat/ChatService.kt @@ -0,0 +1,140 @@ +package com.likelionhgu.stepper.chat + +import com.likelionhgu.stepper.chat.response.ChatHistoryResponseWrapper +import com.likelionhgu.stepper.chat.response.ChatHistoryResponseWrapper.ChatHistoryResponse +import com.likelionhgu.stepper.chat.response.ChatResponse +import com.likelionhgu.stepper.chat.response.ChatSummaryResponse +import com.likelionhgu.stepper.exception.FailedAssistantException +import com.likelionhgu.stepper.exception.FailedCompletionException +import com.likelionhgu.stepper.exception.FailedMessageException +import com.likelionhgu.stepper.exception.FailedRunException +import com.likelionhgu.stepper.exception.FailedThreadException +import com.likelionhgu.stepper.goal.Goal +import com.likelionhgu.stepper.openai.OpenAiProperties +import com.likelionhgu.stepper.openai.assistant.AssistantService +import com.likelionhgu.stepper.openai.assistant.message.MessageResponseWrapper +import com.likelionhgu.stepper.openai.assistant.request.AssistantRequest +import com.likelionhgu.stepper.openai.assistant.run.RunRequest +import com.likelionhgu.stepper.openai.assistant.thread.ThreadCreationRequest +import com.likelionhgu.stepper.openai.completion.CompletionService +import com.likelionhgu.stepper.openai.completion.request.CompletionRequest +import com.likelionhgu.stepper.websocket.MessagePayload +import org.slf4j.LoggerFactory +import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.stereotype.Service +import retrofit2.Call +import retrofit2.Response + +@Service +class ChatService( + private val assistantService: AssistantService, + private val completionService: CompletionService, + private val openAiProperties: OpenAiProperties, + private val redisTemplate: StringRedisTemplate +) { + + /** + * Initialize a chat with the goal. + * + * @param goal The goal to initialize the chat with. + * @return The thread ID of the chat. + */ + fun initChat(goal: Goal): ChatResponse { + val contents = openAiProperties.assistant.welcomeMessages + val requestBody = ThreadCreationRequest.withDefault(contents, goal.title) + + return assistantService.createThread(requestBody).resolve() + ?.let(ChatResponse.Companion::of) + ?: throw FailedThreadException("Failed to create a thread") + } + + /** + * Retrieve chat history of the given chatId. + * + * The chat history consists of messages between the user and the assistant. + * + * @param chatId The chatId to retrieve chat history. + * @return The chat history of the given chatId. + */ + fun chatHistoryOf(chatId: String): MessageResponseWrapper { + return assistantService.listMessagesOf(chatId).resolve() + ?: throw FailedThreadException("Failed to get chat history for thread $chatId") + } + + fun generateQuestion(chatId: String, message: MessagePayload): ChatHistoryResponse { + addMessageToThread(chatId, message) + runAssistantOn(chatId).also { runId -> + waitUntilRunComplete(chatId, runId) + } + return assistantService.listMessagesOf(chatId).resolve() + ?.let(ChatHistoryResponseWrapper.Companion::firstOf) + ?: throw FailedMessageException("Failed to retrieve messages of thread $chatId") + } + + private fun addMessageToThread(chatId: String, message: MessagePayload) { + logger.info("Adding message to thread $chatId") + assistantService.createMessageOf(chatId, message.toSimpleMessage()).resolve() + ?: throw FailedMessageException("Failed to add message to thread $chatId") + } + + private fun runAssistantOn(chatId: String): String { + logger.info("Running assistant on thread $chatId") + val run = assistantService.createRunOf(chatId, RunRequest(assistant())).resolve() + ?: throw FailedRunException("Failed to run assistant on thread $chatId") + return run.id + } + + private fun waitUntilRunComplete(chatId: String, runId: String) { + do { + Thread.sleep(1_000) + val run = assistantService.retrieveRunOf(chatId, runId).resolve() + ?: throw FailedRunException("Failed to retrieve run $runId of thread $chatId") + } while (run.status != "completed") + } + + private fun assistant(assistantName: String = DEFAULT_ASSISTANT_NAME): String { + val assistantKey = ASSISTANT_REDIS_KEY_PREFIX + assistantName + return redisTemplate.opsForValue().get(assistantKey) + ?: fetchAssistantOf(assistantName) + ?: createAssistant(assistantName) + } + + private fun fetchAssistantOf(assistantName: String): String? { + val res = assistantService.listAssistants().resolve() + ?: throw FailedAssistantException("Failed to fetch assistants") + + return res.data.find { it.name == assistantName }?.id + } + + private fun createAssistant(assistantName: String): String { + with(openAiProperties.assistant) { + val requestBody = AssistantRequest(modelType.id, instructions, assistantName) + return assistantService.createAssistant(requestBody).resolve() + ?.let { + redisTemplate.opsForValue().set(ASSISTANT_REDIS_KEY_PREFIX + assistantName, it.id) + it.id + } ?: throw FailedAssistantException("Failed to create assistant") + } + } + + fun generateSummaryOf(chatId: String): ChatSummaryResponse { + val chatHistory = chatHistoryOf(chatId).toSimpleMessage() + + with(openAiProperties.completion) { + val requestBody = CompletionRequest.of(modelType.id, instructions, chatHistory) + return completionService.createChatCompletion(requestBody).resolve() + ?.let(ChatSummaryResponse.Companion::of) + ?: throw FailedCompletionException("Failed to create completion for chat $chatId") + } + } + + companion object { + private const val ASSISTANT_REDIS_KEY_PREFIX = "openai:assistant:" + private const val DEFAULT_ASSISTANT_NAME = "default" + private val logger = LoggerFactory.getLogger(ChatService::class.java) + } +} + +private fun Call.resolve(): T? { + return execute().run(Response::body) +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatHistoryResponseWrapper.kt b/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatHistoryResponseWrapper.kt new file mode 100644 index 0000000..499ac3b --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatHistoryResponseWrapper.kt @@ -0,0 +1,31 @@ +package com.likelionhgu.stepper.chat.response + +import com.likelionhgu.stepper.chat.ChatRole +import com.likelionhgu.stepper.openai.assistant.message.MessageResponseWrapper + +data class ChatHistoryResponseWrapper(val messages: List) { + data class ChatHistoryResponse(val messageId: String, val role: ChatRole, val content: String) + + companion object { + fun of(response: MessageResponseWrapper): ChatHistoryResponseWrapper { + val messages = response.data.map { message -> + ChatHistoryResponse( + messageId = message.id, + role = ChatRole.of(message.role), + content = message.content.first().text.value + ) + }.reversed() + return ChatHistoryResponseWrapper(messages) + } + + fun firstOf(response: MessageResponseWrapper): ChatHistoryResponse { + with(response.data.first()) { + return ChatHistoryResponse( + messageId = id, + role = ChatRole.of(role), + content = content.first().text.value + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatResponse.kt b/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatResponse.kt new file mode 100644 index 0000000..7f7a731 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatResponse.kt @@ -0,0 +1,12 @@ +package com.likelionhgu.stepper.chat.response + +import com.likelionhgu.stepper.openai.assistant.thread.ThreadResponse + +data class ChatResponse(val chatId: String) { + + companion object { + fun of(response: ThreadResponse): ChatResponse { + return ChatResponse(response.id) + } + } +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatSummaryResponse.kt b/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatSummaryResponse.kt new file mode 100644 index 0000000..4ed20a8 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/chat/response/ChatSummaryResponse.kt @@ -0,0 +1,13 @@ +package com.likelionhgu.stepper.chat.response + +import com.likelionhgu.stepper.openai.completion.response.CompletionResponse + +data class ChatSummaryResponse(val content: String) { + + companion object { + fun of(response: CompletionResponse): ChatSummaryResponse { + val content = response.choices.first().message.content + return ChatSummaryResponse(content) + } + } +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/exception/ChatRoleNotSupportedException.kt b/src/main/kotlin/com/likelionhgu/stepper/exception/ChatRoleNotSupportedException.kt new file mode 100644 index 0000000..c3fd983 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/exception/ChatRoleNotSupportedException.kt @@ -0,0 +1,4 @@ +package com.likelionhgu.stepper.exception + +class ChatRoleNotSupportedException(message: String) : RuntimeException(message) { +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/exception/FailedAssistantException.kt b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedAssistantException.kt new file mode 100644 index 0000000..e3e7c01 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedAssistantException.kt @@ -0,0 +1,5 @@ +package com.likelionhgu.stepper.exception + +class FailedAssistantException(message: String) : RuntimeException(message) { + +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/exception/FailedCompletionException.kt b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedCompletionException.kt new file mode 100644 index 0000000..64cacd9 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedCompletionException.kt @@ -0,0 +1,5 @@ +package com.likelionhgu.stepper.exception + +class FailedCompletionException(message: String) : RuntimeException(message) { + +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/exception/FailedMessageException.kt b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedMessageException.kt new file mode 100644 index 0000000..7803569 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedMessageException.kt @@ -0,0 +1,5 @@ +package com.likelionhgu.stepper.exception + +class FailedMessageException(message: String) : RuntimeException(message) { + +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/exception/FailedRunException.kt b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedRunException.kt new file mode 100644 index 0000000..3db135b --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedRunException.kt @@ -0,0 +1,5 @@ +package com.likelionhgu.stepper.exception + +class FailedRunException(message: String) : RuntimeException(message) { + +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/exception/FailedThreadException.kt b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedThreadException.kt new file mode 100644 index 0000000..90f6689 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/exception/FailedThreadException.kt @@ -0,0 +1,4 @@ +package com.likelionhgu.stepper.exception + +class FailedThreadException(message: String) : RuntimeException(message) { +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/exception/ModelNotSupportedException.kt b/src/main/kotlin/com/likelionhgu/stepper/exception/ModelNotSupportedException.kt new file mode 100644 index 0000000..9ae0e4f --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/exception/ModelNotSupportedException.kt @@ -0,0 +1,4 @@ +package com.likelionhgu.stepper.exception + +class ModelNotSupportedException(message: String) : RuntimeException(message) { +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/goal/GoalService.kt b/src/main/kotlin/com/likelionhgu/stepper/goal/GoalService.kt index 8dd64dc..a33df0e 100644 --- a/src/main/kotlin/com/likelionhgu/stepper/goal/GoalService.kt +++ b/src/main/kotlin/com/likelionhgu/stepper/goal/GoalService.kt @@ -44,8 +44,8 @@ class GoalService( * @return Goal The `Goal` entity corresponding to the provided ID. * @throws GoalNotFoundException if no goal is found with the provided ID. */ - fun goalInfo(goalId: String): Goal { - return goalRepository.findById(goalId.toLong()).getOrNull() + fun goalInfo(goalId: Long): Goal { + return goalRepository.findById(goalId).getOrNull() ?: throw GoalNotFoundException("The goal with the id \"$goalId\" does not exist") } } diff --git a/src/main/kotlin/com/likelionhgu/stepper/journal/JournalController.kt b/src/main/kotlin/com/likelionhgu/stepper/journal/JournalController.kt index cb2e85d..d79008d 100644 --- a/src/main/kotlin/com/likelionhgu/stepper/journal/JournalController.kt +++ b/src/main/kotlin/com/likelionhgu/stepper/journal/JournalController.kt @@ -27,7 +27,7 @@ class JournalController( @PostMapping("/v1/goals/{goalId}/journals") fun writeJournal( @AuthenticationPrincipal user: CommonOAuth2Attribute, - @PathVariable goalId: String, + @PathVariable goalId: Long, @Valid @RequestBody journalRequest: JournalRequest ): ResponseEntity { val member = memberService.memberInfo(user.oauth2UserId) @@ -39,7 +39,7 @@ class JournalController( @GetMapping("/v1/goals/{goalId}/journals") fun displayJournals( - @PathVariable goalId: String, + @PathVariable goalId: Long, @RequestParam(required = false) sort: JournalSortType = JournalSortType.NEWEST, @RequestParam(required = false) q: String? ): ResponseEntity { diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/ModelType.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/ModelType.kt new file mode 100644 index 0000000..dc95c61 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/ModelType.kt @@ -0,0 +1,17 @@ +package com.likelionhgu.stepper.openai + +import com.likelionhgu.stepper.exception.ModelNotSupportedException + +enum class ModelType(val id: String) { + GPT_4O("gpt-4o"), + GPT_4O_MINI("gpt-4o-mini"); + + companion object { + fun of(model: String): ModelType { + return entries.find { supportedModel -> + val modelId = model.takeIf(String::isNotBlank) ?: GPT_4O_MINI.id + supportedModel.id == modelId + } ?: throw ModelNotSupportedException("Model $model is not supported") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/OpenAiConfig.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/OpenAiConfig.kt new file mode 100644 index 0000000..64dd564 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/OpenAiConfig.kt @@ -0,0 +1,92 @@ +package com.likelionhgu.stepper.openai + +import com.google.gson.FieldNamingPolicy +import com.google.gson.Gson +import com.likelionhgu.stepper.openai.assistant.AssistantService +import com.likelionhgu.stepper.openai.completion.CompletionService +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +@Configuration +@EnableConfigurationProperties(OpenAiProperties::class) +class OpenAiConfig( + private val openAiProperties: OpenAiProperties +) { + + @Bean + fun defaultClient(): OkHttpClient { + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + return OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .addInterceptor { chain -> + chain.request().newBuilder() + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .addHeader(HttpHeaders.AUTHORIZATION, "Bearer ${openAiProperties.apiKey}") + .build() + .let(chain::proceed) + } + .build() + } + + @Bean + fun assistantClient(): OkHttpClient { + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + return OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .addInterceptor { chain -> + chain.request().newBuilder() + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .addHeader(HttpHeaders.AUTHORIZATION, "Bearer ${openAiProperties.apiKey}") + .addHeader(OPENAI_BETA, "assistants=v2") + .build() + .let(chain::proceed) + } + .build() + } + + @Bean + fun completionService(defaultClient: OkHttpClient): CompletionService { + val snakeCasePolicy = Gson().newBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create() + + return Retrofit.Builder() + .client(defaultClient) + .baseUrl(OPENAI_BASE_URL) + .addConverterFactory(GsonConverterFactory.create(snakeCasePolicy)) + .build() + .create(CompletionService::class.java) + } + + @Bean + fun assistantService(assistantClient: OkHttpClient): AssistantService { + val snakeCasePolicy = Gson().newBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create() + + return Retrofit.Builder() + .client(assistantClient) + .baseUrl(OPENAI_BASE_URL) + .addConverterFactory(GsonConverterFactory.create(snakeCasePolicy)) + .build() + .create(AssistantService::class.java) + } + + companion object { + private const val OPENAI_BASE_URL = "https://api.openai.com" + private const val OPENAI_BETA = "OpenAI-Beta" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/OpenAiProperties.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/OpenAiProperties.kt new file mode 100644 index 0000000..b9f67ca --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/OpenAiProperties.kt @@ -0,0 +1,18 @@ +package com.likelionhgu.stepper.openai + +import com.likelionhgu.stepper.openai.assistant.AssistantProperties +import com.likelionhgu.stepper.openai.completion.CompletionProperties +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.NestedConfigurationProperty + +@ConfigurationProperties(prefix = "custom.openai") +data class OpenAiProperties( + val apiKey: String, + + @NestedConfigurationProperty + val assistant: AssistantProperties, + + @NestedConfigurationProperty + val completion: CompletionProperties +) { +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/SimpleMessage.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/SimpleMessage.kt new file mode 100644 index 0000000..db6d58c --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/SimpleMessage.kt @@ -0,0 +1,12 @@ +package com.likelionhgu.stepper.openai + +import com.likelionhgu.stepper.chat.ChatRole + +data class SimpleMessage(val role: String, val content: String) { + + companion object { + fun withDefaultRole(content: String): SimpleMessage { + return SimpleMessage(ChatRole.USER.alias, content) + } + } +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/AssistantProperties.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/AssistantProperties.kt new file mode 100644 index 0000000..6baf3b6 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/AssistantProperties.kt @@ -0,0 +1,12 @@ +package com.likelionhgu.stepper.openai.assistant + +import com.likelionhgu.stepper.openai.ModelType + + +class AssistantProperties( + val welcomeMessages: List, + val instructions: String, + model: String +) { + val modelType: ModelType = ModelType.of(model) +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/AssistantService.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/AssistantService.kt new file mode 100644 index 0000000..1fa255b --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/AssistantService.kt @@ -0,0 +1,42 @@ +package com.likelionhgu.stepper.openai.assistant + +import com.likelionhgu.stepper.openai.assistant.response.AssistantResponseWrapper.AssistantResponse +import com.likelionhgu.stepper.openai.SimpleMessage +import com.likelionhgu.stepper.openai.assistant.run.RunRequest +import com.likelionhgu.stepper.openai.assistant.run.RunResponse +import com.likelionhgu.stepper.openai.assistant.message.MessageResponseWrapper +import com.likelionhgu.stepper.openai.assistant.message.MessageResponseWrapper.MessageResponse +import com.likelionhgu.stepper.openai.assistant.request.AssistantRequest +import com.likelionhgu.stepper.openai.assistant.response.AssistantResponseWrapper +import com.likelionhgu.stepper.openai.assistant.thread.ThreadCreationRequest +import com.likelionhgu.stepper.openai.assistant.thread.ThreadResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +interface AssistantService { + + @POST("/v1/assistants") + fun createAssistant(@Body requestBody: AssistantRequest): Call + + @GET("/v1/assistants") + fun listAssistants(): Call + + @POST("/v1/threads") + fun createThread(@Body requestBody: ThreadCreationRequest): Call + + @GET("/v1/threads/{thread_id}/messages") + fun listMessagesOf(@Path("thread_id") threadId: String): Call + + @POST("/v1/threads/{thread_id}/messages") + fun createMessageOf(@Path("thread_id") threadId: String, @Body message: SimpleMessage): Call + + @POST("/v1/threads/{thread_id}/runs") + fun createRunOf(@Path("thread_id") threadId: String, @Body runRequest: RunRequest): Call + + @GET("/v1/threads/{thread_id}/runs/{run_id}") + fun retrieveRunOf(@Path("thread_id") threadId: String, @Path("run_id") runId: String): Call + +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/message/MessageResponseWrapper.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/message/MessageResponseWrapper.kt new file mode 100644 index 0000000..803942e --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/message/MessageResponseWrapper.kt @@ -0,0 +1,20 @@ +package com.likelionhgu.stepper.openai.assistant.message + +import com.likelionhgu.stepper.openai.SimpleMessage + +data class MessageResponseWrapper(val data: List) { + data class MessageResponse(val id: String, val role: String, val content: List) { + data class Content(val type: String, val text: Text) { + data class Text(val value: String) + } + } + + fun toSimpleMessage(): List { + return data.map { messageResponse -> + SimpleMessage( + role = messageResponse.role, + content = messageResponse.content.first().text.value + ) + } + } +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/request/AssistantRequest.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/request/AssistantRequest.kt new file mode 100644 index 0000000..04d3f7e --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/request/AssistantRequest.kt @@ -0,0 +1,8 @@ +package com.likelionhgu.stepper.openai.assistant.request + +data class AssistantRequest( + val model: String, + val instructions: String, + val name: String +) { +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/response/AssistantResponseWrapper.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/response/AssistantResponseWrapper.kt new file mode 100644 index 0000000..8b6a3ed --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/response/AssistantResponseWrapper.kt @@ -0,0 +1,10 @@ +package com.likelionhgu.stepper.openai.assistant.response + +data class AssistantResponseWrapper(val data: List) { + data class AssistantResponse( + val id: String, + val name: String, + val model: String, + val instructions: String + ) +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/run/RunRequest.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/run/RunRequest.kt new file mode 100644 index 0000000..2bc0cf1 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/run/RunRequest.kt @@ -0,0 +1,6 @@ +package com.likelionhgu.stepper.openai.assistant.run + +data class RunRequest( + val assistantId: String, +) { +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/run/RunResponse.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/run/RunResponse.kt new file mode 100644 index 0000000..7265b1c --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/run/RunResponse.kt @@ -0,0 +1,8 @@ +package com.likelionhgu.stepper.openai.assistant.run + +data class RunResponse( + val id: String, + val status: String, +) { + +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/thread/ThreadCreationRequest.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/thread/ThreadCreationRequest.kt new file mode 100644 index 0000000..9ee908d --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/thread/ThreadCreationRequest.kt @@ -0,0 +1,29 @@ +package com.likelionhgu.stepper.openai.assistant.thread + +data class ThreadCreationRequest( + val messages: List +) { + data class Message( + val role: String, + val content: String + ) { + companion object { + fun withAssistant(content: String): Message { + return Message(DEFAULT_ROLE, content) + } + } + } + + companion object { + private const val DEFAULT_ROLE = "assistant" + private const val GOAL_TITLE = "{goal_title}" + + fun withDefault(contents: List, goalTitle: String): ThreadCreationRequest { + val messages = contents.map { content -> + val replacedContent = content.replace(GOAL_TITLE, goalTitle) + Message.withAssistant(replacedContent) + } + return ThreadCreationRequest(messages) + } + } +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/thread/ThreadResponse.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/thread/ThreadResponse.kt new file mode 100644 index 0000000..d7281d0 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/assistant/thread/ThreadResponse.kt @@ -0,0 +1,3 @@ +package com.likelionhgu.stepper.openai.assistant.thread + +data class ThreadResponse(val id: String) diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/completion/CompletionProperties.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/completion/CompletionProperties.kt new file mode 100644 index 0000000..13c8172 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/completion/CompletionProperties.kt @@ -0,0 +1,11 @@ +package com.likelionhgu.stepper.openai.completion + +import com.likelionhgu.stepper.openai.ModelType + +class CompletionProperties( + val instructions: String, + model: String +) { + + val modelType: ModelType = ModelType.of(model) +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/completion/CompletionService.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/completion/CompletionService.kt new file mode 100644 index 0000000..d1e50cc --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/completion/CompletionService.kt @@ -0,0 +1,14 @@ +package com.likelionhgu.stepper.openai.completion + +import com.likelionhgu.stepper.openai.completion.request.CompletionRequest +import com.likelionhgu.stepper.openai.completion.response.CompletionResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface CompletionService { + + @POST("/v1/chat/completions") + fun createChatCompletion(@Body completionRequest: CompletionRequest): Call + +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/completion/request/CompletionRequest.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/completion/request/CompletionRequest.kt new file mode 100644 index 0000000..25348a0 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/completion/request/CompletionRequest.kt @@ -0,0 +1,21 @@ +package com.likelionhgu.stepper.openai.completion.request + +import com.likelionhgu.stepper.openai.SimpleMessage + +data class CompletionRequest( + val model: String, + val messages: List +) { + companion object { + fun of(model: String, instructions: String, chatHistory: List): CompletionRequest { + val systemMessage = SimpleMessage( + role = SYSTEM_ROLE, + content = instructions + ) + val messages = listOf(systemMessage).plus(chatHistory) + return CompletionRequest(model, messages) + } + + private const val SYSTEM_ROLE = "system" + } +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/openai/completion/response/CompletionResponse.kt b/src/main/kotlin/com/likelionhgu/stepper/openai/completion/response/CompletionResponse.kt new file mode 100644 index 0000000..916d20e --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/openai/completion/response/CompletionResponse.kt @@ -0,0 +1,7 @@ +package com.likelionhgu.stepper.openai.completion.response + +import com.likelionhgu.stepper.openai.SimpleMessage + +data class CompletionResponse(val id: String, val choices: List) { + data class Choice(val index: Int, val message: SimpleMessage) +} diff --git a/src/main/kotlin/com/likelionhgu/stepper/test/DemoController.kt b/src/main/kotlin/com/likelionhgu/stepper/test/DemoController.kt deleted file mode 100644 index f792457..0000000 --- a/src/main/kotlin/com/likelionhgu/stepper/test/DemoController.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.likelionhgu.stepper.test - -import com.likelionhgu.stepper.security.oauth2.CommonOAuth2Attribute -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RestController - -@RestController -class DemoController { - - @GetMapping("/") - fun hello(@AuthenticationPrincipal user: CommonOAuth2Attribute): String { - return "Hello, ${user.username ?: user.email}!" - } - - @PostMapping("/test") - fun test(): String { - return "Test" - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/websocket/MessageController.kt b/src/main/kotlin/com/likelionhgu/stepper/websocket/MessageController.kt new file mode 100644 index 0000000..ede3e42 --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/websocket/MessageController.kt @@ -0,0 +1,47 @@ +package com.likelionhgu.stepper.websocket + +import com.likelionhgu.stepper.chat.ChatService +import com.likelionhgu.stepper.chat.response.ChatHistoryResponseWrapper +import com.likelionhgu.stepper.chat.response.ChatHistoryResponseWrapper.ChatHistoryResponse +import org.springframework.messaging.handler.annotation.DestinationVariable +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.handler.annotation.Payload +import org.springframework.messaging.simp.annotation.SendToUser +import org.springframework.messaging.simp.annotation.SubscribeMapping +import org.springframework.stereotype.Controller + +@Controller +class MessageController( + private val chatService: ChatService, +) { + + /** + * Retrieve chat history of the given chatId on initial subscription + * + * Triggered when client subscribes to: /app/chats/{chatId}/history + * + * @param chatId the chatId to retrieve chat history + * @return the chat history of the given chatId + */ + @SubscribeMapping("/chats/{chatId}/history") + fun onSubscribe(@DestinationVariable chatId: String): ChatHistoryResponseWrapper { + return chatService.chatHistoryOf(chatId).let(ChatHistoryResponseWrapper.Companion::of) + } + + /** + * Deliver incoming message to the assistant. + * The message will be added to the chat history of the given chatId. + * + * Triggered when client sends message to: /app/chats/{chatId}/messages + * Return the message to the client who subscribed to: /user/queue/messages + * + * + * @param chatId the chatId where the message will be added + * @param message generated message from the assistant + */ + @MessageMapping("/chats/{chatId}/messages") + @SendToUser("/queue/messages") + fun onMessage(@DestinationVariable chatId: String, @Payload message: MessagePayload): ChatHistoryResponse { + return chatService.generateQuestion(chatId, message) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/websocket/MessagePayload.kt b/src/main/kotlin/com/likelionhgu/stepper/websocket/MessagePayload.kt new file mode 100644 index 0000000..12b9fce --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/websocket/MessagePayload.kt @@ -0,0 +1,12 @@ +package com.likelionhgu.stepper.websocket + +import com.likelionhgu.stepper.openai.SimpleMessage + +data class MessagePayload( + val content: String +) { + + fun toSimpleMessage(): SimpleMessage { + return SimpleMessage.withDefaultRole(content) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/likelionhgu/stepper/websocket/WebSocketConfig.kt b/src/main/kotlin/com/likelionhgu/stepper/websocket/WebSocketConfig.kt new file mode 100644 index 0000000..66b9edd --- /dev/null +++ b/src/main/kotlin/com/likelionhgu/stepper/websocket/WebSocketConfig.kt @@ -0,0 +1,32 @@ +package com.likelionhgu.stepper.websocket + +import com.likelionhgu.stepper.security.SecurityConfig +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer + +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfig( + private val clientProperties: SecurityConfig.ClientProperties +) : WebSocketMessageBrokerConfigurer { + + override fun configureMessageBroker(config: MessageBrokerRegistry) { + config.setApplicationDestinationPrefixes(DEFAULT_APP_DESTINATION) + config.enableSimpleBroker(DEFAULT_BROKER_DESTINATION) + } + + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + val allowedOrigins = clientProperties.allowedOrigin.toTypedArray() + registry.addEndpoint(ENDPOINT) + .setAllowedOrigins(*allowedOrigins) + } + + companion object { + private const val DEFAULT_APP_DESTINATION = "/app" + private const val DEFAULT_BROKER_DESTINATION = "/queue" + private const val ENDPOINT = "/stepper" + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cd13d6a..f145468 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,18 +1,21 @@ spring: profiles: - include: local + include: security: oauth2: client: registration: google: - client-id: # Required - client-secret: # Required + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} scope: email, profile data: redis: host: localhost password: + jpa: + hibernate: + ddl-auto: update datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:testdb @@ -34,4 +37,14 @@ custom: client: redirect-uri: http://localhost:3000/oauth2/redirect allowed-origin: - - http://localhost:3000 \ No newline at end of file + - http://localhost:3000 + openai: + api-key: ${OPENAI_API_KEY} + completion: + model: + instructions: + assistant: + model: + instructions: + welcome-messages: + - Message 1 diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml deleted file mode 100644 index 15cc9ba..0000000 --- a/src/test/resources/application.yml +++ /dev/null @@ -1,25 +0,0 @@ -spring: - profiles: - include: env - security: - oauth2: - client: - registration: - google: - client-id: ${GOOGLE_CLIENT_ID:test-id} - client-secret: ${GOOGLE_CLIENT_SECRET:test-secret} - scope: email, profile - data: - redis: - host: ${REDIS_HOST:localhost} - password: ${REDIS_PASSWORD} - datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:testdb - username: sa - password: -custom: - client: - redirect-uri: http://localhost:3000/oauth2/redirect - allowed-origin: - - http://localhost:3000 \ No newline at end of file