From 67a810bf5cba748e396c09790ca90e9698be900d Mon Sep 17 00:00:00 2001 From: Koji Osugi Date: Mon, 6 Mar 2023 21:11:15 -0300 Subject: [PATCH 1/4] feat: Add ChatCompletions API --- .../commonMain/kotlin/co/yml/ychat/YChat.kt | 45 ++++++++++ .../co/yml/ychat/data/api/ChatGptApi.kt | 4 + .../yml/ychat/data/api/impl/ChatGptApiImpl.kt | 10 +++ .../ychat/data/dto/ChatCompletionParamsDto.kt | 20 +++++ .../data/dto/ChatCompletionsChoiceDto.kt | 14 ++++ .../yml/ychat/data/dto/ChatCompletionsDto.kt | 16 ++++ .../co/yml/ychat/data/dto/ChatMessageDto.kt | 12 +++ .../co/yml/ychat/di/module/LibraryModule.kt | 5 ++ .../domain/mapper/ChatCompletionsMapper.kt | 24 ++++++ .../domain/model/ChatCompletionsParams.kt | 10 +++ .../co/yml/ychat/domain/model/ChatMessage.kt | 12 +++ .../domain/usecases/ChatCompletionsUseCase.kt | 16 ++++ .../entrypoint/features/ChatCompletions.kt | 84 +++++++++++++++++++ .../entrypoint/impl/ChatCompletionsImpl.kt | 65 ++++++++++++++ .../co/yml/ychat/entrypoint/impl/YChatImpl.kt | 5 ++ 15 files changed, 342 insertions(+) create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionParamsDto.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsChoiceDto.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsDto.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatMessageDto.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/domain/mapper/ChatCompletionsMapper.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/ChatCompletionsParams.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/ChatMessage.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCase.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ChatCompletions.kt create mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/ChatCompletionsImpl.kt diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/YChat.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/YChat.kt index 8f7eb52..4bca17e 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/YChat.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/YChat.kt @@ -1,5 +1,6 @@ package co.yml.ychat +import co.yml.ychat.entrypoint.features.ChatCompletions import co.yml.ychat.entrypoint.features.Completion import co.yml.ychat.entrypoint.impl.YChatImpl import kotlin.jvm.JvmStatic @@ -32,6 +33,50 @@ interface YChat { */ fun completion(): Completion + /** + * The chatCompletions api generates a list of chat completions for the given input message. + * It uses machine learning algorithms to generate responses that match the context or pattern + * provided in the input message. + * + * You can configure various parameters to customize the chat completions, such as the model to use, + * the number of results to generate, and the maximum number of tokens allowed for the generated answer. + * + * Example usage: + * ``` + * val result = YChat.create(apiKey).chatCompletions() + * .setModel("gpt-3.5-turbo") + * .setResults(3) + * .setMaxTokens(1024) + * .execute("Hello, how are you?") + * ``` + * This would generate three chat completion strings based on the input message "Hello, how are you?" + * using the "gpt-3.5-turbo" model, with a maximum of 1024 tokens allowed for each generated answer. + * + * You can also use the `addMessage` method to provide additional context or information to the API, + * which can be used to restrict the generated responses to a certain topic or context. + * + * Example usage: + * ``` + * val result = YChat.create(apiKey).chatCompletions() + * .setModel("gpt-3.5-turbo") + * .setResults(3) + * .setMaxTokens(1024) + * .addMessage( + * role = "assistant", + * content = "You are a helpful assistant that only answers questions related to fitness" + * ) + * .execute("What is the best exercise for building muscle?") + * ``` + * This would generate three chat completion strings based on the input message "What is the + * best exercise for building muscle?", using the "gpt-3.5-turbo" model, with a maximum of 1024 + * tokens allowed for each generated answer. The `addMessage` method is used to provide context + * to the API, restricting the generated responses to questions related to fitness, since the + * assistant is set to only answer questions related to that topic. + * + * @return A new instance of the `ChatCompletions` class. + */ + fun chatCompletions(): ChatCompletions + /** * Callback is an interface used for handling the results of an operation. * It provides two methods, `onSuccess` and `onError`, for handling the success diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt index 157d6c6..4996e4c 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt @@ -1,5 +1,7 @@ package co.yml.ychat.data.api +import co.yml.ychat.data.dto.ChatCompletionParamsDto +import co.yml.ychat.data.dto.ChatCompletionsDto import co.yml.ychat.data.dto.CompletionDto import co.yml.ychat.data.dto.CompletionParamsDto import co.yml.ychat.data.infrastructure.ApiResult @@ -7,4 +9,6 @@ import co.yml.ychat.data.infrastructure.ApiResult internal interface ChatGptApi { suspend fun completion(paramsDto: CompletionParamsDto): ApiResult + + suspend fun chatCompletions(paramsDto: ChatCompletionParamsDto): ApiResult } diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt index bdee830..1135ed1 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt @@ -1,6 +1,8 @@ package co.yml.ychat.data.api.impl import co.yml.ychat.data.api.ChatGptApi +import co.yml.ychat.data.dto.ChatCompletionParamsDto +import co.yml.ychat.data.dto.ChatCompletionsDto import co.yml.ychat.data.dto.CompletionDto import co.yml.ychat.data.dto.CompletionParamsDto import co.yml.ychat.data.infrastructure.ApiExecutor @@ -16,4 +18,12 @@ internal class ChatGptApiImpl(private val apiExecutor: ApiExecutor) : ChatGptApi .setBody(paramsDto) .execute() } + + override suspend fun chatCompletions(paramsDto: ChatCompletionParamsDto): ApiResult { + return apiExecutor + .setEndpoint("v1/chat/completions") + .setHttpMethod(HttpMethod.Post) + .setBody(paramsDto) + .execute() + } } diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionParamsDto.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionParamsDto.kt new file mode 100644 index 0000000..1cb5add --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionParamsDto.kt @@ -0,0 +1,20 @@ +package co.yml.ychat.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ChatCompletionParamsDto( + @SerialName("model") + val model: String, + @SerialName("messages") + val messages: List, + @SerialName("max_tokens") + val maxTokens: Int, + @SerialName("temperature") + val temperature: Double, + @SerialName("top_p") + val topP: Double, + @SerialName("n") + val maxResults: Int = 1, +) diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsChoiceDto.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsChoiceDto.kt new file mode 100644 index 0000000..2d4b684 --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsChoiceDto.kt @@ -0,0 +1,14 @@ +package co.yml.ychat.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ChatCompletionsChoiceDto( + @SerialName("index") + val index: Int, + @SerialName("message") + val message: ChatMessageDto, + @SerialName("finish_reason") + val finishReason: String, +) diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsDto.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsDto.kt new file mode 100644 index 0000000..fc9094c --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsDto.kt @@ -0,0 +1,16 @@ +package co.yml.ychat.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ChatCompletionsDto( + @SerialName("id") + val id: String, + @SerialName("model") + val model: String, + @SerialName("choices") + val choices: List, + @SerialName("usage") + val usage: UsageDto, +) diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatMessageDto.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatMessageDto.kt new file mode 100644 index 0000000..690c91f --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatMessageDto.kt @@ -0,0 +1,12 @@ +package co.yml.ychat.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ChatMessageDto( + @SerialName("role") + val role: String, + @SerialName("content") + val content: String, +) diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt index 3bd47e4..4106d79 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt @@ -5,8 +5,11 @@ import co.yml.ychat.data.api.impl.ChatGptApiImpl import co.yml.ychat.data.infrastructure.ApiExecutor import co.yml.ychat.data.storage.ChatLogStorage import co.yml.ychat.di.provider.NetworkProvider +import co.yml.ychat.domain.usecases.ChatCompletionsUseCase import co.yml.ychat.domain.usecases.CompletionUseCase +import co.yml.ychat.entrypoint.features.ChatCompletions import co.yml.ychat.entrypoint.features.Completion +import co.yml.ychat.entrypoint.impl.ChatCompletionsImpl import co.yml.ychat.entrypoint.impl.CompletionImpl import kotlinx.coroutines.Dispatchers import org.koin.core.module.Module @@ -19,10 +22,12 @@ internal class LibraryModule(private val apiKey: String) { private val entrypointModule = module { factory { CompletionImpl(Dispatchers.Default, get()) } + factory { ChatCompletionsImpl(Dispatchers.Default, get()) } } private val domainModule = module { factory { CompletionUseCase(get(), get()) } + factory { ChatCompletionsUseCase(get()) } } private val dataModule = module { diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/mapper/ChatCompletionsMapper.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/mapper/ChatCompletionsMapper.kt new file mode 100644 index 0000000..12282ed --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/mapper/ChatCompletionsMapper.kt @@ -0,0 +1,24 @@ +package co.yml.ychat.domain.mapper + +import co.yml.ychat.data.dto.ChatCompletionParamsDto +import co.yml.ychat.data.dto.ChatCompletionsDto +import co.yml.ychat.data.dto.ChatMessageDto +import co.yml.ychat.domain.model.ChatCompletionsParams +import co.yml.ychat.domain.model.ChatMessage + +internal fun ChatCompletionsDto.toChatMessages(): List { + return this.choices.map { + ChatMessage(it.message.role, it.message.content) + } +} + +internal fun ChatCompletionsParams.toChatCompletionParamsDto(): ChatCompletionParamsDto { + return ChatCompletionParamsDto( + model = this.model, + messages = this.messages.map { ChatMessageDto(it.role, it.content) }, + maxTokens = this.maxTokens, + temperature = this.temperature, + topP = this.topP, + maxResults = this.maxResults, + ) +} diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/ChatCompletionsParams.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/ChatCompletionsParams.kt new file mode 100644 index 0000000..e87ab9e --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/ChatCompletionsParams.kt @@ -0,0 +1,10 @@ +package co.yml.ychat.domain.model + +internal data class ChatCompletionsParams( + var messages: ArrayList = arrayListOf(), + var model: String = "gpt-3.5-turbo", + var maxResults: Int = 1, + var maxTokens: Int = 4096, + var temperature: Double = 1.0, + var topP: Double = 1.0, +) diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/ChatMessage.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/ChatMessage.kt new file mode 100644 index 0000000..b736bf7 --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/ChatMessage.kt @@ -0,0 +1,12 @@ +package co.yml.ychat.domain.model + +/** + * Represents a message in a conversation, consisting of a [role] indicating the speaker + * (e.g., “system”, “user” or “assistant”), and the [content] of the message sent by the speaker. + * @property role The role of the speaker who sends the message. + * @property content The content of the message sent by the speaker. + */ +data class ChatMessage( + val role: String, + val content: String +) diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCase.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCase.kt new file mode 100644 index 0000000..e27f943 --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCase.kt @@ -0,0 +1,16 @@ +package co.yml.ychat.domain.usecases + +import co.yml.ychat.data.api.ChatGptApi +import co.yml.ychat.domain.mapper.toChatCompletionParamsDto +import co.yml.ychat.domain.mapper.toChatMessages +import co.yml.ychat.domain.model.ChatCompletionsParams +import co.yml.ychat.domain.model.ChatMessage + +internal data class ChatCompletionsUseCase(private val chatGptApi: ChatGptApi) { + + suspend fun requestChatCompletions(params: ChatCompletionsParams): List { + val requestDto = params.toChatCompletionParamsDto() + val response = chatGptApi.chatCompletions(requestDto) + return response.getBodyOrThrow().toChatMessages() + } +} diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ChatCompletions.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ChatCompletions.kt new file mode 100644 index 0000000..5f6e6a8 --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ChatCompletions.kt @@ -0,0 +1,84 @@ +package co.yml.ychat.entrypoint.features + +import co.yml.ychat.YChat +import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.domain.model.ChatMessage +import kotlin.coroutines.cancellation.CancellationException + +interface ChatCompletions { + + /** + * Sets the ID of the [model] to use. The default value is "gpt-3.5-turbo". + * + * @param model ID of the model to use. + * @return The updated [ChatCompletions] object with the new model ID. + */ + fun setModel(model: String): ChatCompletions + + /** + * Sets the [topP] value for nucleus sampling. The default value is 1.0. + * + * @param topP An alternative to sampling with temperature, called nucleus sampling, where the model + * considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens + * comprising the top 10% probability mass are considered. We generally recommend altering + * this or [setTemperature] but not both. + * @return The updated [ChatCompletions] object with the new top_p value. + */ + fun setTopP(topP: Double): ChatCompletions + + /** + * Sets the sampling [temperature] to use. The default value is 1.0. + * + * @param temperature What sampling temperature to use. Higher values means the model will take more + * risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a + * well-defined answer. We generally recommend altering this or [setTopP] but not both. + * @return The updated [ChatCompletions] object with the new sampling temperature. + */ + fun setTemperature(temperature: Double): ChatCompletions + + /** + * Sets the number of chat completion choices to generate for each input message. + * The default value is 1. + * + * @param results How many chat completion choices to generate for each input message. + * @return The updated [ChatCompletions] object with the new number of results. + */ + fun setMaxResults(results: Int): ChatCompletions + + /** + * The default value of [tokens] is 4096. + * + * @param tokens The maximum number of tokens allowed for the generated answer. + * @return The updated [ChatCompletions] object with the new maximum number of tokens. + */ + fun setMaxTokens(tokens: Int): ChatCompletions + + /** + * Adds a new message to the ongoing conversation with the given [role] and [content]. + * + * @param role The role of the speaker who sends the message (either “system”, “user” or “assistant”). + * @param content The content of the message sent by the speaker. + * @return The updated [ChatCompletions] object with the newly added message. + */ + fun addMessage(role: String, content: String): ChatCompletions + + /** + * Generates a list of chat completions for the given user [content]. + * + * @param content The content of the user input. + * @return A list of chat message objects representing the possible completions. + * @throws CancellationException if the operation is cancelled. + * @throws ChatGptException if there is an error generating chat completions. + */ + @Throws(CancellationException::class, ChatGptException::class) + suspend fun execute(content: String): List + + /** + * Generates a list of chat completions for the given [content] message and passes it to the + * provided callback. + * + * @param content The content of the user input. + * @param callback The callback to receive the list of possible completions. + */ + fun execute(content: String, callback: YChat.Callback>) +} diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/ChatCompletionsImpl.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/ChatCompletionsImpl.kt new file mode 100644 index 0000000..124e44b --- /dev/null +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/ChatCompletionsImpl.kt @@ -0,0 +1,65 @@ +package co.yml.ychat.entrypoint.impl + +import co.yml.ychat.YChat +import co.yml.ychat.domain.model.ChatCompletionsParams +import co.yml.ychat.domain.model.ChatMessage +import co.yml.ychat.domain.usecases.ChatCompletionsUseCase +import co.yml.ychat.entrypoint.features.ChatCompletions +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +internal class ChatCompletionsImpl( + private val dispatcher: CoroutineDispatcher, + private val chatCompletionsUseCase: ChatCompletionsUseCase, +) : ChatCompletions { + + private val scope by lazy { CoroutineScope(SupervisorJob() + dispatcher) } + + private var params: ChatCompletionsParams = ChatCompletionsParams() + + override fun setModel(model: String): ChatCompletions { + params.model = model + return this + } + + override fun setTopP(topP: Double): ChatCompletions { + params.topP = topP + return this + } + + override fun setTemperature(temperature: Double): ChatCompletions { + params.temperature = temperature + return this + } + + override fun setMaxResults(results: Int): ChatCompletions { + params.maxResults = results + return this + } + + override fun setMaxTokens(tokens: Int): ChatCompletions { + params.maxTokens = tokens + return this + } + + override fun addMessage(role: String, content: String): ChatCompletions { + params.messages.add(ChatMessage(role, content)) + return this + } + + override suspend fun execute(content: String): List { + params.messages.add(ChatMessage("user", content)) + return chatCompletionsUseCase.requestChatCompletions(params) + .also { params.messages.addAll(it) } + } + + override fun execute(content: String, callback: YChat.Callback>) { + scope.launch { + runCatching { execute(content) } + .onSuccess { callback.onSuccess(it) } + .onFailure { callback.onError(it) } + } + } +} diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/YChatImpl.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/YChatImpl.kt index e8e2031..324a95f 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/YChatImpl.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/YChatImpl.kt @@ -2,6 +2,7 @@ package co.yml.ychat.entrypoint.impl import co.yml.ychat.YChat import co.yml.ychat.di.module.LibraryModule +import co.yml.ychat.entrypoint.features.ChatCompletions import co.yml.ychat.entrypoint.features.Completion import org.koin.core.KoinApplication @@ -17,4 +18,8 @@ internal class YChatImpl(apiKey: String) : YChat { override fun completion(): Completion { return koinApp.koin.get() } + + override fun chatCompletions(): ChatCompletions { + return koinApp.koin.get() + } } From a15d5865196d057d79df47c3613eb132a414f862 Mon Sep 17 00:00:00 2001 From: Koji Osugi Date: Mon, 6 Mar 2023 21:12:10 -0300 Subject: [PATCH 2/4] test: Create ChatCompletions tests --- .../co/yml/ychat/di/LibraryModuleTest.kt | 4 + .../domain/model/ChatCompletionsParamsTest.kt | 21 +++++ .../{ => model}/CompletionParamsTest.kt | 3 +- .../usecases/ChatCompletionsUseCaseTest.kt | 80 +++++++++++++++++++ .../{ => usecases}/CompletionUseCaseTest.kt | 3 +- ...pletionIntegrationTest.kt => YChatTest.kt} | 39 ++++++++- .../kotlin/infrastructure/MockStorage.kt | 6 ++ 7 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 ychat/src/commonTest/kotlin/co/yml/ychat/domain/model/ChatCompletionsParamsTest.kt rename ychat/src/commonTest/kotlin/co/yml/ychat/domain/{ => model}/CompletionParamsTest.kt (86%) create mode 100644 ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCaseTest.kt rename ychat/src/commonTest/kotlin/co/yml/ychat/domain/{ => usecases}/CompletionUseCaseTest.kt (97%) rename ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/{CompletionIntegrationTest.kt => YChatTest.kt} (57%) diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/di/LibraryModuleTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/di/LibraryModuleTest.kt index 2eb72c1..fef41e9 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/di/LibraryModuleTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/di/LibraryModuleTest.kt @@ -4,7 +4,9 @@ import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.infrastructure.ApiExecutor import co.yml.ychat.data.storage.ChatLogStorage import co.yml.ychat.di.module.LibraryModule +import co.yml.ychat.domain.usecases.ChatCompletionsUseCase import co.yml.ychat.domain.usecases.CompletionUseCase +import co.yml.ychat.entrypoint.features.ChatCompletions import co.yml.ychat.entrypoint.features.Completion import io.ktor.client.HttpClient import kotlin.test.AfterTest @@ -35,5 +37,7 @@ class LibraryModuleTest : KoinTest { get() get() get() + get() + get() } } diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/model/ChatCompletionsParamsTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/model/ChatCompletionsParamsTest.kt new file mode 100644 index 0000000..35a91b1 --- /dev/null +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/model/ChatCompletionsParamsTest.kt @@ -0,0 +1,21 @@ +package co.yml.ychat.domain.model + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ChatCompletionsParamsTest { + + @Test + fun `on ChatCompletionsParams verify default values`() { + // arrange + val params = ChatCompletionsParams() + + // assert + assertEquals(true, params.messages.isEmpty()) + assertEquals("gpt-3.5-turbo", params.model) + assertEquals(4096, params.maxTokens) + assertEquals(1, params.maxResults) + assertEquals(1.0, params.temperature) + assertEquals(1.0, params.topP) + } +} diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/CompletionParamsTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/model/CompletionParamsTest.kt similarity index 86% rename from ychat/src/commonTest/kotlin/co/yml/ychat/domain/CompletionParamsTest.kt rename to ychat/src/commonTest/kotlin/co/yml/ychat/domain/model/CompletionParamsTest.kt index a27ae31..c2f5254 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/CompletionParamsTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/model/CompletionParamsTest.kt @@ -1,6 +1,5 @@ -package co.yml.ychat.domain +package co.yml.ychat.domain.model -import co.yml.ychat.domain.model.CompletionParams import kotlin.test.Test import kotlin.test.assertEquals diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCaseTest.kt new file mode 100644 index 0000000..93ac91d --- /dev/null +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCaseTest.kt @@ -0,0 +1,80 @@ +package co.yml.ychat.domain.usecases + +import co.yml.ychat.data.api.ChatGptApi +import co.yml.ychat.data.dto.ChatCompletionsChoiceDto +import co.yml.ychat.data.dto.ChatCompletionsDto +import co.yml.ychat.data.dto.ChatMessageDto +import co.yml.ychat.data.dto.UsageDto +import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.data.infrastructure.ApiResult +import co.yml.ychat.domain.model.ChatCompletionsParams +import co.yml.ychat.domain.model.ChatMessage +import io.mockk.coEvery +import io.mockk.mockk +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking + +class ChatCompletionsUseCaseTest { + + private lateinit var chatCompletionsUseCase: ChatCompletionsUseCase + + private val chatGptApiMock = mockk() + + @BeforeTest + fun setup() { + chatCompletionsUseCase = ChatCompletionsUseCase(chatGptApiMock) + } + + @Test + fun `on requestChatCompletions when request succeed then should return formatted result`() { + // arrange + val messages = arrayListOf(ChatMessage("user", "Say this is a test.")) + val chatCompletionDto = buildChatCompletionsDto("This indeed a test") + val params = ChatCompletionsParams(messages) + val apiResult = ApiResult(body = chatCompletionDto) + coEvery { chatGptApiMock.chatCompletions(any()) } returns apiResult + + // act + val result = runBlocking { chatCompletionsUseCase.requestChatCompletions(params) } + + // assert + assertEquals("This indeed a test", result.last().content) + } + + @Test + fun `on requestChatCompletions when not request succeed then should throw an exception`() { + // arrange + val messages = arrayListOf(ChatMessage("user", "Say this is a test.")) + val params = ChatCompletionsParams(messages) + val apiResult = ApiResult(exception = ChatGptException()) + coEvery { chatGptApiMock.chatCompletions(any()) } returns apiResult + + // act + val result = + runCatching { runBlocking { chatCompletionsUseCase.requestChatCompletions(params) } } + + // assert + assertEquals(true, result.exceptionOrNull() is ChatGptException) + } + + private fun buildChatCompletionsDto(answer: String): ChatCompletionsDto { + return ChatCompletionsDto( + id = "1", + model = "gpt", + choices = listOf( + ChatCompletionsChoiceDto( + index = 0, + message = ChatMessageDto("assistance", answer), + finishReason = "" + ) + ), + usage = UsageDto( + promptToken = 1, + completionTokens = 1, + totalTokens = 1 + ) + ) + } +} diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/CompletionUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/CompletionUseCaseTest.kt similarity index 97% rename from ychat/src/commonTest/kotlin/co/yml/ychat/domain/CompletionUseCaseTest.kt rename to ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/CompletionUseCaseTest.kt index ec0b767..96a7c88 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/CompletionUseCaseTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/CompletionUseCaseTest.kt @@ -1,4 +1,4 @@ -package co.yml.ychat.domain +package co.yml.ychat.domain.usecases import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.ChoiceDto @@ -7,7 +7,6 @@ import co.yml.ychat.data.dto.UsageDto import co.yml.ychat.data.exception.ChatGptException import co.yml.ychat.data.infrastructure.ApiResult import co.yml.ychat.data.storage.ChatLogStorage -import co.yml.ychat.domain.usecases.CompletionUseCase import co.yml.ychat.domain.model.CompletionParams import io.mockk.coEvery import io.mockk.every diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/CompletionIntegrationTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/YChatTest.kt similarity index 57% rename from ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/CompletionIntegrationTest.kt rename to ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/YChatTest.kt index e175021..c0e264c 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/CompletionIntegrationTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/YChatTest.kt @@ -15,19 +15,29 @@ import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import org.koin.dsl.module -class CompletionIntegrationTest { +class YChatTest { private lateinit var yChat: YChatImpl @BeforeTest fun setup() { - yChat = YChat.create("api.key") as YChatImpl + yChat = YChatImpl("api.key") + } + + @Test + fun `on create method should return singleton instance`() { + // arrange + val yChatOne = YChat.create("api.key") + val yChatTwo = YChat.create("api.key") + + // assert + assertEquals(yChatOne, yChatTwo) } @Test fun `on completion execute method should return result successfully`() { // arrange - val textResult = "This in indeed a text" + val textResult = "This in indeed a test" val completionSuccessResult = MockStorage.completionSuccessResult(textResult) mockHttpEngine(completionSuccessResult) @@ -39,7 +49,28 @@ class CompletionIntegrationTest { } // assert - assertEquals("This in indeed a text", result) + assertEquals("This in indeed a test", result) + } + + @Test + fun `on chatCompletions execute method should return result successfully`() { + // arrange + val textResult = "This in indeed a test" + val chatCompletionSuccessResult = MockStorage.chatCompletionsSuccessResult(textResult) + mockHttpEngine(chatCompletionSuccessResult) + + // act + val result = runBlocking { + yChat.chatCompletions() + .setMaxResults(1) + .setTemperature(1.0) + .setTopP(1.0) + .execute("Say this is a test") + .first().content + } + + // assert + assertEquals("This in indeed a test", result) } private fun mockHttpEngine(result: String) { diff --git a/ychat/src/commonTest/kotlin/infrastructure/MockStorage.kt b/ychat/src/commonTest/kotlin/infrastructure/MockStorage.kt index d9cc84d..8c81b1c 100644 --- a/ychat/src/commonTest/kotlin/infrastructure/MockStorage.kt +++ b/ychat/src/commonTest/kotlin/infrastructure/MockStorage.kt @@ -7,4 +7,10 @@ object MockStorage { "\"choices\":[{\"text\":\"\n\n$text\",\"index\":0,\"logprobs\":null," + "\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":8,\"completion_tokens\":9," + "\"total_tokens\":17}}" + + fun chatCompletionsSuccessResult(text: String) = "{\"id\":\"1\",\"object\":\"chat.completion\"," + + "\"created\":1678144798,\"model\":\"gpt-3.5-turbo-0301\"," + + "\"usage\":{\"prompt_tokens\":13,\"completion_tokens\":12,\"total_tokens\":25}," + + "\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"$text\"}," + + "\"finish_reason\":\"stop\",\"index\":0}]}" } From cfc9aeed5eedcdb0041b112a35c9343f302fbfc2 Mon Sep 17 00:00:00 2001 From: Koji Osugi Date: Tue, 7 Mar 2023 13:59:00 -0300 Subject: [PATCH 3/4] refactor: Adjust android and iOS sample to use ChatCompletion entrypoint --- .../java/co/yml/ychat/android/MainViewModel.kt | 15 ++++++++++----- .../ViewModel/CompletionViewModel.swift | 15 ++++++++------- .../ychat/data/dto/ChatCompletionsChoiceDto.kt | 2 +- .../ychat/entrypoint/impl/ChatCompletionsImpl.kt | 2 +- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/sample/android/src/main/java/co/yml/ychat/android/MainViewModel.kt b/sample/android/src/main/java/co/yml/ychat/android/MainViewModel.kt index f7c7c57..90daeda 100644 --- a/sample/android/src/main/java/co/yml/ychat/android/MainViewModel.kt +++ b/sample/android/src/main/java/co/yml/ychat/android/MainViewModel.kt @@ -11,6 +11,15 @@ import kotlinx.coroutines.launch class MainViewModel(private val chatGpt: YChat) : ViewModel() { + private val chatCompletions by lazy { + chatGpt.chatCompletions() + .setMaxTokens(MAX_TOKENS) + .addMessage( + role = "assistant", + content = "You are a helpful assistant." + ) + } + private val _items = mutableStateListOf() val items = _items @@ -47,11 +56,7 @@ class MainViewModel(private val chatGpt: YChat) : ViewModel() { private suspend fun requestCompletion(message: String): String { return try { - chatGpt.completion() - .setInput(message) - .saveHistory(false) - .setMaxTokens(MAX_TOKENS) - .execute() + chatCompletions.execute(message).last().content } catch (e: Exception) { e.message ?: ERROR } diff --git a/sample/ios/YChatApp/Features/Completion/ViewModel/CompletionViewModel.swift b/sample/ios/YChatApp/Features/Completion/ViewModel/CompletionViewModel.swift index cd4809b..f13f22f 100644 --- a/sample/ios/YChatApp/Features/Completion/ViewModel/CompletionViewModel.swift +++ b/sample/ios/YChatApp/Features/Completion/ViewModel/CompletionViewModel.swift @@ -10,9 +10,14 @@ import YChat import Foundation internal final class CompletionViewModel: ObservableObject { - private var chatGpt: YChat { + private var chatCompletions: ChatCompletions = YChatCompanion.shared.create(apiKey: Config.apiKey) - } + .chatCompletions() + .setMaxTokens(tokens: 1024) + .addMessage( + role: "assistant", + content: "You are a helpful assistant." + ) @Published var message: String = "" @@ -32,11 +37,7 @@ internal final class CompletionViewModel: ObservableObject { cleanLastMessage() addLoading() do { - let result = try await chatGpt.completion() - .setInput(input: input) - .setMaxTokens(tokens: 1024) - .saveHistory(isSaveHistory: false) - .execute() + let result = try await chatCompletions.execute(content: input)[0].content removeLoading() addAIMessage(message: result) } catch { diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsChoiceDto.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsChoiceDto.kt index 2d4b684..26e3806 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsChoiceDto.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/ChatCompletionsChoiceDto.kt @@ -10,5 +10,5 @@ internal data class ChatCompletionsChoiceDto( @SerialName("message") val message: ChatMessageDto, @SerialName("finish_reason") - val finishReason: String, + val finishReason: String?, ) diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/ChatCompletionsImpl.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/ChatCompletionsImpl.kt index 124e44b..54477f5 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/ChatCompletionsImpl.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/ChatCompletionsImpl.kt @@ -50,7 +50,7 @@ internal class ChatCompletionsImpl( } override suspend fun execute(content: String): List { - params.messages.add(ChatMessage("user", content)) + addMessage("user", content) return chatCompletionsUseCase.requestChatCompletions(params) .also { params.messages.addAll(it) } } From d58093b4b1e29bd471504ce3d63557238d83b11f Mon Sep 17 00:00:00 2001 From: Koji Osugi Date: Tue, 7 Mar 2023 14:38:42 -0300 Subject: [PATCH 4/4] docs: Add an example in the JVM sample --- sample/jvm/README.md | 17 +++++- .../ychat/jvm/controller/YChatController.java | 17 +++++- .../ychat/jvm/services/CompletionService.java | 44 --------------- .../yml/ychat/jvm/services/YChatService.java | 56 +++++++++++++++++++ 4 files changed, 86 insertions(+), 48 deletions(-) delete mode 100644 sample/jvm/src/main/java/co/yml/ychat/jvm/services/CompletionService.java create mode 100644 sample/jvm/src/main/java/co/yml/ychat/jvm/services/YChatService.java diff --git a/sample/jvm/README.md b/sample/jvm/README.md index fe9c074..e870312 100644 --- a/sample/jvm/README.md +++ b/sample/jvm/README.md @@ -23,8 +23,23 @@ This endpoint generates text based on the provided prompt. ##### Parameters: -- input: The prompt for generating text. +- `input`: The prompt for generating text. ##### Example: `GET http://localhost:8080/api/ychat/completion?input="What is 1 + 1?"` + +### Chat Completions Endpoint + +This endpoint generates text based on the provided prompt and a specified topic. The generated text will be related to the topic provided. + +##### Endpoint: http://localhost:[port_number]/api/ychat/chat-completions + +##### Parameters: + +- `input`: The prompt for generating text. +- `topic`: The topic to limit the response to. + +##### Example: + +`GET http://localhost:8080/api/ychat/chat-completions?input="Tell me an exercise plan"&topic=fitness` \ No newline at end of file diff --git a/sample/jvm/src/main/java/co/yml/ychat/jvm/controller/YChatController.java b/sample/jvm/src/main/java/co/yml/ychat/jvm/controller/YChatController.java index 6932b6e..4e1b1ac 100644 --- a/sample/jvm/src/main/java/co/yml/ychat/jvm/controller/YChatController.java +++ b/sample/jvm/src/main/java/co/yml/ychat/jvm/controller/YChatController.java @@ -6,24 +6,35 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import co.yml.ychat.jvm.services.CompletionService; +import co.yml.ychat.jvm.services.YChatService; @RestController @RequestMapping("api/ychat") public class YChatController { @Autowired - private CompletionService completionService; + private YChatService YChatService; @GetMapping("completion") public ResponseEntity completion( @RequestParam(value = "input", defaultValue = Defaults.COMPLETION_INPUT) String input ) throws Exception { - String result = completionService.getCompletionAnswer(input); + String result = YChatService.getCompletionAnswer(input); + return ResponseEntity.ok(result); + } + + @GetMapping("chat-completions") + public ResponseEntity chatCompletions( + @RequestParam(value = "input", defaultValue = Defaults.CHAT_COMPLETION_INPUT) String input, + @RequestParam(value = "topic", defaultValue = Defaults.CHAT_COMPLETION_TOPIC) String topic + ) throws Exception { + String result = YChatService.getChatCompletionsAnswer(input, topic); return ResponseEntity.ok(result); } private static class Defaults { static final String COMPLETION_INPUT = "Say this is a test."; + static final String CHAT_COMPLETION_INPUT = "Tell me one strength exercise"; + static final String CHAT_COMPLETION_TOPIC = "fitness"; } } diff --git a/sample/jvm/src/main/java/co/yml/ychat/jvm/services/CompletionService.java b/sample/jvm/src/main/java/co/yml/ychat/jvm/services/CompletionService.java deleted file mode 100644 index a9585f2..0000000 --- a/sample/jvm/src/main/java/co/yml/ychat/jvm/services/CompletionService.java +++ /dev/null @@ -1,44 +0,0 @@ -package co.yml.ychat.jvm.services; - -import org.jetbrains.annotations.NotNull; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import java.util.concurrent.CompletableFuture; -import co.yml.ychat.YChat; - -@Service -public class CompletionService { - - @Autowired - private YChat ychat; - - private static final int MAX_TOKENS = 512; - - public String getCompletionAnswer(String input) throws Exception { - final CompletableFuture future = new CompletableFuture<>(); - ychat.completion() - .setMaxTokens(MAX_TOKENS) - .setInput(input) - .execute(new CompletionCallbackResult(future)); - return future.get(); - } - - private static class CompletionCallbackResult implements YChat.Callback { - - private final CompletableFuture future; - - CompletionCallbackResult(CompletableFuture future) { - this.future = future; - } - - @Override - public void onSuccess(String result) { - future.complete(result); - } - - @Override - public void onError(@NotNull Throwable throwable) { - future.completeExceptionally(throwable); - } - } -} diff --git a/sample/jvm/src/main/java/co/yml/ychat/jvm/services/YChatService.java b/sample/jvm/src/main/java/co/yml/ychat/jvm/services/YChatService.java new file mode 100644 index 0000000..a6aaa34 --- /dev/null +++ b/sample/jvm/src/main/java/co/yml/ychat/jvm/services/YChatService.java @@ -0,0 +1,56 @@ +package co.yml.ychat.jvm.services; + +import co.yml.ychat.domain.model.ChatMessage; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.concurrent.CompletableFuture; +import co.yml.ychat.YChat; + +@Service +public class YChatService { + + @Autowired + private YChat ychat; + + private static final int MAX_TOKENS = 512; + + public String getCompletionAnswer(String input) throws Exception { + final CompletableFuture future = new CompletableFuture<>(); + ychat.completion() + .setMaxTokens(MAX_TOKENS) + .setInput(input) + .execute(new CompletionCallbackResult<>(future)); + return future.get(); + } + + public String getChatCompletionsAnswer(String input, String topic) throws Exception { + final CompletableFuture> future = new CompletableFuture<>(); + String content = "You are a helpful assistant the only answer questions related to " + topic; + ychat.chatCompletions() + .setMaxTokens(MAX_TOKENS) + .addMessage("system", content) + .execute(input, new CompletionCallbackResult<>(future)); + return future.get().get(0).getContent(); + } + + private static class CompletionCallbackResult implements YChat.Callback { + + private final CompletableFuture future; + + CompletionCallbackResult(CompletableFuture future) { + this.future = future; + } + + @Override + public void onSuccess(T result) { + future.complete(result); + } + + @Override + public void onError(@NotNull Throwable throwable) { + future.completeExceptionally(throwable); + } + } +}