Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cu 861mwhnjx split kotlinx serialization #170

Merged
merged 9 commits into from
Jun 13, 2023
28 changes: 28 additions & 0 deletions core/TECHNICAL.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Technical

This document tracks technical decisions for the Core module,
such that we can document and backtrack our decisions.

There is a trade-off between building a lean and mean _core_ library,
and writing as much as common code in Kotlin Multiplatform as possible.

To achieve this we try to keep the dependencies on external dependencies as small as possible,
but we have a need for a couple _base_ dependencies which we document below.
The breakdown is only in terms of JVM, since that's where we _mostly_ care about this.

## Kotlin's dependency breakdown (JVM - common)

We include the following dependencies in our _core_ module to implement a _common_ layer to interact with LLMs.
The dependency on Kotlin Stdlib is unavoidable, since we use Kotlin as our main language.
We also require KotlinX Coroutines such that we can leverage the `suspend` keyword in our API and expose `Future` to the
Java/Scala API.
Additionally, we also have a need for a HTTP client, and a serialization framework. Here we use Ktor and KotlinX
Serialization respectively,
and Xef relies on the CIO engine for Ktor, which avoids any additional dependencies.

- kotlin-stdlib (1810 Kb = 1598 Kb + 212 Kb)
- kotlinx-coroutines-core (1608 Kb = 1442 Kb + 166 Kb)
- Ktor Client (288Kb)
- KotlinX Serialization (791 Kb)

