Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ ChatGPT is a large language model developed by OpenAI that is trained to generat

YChatGPT aims to abstract all API call logic from ChatGPT for multiple platforms.

## 🤝 Contributions

Feel free to make a suggestion or if you find any error in this project, please open an issue.

## License

```
Expand Down
8 changes: 8 additions & 0 deletions android-sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

plugins {
id("com.android.application")
kotlin("android")
Expand All @@ -12,6 +14,12 @@ android {
targetSdk = Config.TARGET_SDK_VERSION
versionCode = 1
versionName = "1.0"

val key = "api.key"
val apiKey = System
.getenv()
.getOrDefault(key, gradleLocalProperties(rootDir).getProperty(key))
buildConfigField("String", "API_KEY", "\"$apiKey\"")
}

buildFeatures {
Expand Down
2 changes: 2 additions & 0 deletions android-sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="false"
android:supportsRtl="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import co.yml.ychatgpt.ChatGpt

class MainActivity : ComponentActivity() {

private val chatGpt by lazy { ChatGpt.create(BuildConfig.API_KEY) }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Expand Down
8 changes: 8 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ object Versions {
const val KTOR = "2.2.2"
const val KOIN = "3.2.0"
const val MATERIAL_DESIGN = "1.6.1"
const val MOCKK = "1.12.3"
}

object Dependencies {
Expand All @@ -34,4 +35,11 @@ object Dependencies {
const val COMPOSE_MATERIAL = "androidx.compose.material:material:${Versions.COMPOSE_FOUNDATION}"
const val COMPOSE_ACTIVITY = "androidx.activity:activity-compose:${Versions.COMPOSE_ACTIVITY}"
}

object Test {
const val MOCKK_COMMON = "io.mockk:mockk-common:${Versions.MOCKK}"
const val MOCKK = "io.mockk:mockk:${Versions.MOCKK}"
const val KTOR = "io.ktor:ktor-client-mock:${Versions.KTOR}"
const val KOIN = "io.insert-koin:koin-test:${Versions.KOIN}"
}
}
10 changes: 9 additions & 1 deletion chat-gpt/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization") version Versions.KOTLIN
id("com.android.library")
id("maven-publish")
id("signing")
Expand Down Expand Up @@ -32,14 +33,21 @@ kotlin {
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(Dependencies.Test.MOCKK_COMMON)
implementation(Dependencies.Test.KTOR)
implementation(Dependencies.Test.KOIN)
}
}
val androidMain by getting {
dependencies {
implementation(Dependencies.Network.KTOR_ANDROID)
}
}
val androidTest by getting
val androidTest by getting {
dependencies {
implementation(Dependencies.Test.MOCKK)
}
}
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
Expand Down
52 changes: 47 additions & 5 deletions chat-gpt/src/commonMain/kotlin/co/yml/ychatgpt/ChatGpt.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,59 @@
package co.yml.ychatgpt

import co.yml.ychatgpt.entrypoint.ChatGptImpl
import co.yml.ychatgpt.entrypoint.CompletionParams
import co.yml.ychatgpt.data.exception.ChatGptException
import co.yml.ychatgpt.entrypoint.impl.ChatGptImpl
import co.yml.ychatgpt.entrypoint.model.CompletionParams
import kotlin.coroutines.cancellation.CancellationException
import kotlin.jvm.Synchronized
import kotlin.jvm.Volatile
import kotlin.native.concurrent.ThreadLocal

/**
* YChatGPT aims to abstract all API call logic from ChatGPT for multiple platforms.
*
* To initialize the SDK, just call the static method ChatGpt.create(apiKey), informing the api key.
* See [this](https://beta.openai.com/docs/api-reference/authentication) for more details on how
* to get the api key.
*/
interface ChatGpt {

/**
* The completions api can be used for a wide variety of tasks. You [input] some text as a
* prompt, and the model will generate a text completion that attempts to match whatever
* context or pattern you gave it. For example, if you give the API the prompt, "As Descartes
* said, I think, therefore", it will return the completion " I am" with high probability.
*
* @param input The prompt(s) to generate completions for.
*/
@Throws(CancellationException::class, ChatGptException::class)
suspend fun completion(input: String): String

suspend fun completion(input: String, completionParams: CompletionParams): String
/**
* The completions api can be used for a wide variety of tasks. You [input] some text as a
* prompt, and the model will generate a text completion that attempts to match whatever
* context or pattern you gave it. For example, if you give the API the prompt, "As Descartes
* said, I think, therefore", it will return the completion " I am" with high probability.
*
* You can configure the behavior of the completion editing the [completionParams].
*
* @param input The prompt(s) to generate completions for.
* @param completionParams Params to set the completion behaviour.
*/
@Throws(CancellationException::class, ChatGptException::class)
suspend fun completion(
input: String,
completionParams: CompletionParams = CompletionParams()
): String

@ThreadLocal
companion object {

@Volatile
private var instance: ChatGpt? = null

@Synchronized
fun create(apiKey: String): ChatGpt {
return ChatGptImpl(apiKey)
return instance ?: ChatGptImpl(apiKey).also { instance = it }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package co.yml.ychatgpt.data.api

import co.yml.ychatgpt.data.dto.CompletionDto
import co.yml.ychatgpt.data.dto.CompletionParamsDto
import co.yml.ychatgpt.data.infrastructure.ApiResult

internal interface ChatGptApi {

suspend fun completion(paramsDto: CompletionParamsDto): ApiResult<CompletionDto>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package co.yml.ychatgpt.data.api.impl

import co.yml.ychatgpt.data.api.ChatGptApi
import co.yml.ychatgpt.data.dto.CompletionDto
import co.yml.ychatgpt.data.dto.CompletionParamsDto
import co.yml.ychatgpt.data.infrastructure.ApiExecutor
import co.yml.ychatgpt.data.infrastructure.ApiResult
import io.ktor.http.HttpMethod

internal class ChatGptApiImpl(private val apiExecutor: ApiExecutor) : ChatGptApi {

override suspend fun completion(paramsDto: CompletionParamsDto): ApiResult<CompletionDto> {
return apiExecutor
.setEndpoint("v1/completions")
.setHttpMethod(HttpMethod.Post)
.setBody(paramsDto)
.execute()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package co.yml.ychatgpt.data.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class ChoiceDto(
@SerialName("text")
val text: String,
@SerialName("index")
val index: Int,
@SerialName("logprobs")
val logProbs: Int?,
@SerialName("finish_reason")
val finishReason: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package co.yml.ychatgpt.data.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class CompletionDto(
@SerialName("id")
val id: String,
@SerialName("model")
val model: String,
@SerialName("choices")
val choices: List<ChoiceDto>,
@SerialName("usage")
val usage: UsageDto,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package co.yml.ychatgpt.data.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class CompletionParamsDto(
@SerialName("model")
val model: String,
@SerialName("prompt")
val prompt: String,
@SerialName("max_tokens")
val maxTokens: Int,
@SerialName("temperature")
val temperature: Double,
@SerialName("top_p")
val topP: Double,
@SerialName("n")
val completionNumber: Int = 1,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package co.yml.ychatgpt.data.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class UsageDto(
@SerialName("prompt_tokens")
val promptToken: Int,
@SerialName("completion_tokens")
val completionTokens: Int,
@SerialName("total_tokens")
val totalTokens: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package co.yml.ychatgpt.data.exception

class ChatGptException(
message: String? = null,
cause: Throwable? = null,
var statusCode: Int? = null,
) : Exception(message, cause) {

constructor(
cause: Throwable?,
statusCode: Int? = null
) : this(message = null, cause = cause, statusCode = statusCode)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package co.yml.ychatgpt.data.infrastructure

import co.yml.ychatgpt.data.exception.ChatGptException
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.ResponseException
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.utils.EmptyContent
import io.ktor.http.HttpMethod
import io.ktor.http.isSuccess
import io.ktor.util.toMap
import kotlin.collections.set

internal class ApiExecutor(private val httpClient: HttpClient) {

private lateinit var endpoint: String

private lateinit var httpMethod: HttpMethod

private var body: Any = EmptyContent

private var query: HashMap<String, String> = HashMap()

fun setEndpoint(endpoint: String): ApiExecutor {
this.endpoint = endpoint
return this
}

fun setHttpMethod(httpMethod: HttpMethod): ApiExecutor {
this.httpMethod = httpMethod
return this
}

fun setBody(body: Any): ApiExecutor {
this.body = body
return this
}

fun addQuery(key: String, value: String): ApiExecutor {
this.query[key] = value
return this
}

fun addQuery(key: String, value: List<String>): ApiExecutor {
this.query[key] = value.joinToString(",")
return this
}

suspend inline fun <reified T> execute(): ApiResult<T> {
return try {
val response = httpClient.request(endpoint) {
url { query.forEach { parameters.append(it.key, it.value) } }
method = httpMethod
setBody(this@ApiExecutor.body)
}
return response.toApiResult()
} catch (responseException: ResponseException) {
responseException.toApiResult()
} catch (throwable: Throwable) {
throwable.toApiResult()
}
}

private suspend inline fun <reified T> HttpResponse.toApiResult(): ApiResult<T> {
val headers = this.headers.toMap()
val statusCode = this.status.value
return if (!this.status.isSuccess()) {
val exception = ChatGptException(null, statusCode)
ApiResult(null, headers, statusCode, exception)
} else {
ApiResult(this.body<T>(), headers, statusCode, null)
}
}

private fun <T> ResponseException.toApiResult(): ApiResult<T> {
return ApiResult(
statusCode = this.response.status.value,
exception = ChatGptException(this.cause, this.response.status.value)
)
}

private fun <T> Throwable.toApiResult(): ApiResult<T> {
return ApiResult(
statusCode = null,
exception = ChatGptException(this.cause)
)
}
}
Loading