Skip to content

Commit 66c00eb

Browse files
nomisRevYawolf
andauthored
Cu 861mwhnjx split kotlinx serialization (#170)
* Split KotlinX Serialization into its own module * Remove unrelated changes * Add technical documentation * Remove SerializationStrategy * SpotlessApply * Fixes after merge --------- Co-authored-by: yago <[email protected]>
1 parent 7668a51 commit 66c00eb

File tree

13 files changed

+328
-286
lines changed

13 files changed

+328
-286
lines changed

core/TECHNICAL.MD

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Technical
2+
3+
This document tracks technical decisions for the Core module,
4+
such that we can document and backtrack our decisions.
5+
6+
There is a trade-off between building a lean and mean _core_ library,
7+
and writing as much as common code in Kotlin Multiplatform as possible.
8+
9+
To achieve this we try to keep the dependencies on external dependencies as small as possible,
10+
but we have a need for a couple _base_ dependencies which we document below.
11+
The breakdown is only in terms of JVM, since that's where we _mostly_ care about this.
12+
13+
## Kotlin's dependency breakdown (JVM - common)
14+
15+
We include the following dependencies in our _core_ module to implement a _common_ layer to interact with LLMs.
16+
The dependency on Kotlin Stdlib is unavoidable, since we use Kotlin as our main language.
17+
We also require KotlinX Coroutines such that we can leverage the `suspend` keyword in our API and expose `Future` to the
18+
Java/Scala API.
19+
Additionally, we also have a need for a HTTP client, and a serialization framework. Here we use Ktor and KotlinX
20+
Serialization respectively,
21+
and Xef relies on the CIO engine for Ktor, which avoids any additional dependencies.
22+
23+
- kotlin-stdlib (1810 Kb = 1598 Kb + 212 Kb)
24+
- kotlinx-coroutines-core (1608 Kb = 1442 Kb + 166 Kb)
25+
- Ktor Client (288Kb)
26+
- KotlinX Serialization (791 Kb)
27+
28+
Total: 4497 Kb

core/build.gradle.kts

+1-2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ kotlin {
6767
api(libs.bundles.ktor.client)
6868
api(projects.xefTokenizer)
6969

70-
// TODO split to a separate module
71-
implementation(libs.kotlinx.serialization.json)
70+
// implementation(libs.arrow.fx.stm)
7271

7372
implementation(libs.uuid)
7473
implementation(libs.klogging)

core/src/commonMain/kotlin/com/xebia/functional/xef/auto/DeserializerLLMAgent.kt

+1-110
Original file line numberDiff line numberDiff line change
@@ -6,125 +6,17 @@ package com.xebia.functional.xef.auto
66
import arrow.core.nonFatalOrThrow
77
import arrow.core.raise.catch
88
import com.xebia.functional.xef.AIError
9-
import com.xebia.functional.xef.auto.serialization.buildJsonSchema
109
import com.xebia.functional.xef.llm.openai.LLMModel
1110
import com.xebia.functional.xef.prompt.Prompt
1211
import com.xebia.functional.xef.prompt.append
1312
import kotlin.jvm.JvmMultifileClass
1413
import kotlin.jvm.JvmName
15-
import kotlinx.serialization.KSerializer
16-
import kotlinx.serialization.SerializationException
17-
import kotlinx.serialization.descriptors.SerialDescriptor
18-
import kotlinx.serialization.json.Json
19-
import kotlinx.serialization.serializer
20-
21-
/**
22-
* Run a [question] describes the task you want to solve within the context of [AIScope]. Returns a
23-
* value of [A] where [A] **has to be** annotated with [kotlinx.serialization.Serializable].
24-
*
25-
* @throws SerializationException if serializer cannot be created (provided [A] or its type argument
26-
* is not serializable).
27-
* @throws IllegalArgumentException if any of [A]'s type arguments contains star projection.
28-
*/
29-
@AiDsl
30-
suspend inline fun <reified A> AIScope.prompt(
31-
question: String,
32-
json: Json = Json {
33-
ignoreUnknownKeys = true
34-
isLenient = true
35-
},
36-
maxDeserializationAttempts: Int = 5,
37-
model: LLMModel = LLMModel.GPT_3_5_TURBO,
38-
user: String = "testing",
39-
echo: Boolean = false,
40-
n: Int = 1,
41-
temperature: Double = 0.0,
42-
bringFromContext: Int = 10
43-
): A =
44-
prompt(
45-
Prompt(question),
46-
json,
47-
maxDeserializationAttempts,
48-
model,
49-
user,
50-
echo,
51-
n,
52-
temperature,
53-
bringFromContext
54-
)
55-
56-
/**
57-
* Run a [prompt] describes the task you want to solve within the context of [AIScope]. Returns a
58-
* value of [A] where [A] **has to be** annotated with [kotlinx.serialization.Serializable].
59-
*
60-
* @throws SerializationException if serializer cannot be created (provided [A] or its type argument
61-
* is not serializable).
62-
* @throws IllegalArgumentException if any of [A]'s type arguments contains star projection.
63-
*/
64-
@AiDsl
65-
suspend inline fun <reified A> AIScope.prompt(
66-
prompt: Prompt,
67-
json: Json = Json {
68-
ignoreUnknownKeys = true
69-
isLenient = true
70-
},
71-
maxDeserializationAttempts: Int = 5,
72-
model: LLMModel = LLMModel.GPT_3_5_TURBO,
73-
user: String = "testing",
74-
echo: Boolean = false,
75-
n: Int = 1,
76-
temperature: Double = 0.0,
77-
bringFromContext: Int = 10
78-
): A =
79-
prompt(
80-
prompt,
81-
serializer(),
82-
json,
83-
maxDeserializationAttempts,
84-
model,
85-
user,
86-
echo,
87-
n,
88-
temperature,
89-
bringFromContext
90-
)
91-
92-
@AiDsl
93-
suspend fun <A> AIScope.prompt(
94-
prompt: Prompt,
95-
serializer: KSerializer<A>,
96-
json: Json = Json {
97-
ignoreUnknownKeys = true
98-
isLenient = true
99-
},
100-
maxDeserializationAttempts: Int = 5,
101-
model: LLMModel = LLMModel.GPT_3_5_TURBO,
102-
user: String = "testing",
103-
echo: Boolean = false,
104-
n: Int = 1,
105-
temperature: Double = 0.0,
106-
bringFromContext: Int = 10,
107-
minResponseTokens: Int = 500,
108-
): A =
109-
prompt(
110-
prompt,
111-
serializer.descriptor,
112-
{ json.decodeFromString(serializer, it) },
113-
maxDeserializationAttempts,
114-
model,
115-
user,
116-
echo,
117-
n,
118-
temperature,
119-
bringFromContext,
120-
minResponseTokens
121-
)
12214

12315
@AiDsl
12416
@JvmName("promptWithSerializer")
12517
suspend fun <A> AIScope.prompt(
12618
prompt: Prompt,
127-
descriptor: SerialDescriptor,
19+
jsonSchema: String,
12820
serializer: (json: String) -> A,
12921
maxDeserializationAttempts: Int = 5,
13022
model: LLMModel = LLMModel.GPT_3_5_TURBO,
@@ -135,7 +27,6 @@ suspend fun <A> AIScope.prompt(
13527
bringFromContext: Int = 10,
13628
minResponseTokens: Int = 500,
13729
): A {
138-
val jsonSchema = buildJsonSchema(descriptor, false)
13930
val responseInstructions =
14031
"""
14132
|

core/src/commonMain/kotlin/com/xebia/functional/xef/auto/ImageGenerationAgent.kt

+2-33
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,6 @@ import com.xebia.functional.xef.llm.openai.LLMModel
1010
import com.xebia.functional.xef.prompt.Prompt
1111
import kotlin.jvm.JvmMultifileClass
1212
import kotlin.jvm.JvmName
13-
import kotlinx.serialization.descriptors.SerialDescriptor
14-
15-
/**
16-
* Run a [prompt] describes the images you want to generate within the context of [AIScope].
17-
* Produces a [ImagesGenerationResponse] which then gets serialized to [A] through [prompt].
18-
*
19-
* @param prompt a [Prompt] describing the images you want to generate.
20-
* @param size the size of the images to generate.
21-
*/
22-
suspend inline fun <reified A> AIScope.image(
23-
prompt: String,
24-
user: String = "testing",
25-
size: String = "1024x1024",
26-
bringFromContext: Int = 10
27-
): A {
28-
val imageResponse = images(prompt, user, 1, size, bringFromContext)
29-
val url = imageResponse.data.firstOrNull() ?: throw AIError.NoResponse()
30-
return prompt<A>(
31-
"""|Instructions: Format this [URL] and [PROMPT] information in the desired JSON response format
32-
|specified at the end of the message.
33-
|[URL]:
34-
|```
35-
|$url
36-
|```
37-
|[PROMPT]:
38-
|```
39-
|$prompt
40-
|```"""
41-
.trimMargin()
42-
)
43-
}
4413

4514
/**
4615
* Run a [prompt] describes the images you want to generate within the context of [AIScope]. Returns
@@ -100,7 +69,7 @@ suspend fun AIScope.images(
10069
@JvmName("imageWithSerializer")
10170
suspend fun <A> AIScope.image(
10271
prompt: Prompt,
103-
descriptor: SerialDescriptor,
72+
jsonSchema: String,
10473
serializer: (json: String) -> A,
10574
maxDeserializationAttempts: Int = 5,
10675
user: String = "testing",
@@ -128,7 +97,7 @@ suspend fun <A> AIScope.image(
12897
|```"""
12998
.trimMargin()
13099
),
131-
descriptor,
100+
jsonSchema,
132101
serializer,
133102
maxDeserializationAttempts,
134103
model,

core/src/commonMain/kotlin/com/xebia/functional/xef/llm/openai/OpenAIClient.kt

+2-4
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,18 @@ import com.xebia.functional.xef.env.OpenAIConfig
55
import io.github.oshai.kotlinlogging.KLogger
66
import io.github.oshai.kotlinlogging.KotlinLogging
77
import io.ktor.client.HttpClient
8+
import io.ktor.client.call.body
89
import io.ktor.client.plugins.HttpTimeout
910
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
1011
import io.ktor.client.plugins.defaultRequest
1112
import io.ktor.client.plugins.timeout
1213
import io.ktor.client.request.post
1314
import io.ktor.client.statement.HttpResponse
14-
import io.ktor.client.statement.bodyAsText
1515
import io.ktor.http.HttpStatusCode
1616
import io.ktor.http.path
1717
import io.ktor.serialization.kotlinx.json.*
1818
import kotlinx.serialization.SerialName
1919
import kotlinx.serialization.Serializable
20-
import kotlinx.serialization.json.Json
2120

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

114113
private suspend inline fun <reified T> HttpResponse.bodyOrError(): T =
115-
if (status == HttpStatusCode.OK) Json.decodeFromString(bodyAsText())
116-
else throw OpenAIClientException(status, Json.decodeFromString(bodyAsText()))
114+
if (status == HttpStatusCode.OK) body() else throw OpenAIClientException(status, body())
117115

118116
class OpenAIClientException(val httpStatusCode: HttpStatusCode, val error: Error) :
119117
IllegalStateException(

examples/kotlin/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ java {
1616
}
1717

1818
dependencies {
19-
implementation(projects.xefCore)
19+
implementation(projects.xefKotlin)
2020
implementation(projects.xefFilesystem)
2121
implementation(projects.xefPdf)
2222
implementation(projects.xefSql)

kotlin/build.gradle.kts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import org.jetbrains.dokka.gradle.DokkaTask
2+
3+
repositories {
4+
mavenCentral()
5+
}
6+
7+
plugins {
8+
base
9+
alias(libs.plugins.kotlin.multiplatform)
10+
alias(libs.plugins.kotest.multiplatform)
11+
alias(libs.plugins.kotlinx.serialization)
12+
alias(libs.plugins.spotless)
13+
alias(libs.plugins.dokka)
14+
alias(libs.plugins.arrow.gradle.publish)
15+
alias(libs.plugins.semver.gradle)
16+
}
17+
18+
java {
19+
sourceCompatibility = JavaVersion.VERSION_11
20+
targetCompatibility = JavaVersion.VERSION_11
21+
toolchain {
22+
languageVersion = JavaLanguageVersion.of(11)
23+
}
24+
}
25+
26+
kotlin {
27+
jvm()
28+
js(IR) {
29+
browser()
30+
nodejs()
31+
}
32+
33+
linuxX64()
34+
macosX64()
35+
macosArm64()
36+
mingwX64()
37+
38+
sourceSets {
39+
val commonMain by getting {
40+
dependencies {
41+
api(projects.xefCore)
42+
}
43+
}
44+
}
45+
}
46+
47+
spotless {
48+
kotlin {
49+
target("**/*.kt")
50+
ktfmt().googleStyle()
51+
}
52+
}
53+
54+
tasks {
55+
withType<Test>().configureEach {
56+
maxParallelForks = Runtime.getRuntime().availableProcessors()
57+
useJUnitPlatform()
58+
testLogging {
59+
setExceptionFormat("full")
60+
setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError"))
61+
}
62+
}
63+
64+
withType<DokkaTask>().configureEach {
65+
kotlin.sourceSets.forEach { kotlinSourceSet ->
66+
dokkaSourceSets.named(kotlinSourceSet.name) {
67+
perPackageOption {
68+
matchingRegex.set(".*\\.internal.*")
69+
suppress.set(true)
70+
}
71+
skipDeprecated.set(true)
72+
reportUndocumented.set(false)
73+
val baseUrl: String = checkNotNull(project.properties["pom.smc.url"]?.toString())
74+
75+
kotlinSourceSet.kotlin.srcDirs.filter { it.exists() }.forEach { srcDir ->
76+
sourceLink {
77+
localDirectory.set(srcDir)
78+
remoteUrl.set(uri("$baseUrl/blob/main/${srcDir.relativeTo(rootProject.rootDir)}").toURL())
79+
remoteLineSuffix.set("#L")
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
tasks.withType<AbstractPublishToMaven> {
88+
dependsOn(tasks.withType<Sign>())
89+
}

0 commit comments

Comments
 (0)