Total: 4497 Kb
3 changes: 1 addition & 2 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ kotlin {
api(libs.bundles.ktor.client)
api(projects.xefTokenizer)

// TODO split to a separate module
implementation(libs.kotlinx.serialization.json)
// implementation(libs.arrow.fx.stm)

implementation(libs.uuid)
implementation(libs.klogging)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,125 +6,17 @@ package com.xebia.functional.xef.auto
import arrow.core.nonFatalOrThrow
import arrow.core.raise.catch
import com.xebia.functional.xef.AIError
import com.xebia.functional.xef.auto.serialization.buildJsonSchema
import com.xebia.functional.xef.llm.openai.LLMModel
import com.xebia.functional.xef.prompt.Prompt
import com.xebia.functional.xef.prompt.append
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer

/**
* Run a [question] describes the task you want to solve within the context of [AIScope]. Returns a
* value of [A] where [A] **has to be** annotated with [kotlinx.serialization.Serializable].
*
* @throws SerializationException if serializer cannot be created (provided [A] or its type argument
* is not serializable).
* @throws IllegalArgumentException if any of [A]'s type arguments contains star projection.
*/
@AiDsl
suspend inline fun <reified A> AIScope.prompt(
question: String,
json: Json = Json {
ignoreUnknownKeys = true
isLenient = true
},
maxDeserializationAttempts: Int = 5,
model: LLMModel = LLMModel.GPT_3_5_TURBO,
user: String = "testing",
echo: Boolean = false,
n: Int = 1,
temperature: Double = 0.0,
bringFromContext: Int = 10
): A =
prompt(
Prompt(question),
json,
maxDeserializationAttempts,
model,
user,
echo,
n,
temperature,
bringFromContext
)

/**
* Run a [prompt] describes the task you want to solve within the context of [AIScope]. Returns a
* value of [A] where [A] **has to be** annotated with [kotlinx.serialization.Serializable].
*
* @throws SerializationException if serializer cannot be created (provided [A] or its type argument
* is not serializable).
* @throws IllegalArgumentException if any of [A]'s type arguments contains star projection.
*/
@AiDsl
suspend inline fun <reified A> AIScope.prompt(
prompt: Prompt,
json: Json = Json {
ignoreUnknownKeys = true
isLenient = true
},
maxDeserializationAttempts: Int = 5,
model: LLMModel = LLMModel.GPT_3_5_TURBO,
user: String = "testing",
echo: Boolean = false,
n: Int = 1,
temperature: Double = 0.0,
bringFromContext: Int = 10
): A =
prompt(
prompt,
serializer(),
json,
maxDeserializationAttempts,
model,
user,
echo,
n,
temperature,
bringFromContext
)

@AiDsl
suspend fun <A> AIScope.prompt(
prompt: Prompt,
serializer: KSerializer<A>,
json: Json = Json {
ignoreUnknownKeys = true
isLenient = true
},
maxDeserializationAttempts: Int = 5,
model: LLMModel = LLMModel.GPT_3_5_TURBO,
user: String = "testing",
echo: Boolean = false,
n: Int = 1,
temperature: Double = 0.0,
bringFromContext: Int = 10,
minResponseTokens: Int = 500,
): A =
prompt(
prompt,
serializer.descriptor,
{ json.decodeFromString(serializer, it) },
maxDeserializationAttempts,
model,
user,
echo,
n,
temperature,
bringFromContext,
minResponseTokens
)

@AiDsl
@JvmName("promptWithSerializer")
suspend fun <A> AIScope.prompt(
prompt: Prompt,
descriptor: SerialDescriptor,
jsonSchema: String,
serializer: (json: String) -> A,
maxDeserializationAttempts: Int = 5,
model: LLMModel = LLMModel.GPT_3_5_TURBO,
Expand All @@ -135,7 +27,6 @@ suspend fun <A> AIScope.prompt(
bringFromContext: Int = 10,
minResponseTokens: Int = 500,
): A {
val jsonSchema = buildJsonSchema(descriptor, false)
val responseInstructions =
"""
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,6 @@ import com.xebia.functional.xef.llm.openai.LLMModel
import com.xebia.functional.xef.prompt.Prompt
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlinx.serialization.descriptors.SerialDescriptor

/**
* Run a [prompt] describes the images you want to generate within the context of [AIScope].
* Produces a [ImagesGenerationResponse] which then gets serialized to [A] through [prompt].
*
* @param prompt a [Prompt] describing the images you want to generate.
* @param size the size of the images to generate.
*/
suspend inline fun <reified A> AIScope.image(
prompt: String,
user: String = "testing",
size: String = "1024x1024",
bringFromContext: Int = 10
): A {
val imageResponse = images(prompt, user, 1, size, bringFromContext)
val url = imageResponse.data.firstOrNull() ?: throw AIError.NoResponse()
return prompt<A>(
"""|Instructions: Format this [URL] and [PROMPT] information in the desired JSON response format
|specified at the end of the message.
|[URL]:
|```
|$url
|```
|[PROMPT]:
|```
|$prompt
|```"""
.trimMargin()
)
}

/**
* Run a [prompt] describes the images you want to generate within the context of [AIScope]. Returns
Expand Down Expand Up @@ -100,7 +69,7 @@ suspend fun AIScope.images(
@JvmName("imageWithSerializer")
suspend fun <A> AIScope.image(
prompt: Prompt,
descriptor: SerialDescriptor,
jsonSchema: String,
serializer: (json: String) -> A,
maxDeserializationAttempts: Int = 5,
user: String = "testing",
Expand Down Expand Up @@ -128,7 +97,7 @@ suspend fun <A> AIScope.image(
|```"""
.trimMargin()
),
descriptor,
jsonSchema,
serializer,
maxDeserializationAttempts,
model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@ import com.xebia.functional.xef.env.OpenAIConfig
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.timeout
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.http.path
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

interface OpenAIClient {
suspend fun createCompletion(request: CompletionRequest): CompletionResult
Expand Down Expand Up @@ -112,8 +111,7 @@ class KtorOpenAIClient(private val config: OpenAIConfig) : OpenAIClient, AutoClo
}

private suspend inline fun <reified T> HttpResponse.bodyOrError(): T =
if (status == HttpStatusCode.OK) Json.decodeFromString(bodyAsText())
else throw OpenAIClientException(status, Json.decodeFromString(bodyAsText()))
if (status == HttpStatusCode.OK) body() else throw OpenAIClientException(status, body())

class OpenAIClientException(val httpStatusCode: HttpStatusCode, val error: Error) :
IllegalStateException(
Expand Down
2 changes: 1 addition & 1 deletion examples/kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ java {
}

dependencies {
implementation(projects.xefCore)
implementation(projects.xefKotlin)
implementation(projects.xefFilesystem)
implementation(projects.xefPdf)
implementation(projects.xefSql)
Expand Down
89 changes: 89 additions & 0 deletions kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import org.jetbrains.dokka.gradle.DokkaTask

repositories {
mavenCentral()
}

plugins {
base
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotest.multiplatform)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.spotless)
alias(libs.plugins.dokka)
alias(libs.plugins.arrow.gradle.publish)
alias(libs.plugins.semver.gradle)
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}

kotlin {
jvm()
js(IR) {
browser()
nodejs()
}

linuxX64()
macosX64()
macosArm64()
mingwX64()

sourceSets {
val commonMain by getting {
dependencies {
api(projects.xefCore)
}
}
}
}

spotless {
kotlin {
target("**/*.kt")
ktfmt().googleStyle()
}
}

tasks {
withType<Test>().configureEach {
maxParallelForks = Runtime.getRuntime().availableProcessors()
useJUnitPlatform()
testLogging {
setExceptionFormat("full")
setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError"))
}
}

withType<DokkaTask>().configureEach {
kotlin.sourceSets.forEach { kotlinSourceSet ->
dokkaSourceSets.named(kotlinSourceSet.name) {
perPackageOption {
matchingRegex.set(".*\\.internal.*")
suppress.set(true)
}
skipDeprecated.set(true)
reportUndocumented.set(false)
val baseUrl: String = checkNotNull(project.properties["pom.smc.url"]?.toString())

kotlinSourceSet.kotlin.srcDirs.filter { it.exists() }.forEach { srcDir ->
sourceLink {
localDirectory.set(srcDir)
remoteUrl.set(uri("$baseUrl/blob/main/${srcDir.relativeTo(rootProject.rootDir)}").toURL())
remoteLineSuffix.set("#L")
}
}
}
}
}
}

tasks.withType<AbstractPublishToMaven> {
dependsOn(tasks.withType<Sign>())
}
Loading