diff --git a/core/TECHNICAL.MD b/core/TECHNICAL.MD new file mode 100644 index 000000000..22b88ceb0 --- /dev/null +++ b/core/TECHNICAL.MD @@ -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 diff --git a/core/build.gradle.kts b/core/build.gradle.kts index a937f98b1..52d637b15 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -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) diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/DeserializerLLMAgent.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/DeserializerLLMAgent.kt index a3e81d54f..ebbb32070 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/DeserializerLLMAgent.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/DeserializerLLMAgent.kt @@ -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 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 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 AIScope.prompt( - prompt: Prompt, - serializer: KSerializer, - 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 AIScope.prompt( prompt: Prompt, - descriptor: SerialDescriptor, + jsonSchema: String, serializer: (json: String) -> A, maxDeserializationAttempts: Int = 5, model: LLMModel = LLMModel.GPT_3_5_TURBO, @@ -135,7 +27,6 @@ suspend fun AIScope.prompt( bringFromContext: Int = 10, minResponseTokens: Int = 500, ): A { - val jsonSchema = buildJsonSchema(descriptor, false) val responseInstructions = """ | diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/ImageGenerationAgent.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/ImageGenerationAgent.kt index 3818820e5..855fc58e8 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/ImageGenerationAgent.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/ImageGenerationAgent.kt @@ -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 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( - """|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 @@ -100,7 +69,7 @@ suspend fun AIScope.images( @JvmName("imageWithSerializer") suspend fun AIScope.image( prompt: Prompt, - descriptor: SerialDescriptor, + jsonSchema: String, serializer: (json: String) -> A, maxDeserializationAttempts: Int = 5, user: String = "testing", @@ -128,7 +97,7 @@ suspend fun AIScope.image( |```""" .trimMargin() ), - descriptor, + jsonSchema, serializer, maxDeserializationAttempts, model, diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/openai/OpenAIClient.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/openai/OpenAIClient.kt index e65e7695c..369528af8 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/openai/OpenAIClient.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/openai/OpenAIClient.kt @@ -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 @@ -112,8 +111,7 @@ class KtorOpenAIClient(private val config: OpenAIConfig) : OpenAIClient, AutoClo } private suspend inline fun 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( diff --git a/examples/kotlin/build.gradle.kts b/examples/kotlin/build.gradle.kts index f19e19708..224ba01ef 100644 --- a/examples/kotlin/build.gradle.kts +++ b/examples/kotlin/build.gradle.kts @@ -16,7 +16,7 @@ java { } dependencies { - implementation(projects.xefCore) + implementation(projects.xefKotlin) implementation(projects.xefFilesystem) implementation(projects.xefPdf) implementation(projects.xefSql) diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts new file mode 100644 index 000000000..f50c4850f --- /dev/null +++ b/kotlin/build.gradle.kts @@ -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().configureEach { + maxParallelForks = Runtime.getRuntime().availableProcessors() + useJUnitPlatform() + testLogging { + setExceptionFormat("full") + setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError")) + } + } + + withType().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 { + dependsOn(tasks.withType()) +} \ No newline at end of file diff --git a/kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/DeserializerLLMAgent.kt b/kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/DeserializerLLMAgent.kt new file mode 100644 index 000000000..5af1b87ff --- /dev/null +++ b/kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/DeserializerLLMAgent.kt @@ -0,0 +1,111 @@ +package com.xebia.functional.xef.auto + +import com.xebia.functional.xef.auto.serialization.encodeJsonSchema +import com.xebia.functional.xef.llm.openai.LLMModel +import com.xebia.functional.xef.prompt.Prompt +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +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 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 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 AIScope.prompt( + prompt: Prompt, + serializer: KSerializer, + 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, + encodeJsonSchema(serializer.descriptor), + { json.decodeFromString(serializer, it) }, + maxDeserializationAttempts, + model, + user, + echo, + n, + temperature, + bringFromContext, + minResponseTokens + ) diff --git a/kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/ImageGenerationAgent.kt b/kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/ImageGenerationAgent.kt new file mode 100644 index 000000000..979f7addc --- /dev/null +++ b/kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/ImageGenerationAgent.kt @@ -0,0 +1,35 @@ +package com.xebia.functional.xef.auto + +import com.xebia.functional.xef.AIError +import com.xebia.functional.xef.llm.openai.ImagesGenerationResponse +import com.xebia.functional.xef.prompt.Prompt + +/** + * 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 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( + """|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() + ) +} diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/serialization/JsonSchema.kt b/kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/serialization/JsonSchema.kt similarity index 74% rename from core/src/commonMain/kotlin/com/xebia/functional/xef/auto/serialization/JsonSchema.kt rename to kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/serialization/JsonSchema.kt index 1aa151d8c..c7ddf8e5b 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/auto/serialization/JsonSchema.kt +++ b/kotlin/src/commonMain/kotlin/com/xebia/functional/xef/auto/serialization/JsonSchema.kt @@ -11,10 +11,8 @@ which states the following: // TODO: We should consider a fork and maintain it ourselves. */ -import kotlin.jvm.JvmOverloads import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialInfo -import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.SerialDescriptor @@ -52,24 +50,6 @@ enum class JsonType(jsonType: String) { override fun toString(): String = json.content } -@OptIn(ExperimentalSerializationApi::class) -fun SerialDescriptor.sample(): JsonElement { - val properties = - elementNames.zip(elementDescriptors).associate { (name, descriptor) -> - name to - when (descriptor.kind.jsonType) { - JsonType.ARRAY -> JsonArray(listOf(descriptor.sample())) - JsonType.NUMBER -> JsonUnquotedLiteral("") - JsonType.STRING -> JsonUnquotedLiteral("") - JsonType.BOOLEAN -> JsonUnquotedLiteral("") - JsonType.OBJECT -> descriptor.sample() - JsonType.OBJECT_SEALED -> descriptor.sample() - JsonType.OBJECT_MAP -> descriptor.sample() - } - } - return JsonObject(properties) -} - @Target() annotation class JsonSchema { /** Description of this property */ @@ -133,52 +113,12 @@ annotation class JsonSchema { annotation class NoDefinition } -/** - * Adds a `$schema` property with the provided [url] that points to the Json Schema, this can be a - * File location or a HTTP URL - * - * This is so when you serialize your [value] it will use [url] as it's Json Schema for code - * completion. - */ -fun Json.encodeWithSchema(serializer: SerializationStrategy, value: T, url: String): String { - val json = encodeToJsonElement(serializer, value) as JsonObject - val append = mapOf("\$schema" to JsonPrimitive(url)) - - return encodeToString(JsonObject.serializer(), JsonObject(append + json)) -} - -/** - * Stringifies the provided [descriptor] with [buildJsonSchema] - * - * @param generateDefinitions Should this generate definitions by default - */ -fun Json.encodeToSchema(descriptor: SerialDescriptor, generateDefinitions: Boolean = true): String { - return encodeToString(JsonObject.serializer(), buildJsonSchema(descriptor, generateDefinitions)) -} +fun encodeJsonSchema(descriptor: SerialDescriptor): String = + Json.encodeToString(JsonObject.serializer(), buildJsonSchema(descriptor)) -/** - * Stringifies the provided [serializer] with [buildJsonSchema], same as doing - * - * ```kotlin - * json.encodeToSchema(serializer.descriptor) - * ``` - * - * @param generateDefinitions Should this generate definitions by default - */ -fun Json.encodeToSchema( - serializer: SerializationStrategy<*>, - generateDefinitions: Boolean = true -): String { - return encodeToSchema(serializer.descriptor, generateDefinitions) -} - -/** - * Creates a Json Schema using the provided [descriptor] - * - * @param autoDefinitions automatically generate definitions by default - */ -@JvmOverloads -fun buildJsonSchema(descriptor: SerialDescriptor, autoDefinitions: Boolean = false): JsonObject { +/** Creates a Json Schema using the provided [descriptor] */ +private fun buildJsonSchema(descriptor: SerialDescriptor): JsonObject { + val autoDefinitions = false val prepend = mapOf("\$schema" to JsonPrimitive("http://json-schema.org/draft-07/schema")) val definitions = JsonSchemaDefinitions(autoDefinitions) val root = descriptor.createJsonSchema(descriptor.annotations, definitions) @@ -187,26 +127,11 @@ fun buildJsonSchema(descriptor: SerialDescriptor, autoDefinitions: Boolean = fal return JsonObject(prepend + root + append) } -/** - * Creates a Json Schema using the provided [serializer], same as doing - * `jsonSchema(serializer.descriptor)` - * - * @param generateDefinitions Should this generate definitions by default - */ -fun buildJsonSchema( - serializer: SerializationStrategy<*>, - generateDefinitions: Boolean = true -): JsonObject { - return buildJsonSchema(serializer.descriptor, generateDefinitions) -} - -@PublishedApi -internal inline val SerialDescriptor.jsonLiteral +private inline val SerialDescriptor.jsonLiteral inline get() = kind.jsonType.json -@PublishedApi -internal val SerialKind.jsonType: JsonType - get() = +private inline val SerialKind.jsonType: JsonType + inline get() = when (this) { StructureKind.LIST -> JsonType.ARRAY StructureKind.MAP -> JsonType.OBJECT_MAP @@ -224,12 +149,10 @@ internal val SerialKind.jsonType: JsonType else -> JsonType.OBJECT } -internal inline fun List.lastOfInstance(): T? { - return filterIsInstance().lastOrNull() -} +private inline fun List.lastOfInstance(): T? = + filterIsInstance().lastOrNull() -@PublishedApi -internal fun SerialDescriptor.jsonSchemaObject(definitions: JsonSchemaDefinitions): JsonObject { +private fun SerialDescriptor.jsonSchemaObject(definitions: JsonSchemaDefinitions): JsonObject { val properties = mutableMapOf() val required = mutableListOf() @@ -255,7 +178,7 @@ internal fun SerialDescriptor.jsonSchemaObject(definitions: JsonSchemaDefinition } } -internal fun SerialDescriptor.jsonSchemaObjectMap(definitions: JsonSchemaDefinitions): JsonObject { +private fun SerialDescriptor.jsonSchemaObjectMap(definitions: JsonSchemaDefinitions): JsonObject { return jsonSchemaElement(annotations, skipNullCheck = false) { val (key, value) = elementDescriptors.toList() @@ -265,8 +188,7 @@ internal fun SerialDescriptor.jsonSchemaObjectMap(definitions: JsonSchemaDefinit } } -@PublishedApi -internal fun SerialDescriptor.jsonSchemaObjectSealed( +private fun SerialDescriptor.jsonSchemaObjectSealed( definitions: JsonSchemaDefinitions ): JsonObject { val properties = mutableMapOf() @@ -319,20 +241,17 @@ internal fun SerialDescriptor.jsonSchemaObjectSealed( } } -@PublishedApi -internal fun SerialDescriptor.jsonSchemaArray( +private fun SerialDescriptor.jsonSchemaArray( annotations: List = listOf(), definitions: JsonSchemaDefinitions -): JsonObject { - return jsonSchemaElement(annotations) { +): JsonObject = + jsonSchemaElement(annotations) { val type = getElementDescriptor(0) it["items"] = type.createJsonSchema(getElementAnnotations(0), definitions) } -} -@PublishedApi -internal fun SerialDescriptor.jsonSchemaString( +private fun SerialDescriptor.jsonSchemaString( annotations: List = listOf() ): JsonObject { return jsonSchemaElement(annotations) { @@ -349,11 +268,10 @@ internal fun SerialDescriptor.jsonSchemaString( } } -@PublishedApi -internal fun SerialDescriptor.jsonSchemaNumber( +private fun SerialDescriptor.jsonSchemaNumber( annotations: List = listOf() -): JsonObject { - return jsonSchemaElement(annotations) { +): JsonObject = + jsonSchemaElement(annotations) { val value = when (kind) { PrimitiveKind.FLOAT, @@ -376,17 +294,12 @@ internal fun SerialDescriptor.jsonSchemaNumber( it["maximum"] = max } } -} -@PublishedApi -internal fun SerialDescriptor.jsonSchemaBoolean( +private fun SerialDescriptor.jsonSchemaBoolean( annotations: List = listOf() -): JsonObject { - return jsonSchemaElement(annotations) -} +): JsonObject = jsonSchemaElement(annotations) -@PublishedApi -internal fun SerialDescriptor.createJsonSchema( +private fun SerialDescriptor.createJsonSchema( annotations: List, definitions: JsonSchemaDefinitions ): JsonObject { @@ -404,8 +317,7 @@ internal fun SerialDescriptor.createJsonSchema( } } -@PublishedApi -internal fun JsonObjectBuilder.applyJsonSchemaDefaults( +private fun JsonObjectBuilder.applyJsonSchemaDefaults( descriptor: SerialDescriptor, annotations: List, skipNullCheck: Boolean = false, @@ -436,7 +348,7 @@ internal fun JsonObjectBuilder.applyJsonSchemaDefaults( } } -internal inline fun SerialDescriptor.jsonSchemaElement( +private inline fun SerialDescriptor.jsonSchemaElement( annotations: List, skipNullCheck: Boolean = false, skipTypeCheck: Boolean = false, @@ -452,11 +364,10 @@ internal inline fun SerialDescriptor.jsonSchemaElement( } } -internal inline fun buildJson(builder: (JsonObjectBuilder) -> Unit): JsonObject { - return JsonObject(JsonObjectBuilder().apply(builder).content) -} +private inline fun buildJson(builder: (JsonObjectBuilder) -> Unit): JsonObject = + JsonObject(JsonObjectBuilder().apply(builder).content) -internal class JsonObjectBuilder(val content: MutableMap = linkedMapOf()) : +private class JsonObjectBuilder(val content: MutableMap = linkedMapOf()) : MutableMap by content { operator fun set(key: String, value: Iterable) = set(key, JsonArray(value.map(::JsonPrimitive))) @@ -466,7 +377,7 @@ internal class JsonObjectBuilder(val content: MutableMap = operator fun set(key: String, value: Number?) = set(key, JsonPrimitive(value)) } -internal class JsonSchemaDefinitions(private val isEnabled: Boolean = true) { +private class JsonSchemaDefinitions(private val isEnabled: Boolean = true) { private val definitions: MutableMap = mutableMapOf() private val creator: MutableMap JsonObject> = mutableMapOf() diff --git a/scala/build.gradle.kts b/scala/build.gradle.kts index d8944f1d1..435730250 100644 --- a/scala/build.gradle.kts +++ b/scala/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } dependencies { - implementation(projects.xefCore) + implementation(projects.xefKotlin) implementation(projects.kotlinLoom) // TODO split to separate Scala library diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/auto/package.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/auto/package.scala index ef7a9705d..1040eae26 100644 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/auto/package.scala +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/auto/package.scala @@ -6,6 +6,7 @@ import com.xebia.functional.xef.llm.openai.LLMModel import io.circe.Decoder import io.circe.parser.parse import com.xebia.functional.xef.auto.{AIKt, Agent as KtAgent} +import com.xebia.functional.xef.auto.serialization.JsonSchemaKt import com.xebia.functional.xef.pdf.PDFLoaderKt import com.xebia.functional.tokenizer.ModelType import com.xebia.functional.xef.scala.textsplitters.TextSplitter @@ -49,7 +50,7 @@ def prompt[A: Decoder: SerialDescriptor]( KtAgent.promptWithSerializer[A]( scope.kt, prompt, - SerialDescriptor[A].serialDescriptor, + JsonSchemaKt.encodeJsonSchema(SerialDescriptor[A].serialDescriptor), (json: String) => parse(json).flatMap(Decoder[A].decodeJson(_)).fold(throw _, identity), maxAttempts, llmModel, @@ -108,7 +109,7 @@ def image[A: Decoder: SerialDescriptor]( KtAgent.imageWithSerializer[A]( scope.kt, prompt, - SerialDescriptor[A].serialDescriptor, + JsonSchemaKt.encodeJsonSchema(SerialDescriptor[A].serialDescriptor), (json: String) => parse(json).flatMap(Decoder[A].decodeJson(_)).fold(throw _, identity), maxAttempts, user, diff --git a/settings.gradle.kts b/settings.gradle.kts index 24bd07b0b..652d70079 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,33 +17,20 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "xef" -include("xef-kotlin-examples") -project(":xef-kotlin-examples").projectDir = file("examples/kotlin") - -include("xef-scala-examples") -project(":xef-scala-examples").projectDir = file("examples/scala") - -include("kotlin-loom") - +// include("xef-core") project(":xef-core").projectDir = file("core") include("xef-filesystem") project(":xef-filesystem").projectDir = file("filesystem") -include("xef-scala") -project(":xef-scala").projectDir = file("scala") - -include("xef-scala-cats") -project(":xef-scala-cats").projectDir = file("scala-cats") - include("xef-tokenizer") project(":xef-tokenizer").projectDir = file("tokenizer") include("xef-gpt4all") project(":xef-gpt4all").projectDir = file("gpt4all-kotlin") -// Integration modules +// include("xef-lucene") project(":xef-lucene").projectDir = file("integrations/lucene") @@ -55,3 +42,26 @@ project(":xef-postgresql").projectDir = file("integrations/postgresql") include("xef-sql") project(":xef-sql").projectDir = file("integrations/sql") +// +// + +// +include("xef-kotlin") +project(":xef-kotlin").projectDir = file("kotlin") + +include("xef-kotlin-examples") +project(":xef-kotlin-examples").projectDir = file("examples/kotlin") + +include("kotlin-loom") +// + +// +include("xef-scala-examples") +project(":xef-scala-examples").projectDir = file("examples/scala") + +include("xef-scala") +project(":xef-scala").projectDir = file("scala") + +include("xef-scala-cats") +project(":xef-scala-cats").projectDir = file("scala-cats") +//