diff --git a/.github/workflows/publish-development-version.yml b/.github/workflows/publish-development-version.yml index da097bd62..d373c3827 100644 --- a/.github/workflows/publish-development-version.yml +++ b/.github/workflows/publish-development-version.yml @@ -29,7 +29,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 20 - name: Assemble uses: gradle/gradle-build-action@v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d69aa9685..f6523d87c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 20 - name: Assemble uses: gradle/gradle-build-action@v2 diff --git a/.run/build.run.xml b/.run/build.run.xml new file mode 100644 index 000000000..87e14e108 --- /dev/null +++ b/.run/build.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/spotless.run.xml b/.run/spotless.run.xml new file mode 100644 index 000000000..74fba4040 --- /dev/null +++ b/.run/spotless.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/test.run.xml b/.run/test.run.xml new file mode 100644 index 000000000..6904dabe8 --- /dev/null +++ b/.run/test.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/wrapper.run.xml b/.run/wrapper.run.xml new file mode 100644 index 000000000..27b5fcbc6 --- /dev/null +++ b/.run/wrapper.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/README.md b/README.md index fc9f1c63b..dccf15acd 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,13 @@ Libraries are published in Maven Central, under the `com.xebia` group. Libraries are published in Maven Central. You may need to add that repository explicitly in your build, if you haven't done it before. -```kotlin -repositories { - mavenCentral() -} +```groovy +repositories { mavenCentral() } ``` Then add the library in the usual way. -```kotlin +```groovy // In Gradle Kotlin dependencies { implementation("com.xebia:xef-kotlin:") @@ -110,7 +108,7 @@ in your build, if you haven't done it before. ## đź“– Quick Introduction -In this small introduction we look at the main features of xef, including the `ai` function. +In this small introduction we look at the main features of xef, including the `conversation` function. - [Kotlin logo Kotlin version](https://github.com/xebia-functional/xef/blob/main/docs/intro/kotlin.md) - [Scala logo Scala version](https://github.com/xebia-functional/xef/blob/main/docs/intro/scala.md) @@ -121,5 +119,5 @@ In this small introduction we look at the main features of xef, including the `a You can also have a look at the examples to have a feeling of how using the library looks like. - [Kotlin logo Examples in Kotlin](https://github.com/xebia-functional/xef/tree/main/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/conversation) -- [Scala logo Examples in Scala](https://github.com/xebia-functional/xef/tree/main/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation) +- [Scala logo Examples in Scala](https://github.com/xebia-functional/xef/tree/main/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala) - [Java logo Examples in Java](https://github.com/xebia-functional/xef/tree/main/examples/java/src/main/java/com/xebia/functional/xef/java/auto) diff --git a/build.gradle.kts b/build.gradle.kts index 21259956a..2b6f31483 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,18 +1,18 @@ @file:Suppress("DSL_SCOPE_VIOLATION") plugins { - base - alias(libs.plugins.kotlin.multiplatform) apply false - alias(libs.plugins.kotlinx.serialization) - alias(libs.plugins.spotless) - alias(libs.plugins.dokka) - alias(libs.plugins.arrow.gradle.nexus) - alias(libs.plugins.arrow.gradle.publish) apply false - alias(libs.plugins.semver.gradle) + base + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.kotlinx.serialization) + alias(libs.plugins.spotless) + alias(libs.plugins.dokka) + alias(libs.plugins.arrow.gradle.nexus) + alias(libs.plugins.arrow.gradle.publish) apply false + alias(libs.plugins.semver.gradle) } allprojects { - group = property("project.group").toString() + group = property("project.group").toString() } fun isMultiplatformModule(project: Project): Boolean { @@ -22,10 +22,7 @@ fun isMultiplatformModule(project: Project): Boolean { val multiPlatformModules = project.subprojects.filter { isMultiplatformModule(it) }.map { it.name } -enum class ModuleType { - MULTIPLATFORM, - SINGLEPLATFORM -} +enum class ModuleType { SINGLEPLATFORM, MULTIPLATFORM } fun Project.configureBuildAndTestTask( taskName: String, @@ -33,7 +30,6 @@ fun Project.configureBuildAndTestTask( multiPlatformModules: List ) { val platform: String by extra - tasks.register(taskName) { doLast { val gradleCommand = getGradleCommand(platform) @@ -42,30 +38,26 @@ fun Project.configureBuildAndTestTask( } project.exec { when (moduleType) { + ModuleType.SINGLEPLATFORM -> { + commandLine(gradleCommand, "build", *buildExcludeOptions(multiPlatformModules)) + } ModuleType.MULTIPLATFORM -> { multiPlatformModules.forEach { module -> commandLine(gradleCommand, ":$module:${platform}Test") } } - ModuleType.SINGLEPLATFORM -> { - commandLine(gradleCommand, "build", *buildExcludeOptions(multiPlatformModules)) - } } } } } } -fun Project.buildExcludeOptions(modules: List): Array { +fun buildExcludeOptions(modules: List): Array { return modules.flatMap { listOf("-x", ":$it:build") }.toTypedArray() } fun getGradleCommand(platform: String): String { - return if (platform == "mingwX64") { - "gradlew.bat" - } else { - "./gradlew" - } + return if (platform == "mingwX64") "gradlew.bat" else "./gradlew" } configureBuildAndTestTask( diff --git a/buildSrc/src/main/kotlin/JavaPublishingConventionsPlugin.kt b/buildSrc/src/main/kotlin/JavaPublishingConventionsPlugin.kt index 1f9c7a29c..d01d6852d 100644 --- a/buildSrc/src/main/kotlin/JavaPublishingConventionsPlugin.kt +++ b/buildSrc/src/main/kotlin/JavaPublishingConventionsPlugin.kt @@ -1,17 +1,14 @@ import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.plugins.BasePlugin -import org.gradle.api.plugins.BasePluginExtension import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.bundling.Jar import org.gradle.kotlin.dsl.findByType import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.register import org.gradle.plugins.signing.SigningExtension class JavaPublishingConventionsPlugin : Plugin { + override fun apply(project: Project): Unit = project.run { val publishingExtension: PublishingExtension = extensions.findByType() @@ -35,7 +32,6 @@ class JavaPublishingConventionsPlugin : Plugin { val signingKeyId: String? = configValue("signing.keyId", "SIGNING_KEY_ID") val signingKey: String? = configValue("signing.key", "SIGNING_KEY") val signingPassphrase: String? = configValue("signing.passphrase", "SIGNING_KEY_PASSPHRASE") - isRequired = !isLocal useGpgCmd() useInMemoryPgpKeys(signingKeyId, signingKey, signingPassphrase) diff --git a/buildSrc/src/main/kotlin/Predef.kt b/buildSrc/src/main/kotlin/Predef.kt index d5efd8a20..4b03924a8 100644 --- a/buildSrc/src/main/kotlin/Predef.kt +++ b/buildSrc/src/main/kotlin/Predef.kt @@ -1,18 +1,13 @@ import org.gradle.api.Project import org.gradle.api.publish.maven.MavenPublication -internal fun Project.configValue(propertyName: String, environmentVariableName: String): String? { +internal fun Project.configValue(propertyName: String, envVarName: String): String? { val property: String? = project.properties[propertyName]?.toString() - val environmentVariable: String? = System.getenv(environmentVariableName) - - val configValue = property ?: environmentVariable - + val envVar: String? = System.getenv(envVarName) + val configValue = property ?: envVar return configValue.also { if (configValue.isNullOrBlank()) { - errorMessage( - "$propertyName Gradle property and " + - "$environmentVariableName environment variable are missing", - ) + errorMessage("$propertyName Gradle property and $envVarName environment variable are missing") } } } @@ -22,21 +17,18 @@ internal fun MavenPublication.pomConfiguration(project: Project) { name.set(project.properties["pom.name"]?.toString()) description.set(project.properties["pom.description"]?.toString()) url.set(project.properties["pom.url"]?.toString()) - licenses { license { name.set(project.properties["pom.license.name"]?.toString()) url.set(project.properties["pom.license.url"]?.toString()) } } - developers { developer { id.set(project.properties["pom.developer.id"].toString()) name.set(project.properties["pom.developer.name"].toString()) } } - scm { url.set(project.properties["pom.smc.url"].toString()) connection.set(project.properties["pom.smc.connection"].toString()) diff --git a/buildSrc/src/main/kotlin/ScalaPublishingConventionsPlugin.kt b/buildSrc/src/main/kotlin/ScalaPublishingConventionsPlugin.kt index 8f3b031cc..400817204 100644 --- a/buildSrc/src/main/kotlin/ScalaPublishingConventionsPlugin.kt +++ b/buildSrc/src/main/kotlin/ScalaPublishingConventionsPlugin.kt @@ -12,6 +12,7 @@ import org.gradle.kotlin.dsl.register import org.gradle.plugins.signing.SigningExtension class ScalaPublishingConventionsPlugin : Plugin { + override fun apply(project: Project): Unit = project.run { val scaladocJarTask: TaskProvider = tasks.register("scaladocJar") { group = BasePlugin.BUILD_GROUP @@ -37,11 +38,9 @@ class ScalaPublishingConventionsPlugin : Plugin { publications { register("maven") { val scala3Suffix = "_3" - artifactId = basePluginExtension.archivesName.get() + scala3Suffix from(components["java"]) artifact(scaladocJarTask) - pomConfiguration(project) } } @@ -52,7 +51,6 @@ class ScalaPublishingConventionsPlugin : Plugin { val signingKeyId: String? = configValue("signing.keyId", "SIGNING_KEY_ID") val signingKey: String? = configValue("signing.key", "SIGNING_KEY") val signingPassphrase: String? = configValue("signing.passphrase", "SIGNING_KEY_PASSPHRASE") - isRequired = !isLocal useGpgCmd() useInMemoryPgpKeys(signingKeyId, signingKey, signingPassphrase) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index dec720c07..a1399cd00 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -42,7 +42,7 @@ console-reports: output-reports: active: false - exclude: +# exclude: # - 'TxtOutputReport' # - 'XmlOutputReport' # - 'HtmlOutputReport' diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 5db9ed039..4db577d5c 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,32 +3,29 @@ import org.jetbrains.dokka.gradle.DokkaTask repositories { - mavenCentral() + 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) - alias(libs.plugins.detekt) - //id("com.xebia.asfuture").version("0.0.1") + 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) + alias(libs.plugins.detekt) } dependencies { - detektPlugins(project(":detekt-rules")) + detektPlugins(project(":detekt-rules")) } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - toolchain { - languageVersion = JavaLanguageVersion.of(11) - } + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + toolchain { languageVersion = JavaLanguageVersion.of(11) } } detekt { @@ -39,187 +36,166 @@ detekt { } kotlin { - jvm { - withJava() - compilations { - val integrationTest by compilations.creating { - // Create a test task to run the tests produced by this compilation: - tasks.register("integrationTest") { - description = "Run the integration tests" - group = "verification" - classpath = compileDependencyFiles + runtimeDependencyFiles + output.allOutputs - testClassesDirs = output.classesDirs - - testLogging { - events("passed") + jvm { + withJava() + compilations { + val integrationTest by compilations.creating { + // Create a test task to run the tests produced by this compilation: + tasks.register("integrationTest") { + description = "Run the integration tests" + group = "verification" + classpath = compileDependencyFiles + runtimeDependencyFiles + output.allOutputs + testClassesDirs = output.classesDirs + testLogging { events("passed") } + } } - } + val test by compilations.getting + integrationTest.associateWith(test) } - val test by compilations.getting - integrationTest.associateWith(test) - } - } - js(IR) { - browser() - nodejs() - } - - linuxX64() - macosX64() - macosArm64() - mingwX64() - - sourceSets { - all { - languageSettings.optIn("kotlin.ExperimentalStdlibApi") } - - val commonMain by getting { - dependencies { - api(libs.bundles.arrow) - api(libs.kotlinx.serialization.json) - api(libs.ktor.utils) - api(projects.xefTokenizer) - implementation(libs.bundles.ktor.client) - implementation(libs.klogging) - implementation(libs.uuid) + js(IR) { + browser() + nodejs() + } + linuxX64() + macosX64() + macosArm64() + mingwX64() + sourceSets { + all { + languageSettings.optIn("kotlin.ExperimentalStdlibApi") } - } - - val commonTest by getting { - dependencies { - implementation(libs.kotest.property) - implementation(libs.kotest.framework) - implementation(libs.kotest.assertions) + val commonMain by getting { + dependencies { + api(libs.bundles.arrow) + api(libs.kotlinx.serialization.json) + api(libs.ktor.utils) + api(projects.xefTokenizer) + implementation(libs.bundles.ktor.client) + implementation(libs.klogging) + implementation(libs.uuid) + } } - } - - val jvmMain by getting { - dependencies { - implementation(libs.ktor.http) - implementation(libs.logback) - implementation(libs.skrape) - implementation(libs.rss.reader) - api(libs.jackson) - api(libs.jackson.schema) - api(libs.jackson.schema.jakarta) - api(libs.jakarta.validation) - implementation(libs.kotlinx.coroutines.reactive) - api(libs.ktor.client.cio) + val commonTest by getting { + dependencies { + implementation(libs.kotest.property) + implementation(libs.kotest.framework) + implementation(libs.kotest.assertions) + } } - } - - val jsMain by getting { - dependencies { - api(libs.ktor.client.js) + val jvmMain by getting { + dependencies { + implementation(libs.ktor.http) + implementation(libs.logback) + implementation(libs.skrape) + implementation(libs.rss.reader) + api(libs.jackson) + api(libs.jackson.schema) + api(libs.jackson.schema.jakarta) + api(libs.jakarta.validation) + implementation(libs.kotlinx.coroutines.reactive) + api(libs.ktor.client.cio) + } } - } - - val jvmTest by getting { - dependencies { - implementation(libs.kotest.junit5) + val jsMain by getting { + dependencies { + api(libs.ktor.client.js) + } } - } - - val linuxX64Main by getting { - dependencies { - implementation(libs.ktor.client.cio) + val jvmTest by getting { + dependencies { + implementation(libs.kotest.junit5) + } } - } - val macosX64Main by getting { - dependencies { - implementation(libs.ktor.client.cio) + val linuxX64Main by getting { + dependencies { + implementation(libs.ktor.client.cio) + } } - } - val macosArm64Main by getting { - dependencies { - implementation(libs.ktor.client.cio) + val macosX64Main by getting { + dependencies { + implementation(libs.ktor.client.cio) + } } - } - val mingwX64Main by getting { - dependencies { - implementation(libs.ktor.client.winhttp) + val macosArm64Main by getting { + dependencies { + implementation(libs.ktor.client.cio) + } + } + val mingwX64Main by getting { + dependencies { + implementation(libs.ktor.client.winhttp) + } + } + val linuxX64Test by getting + val macosX64Test by getting + val macosArm64Test by getting + val mingwX64Test by getting + create("nativeMain") { + dependsOn(commonMain) + linuxX64Main.dependsOn(this) + macosX64Main.dependsOn(this) + macosArm64Main.dependsOn(this) + mingwX64Main.dependsOn(this) + } + create("nativeTest") { + dependsOn(commonTest) + linuxX64Test.dependsOn(this) + macosX64Test.dependsOn(this) + macosArm64Test.dependsOn(this) + mingwX64Test.dependsOn(this) } } - val linuxX64Test by getting - val macosX64Test by getting - val macosArm64Test by getting - val mingwX64Test by getting - - create("nativeMain") { - dependsOn(commonMain) - linuxX64Main.dependsOn(this) - macosX64Main.dependsOn(this) - macosArm64Main.dependsOn(this) - mingwX64Main.dependsOn(this) - } - - create("nativeTest") { - dependsOn(commonTest) - linuxX64Test.dependsOn(this) - macosX64Test.dependsOn(this) - macosArm64Test.dependsOn(this) - mingwX64Test.dependsOn(this) - } - } } spotless { - kotlin { - target("**/*.kt") - ktfmt().googleStyle().configure { - it.setRemoveUnusedImport(true) + kotlin { + target("**/*.kt") + ktfmt().googleStyle().configure { it.setRemoveUnusedImport(true) } } - } } tasks { - - withType().configureEach { - dependsOn(":detekt-rules:assemble") - autoCorrect = true - } - named("detektJvmMain") { - dependsOn(":detekt-rules:assemble") - getByName("build").dependsOn(this) - } - named("detekt") { - dependsOn(":detekt-rules:assemble") - getByName("build").dependsOn(this) - } - - 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) + withType().configureEach { + dependsOn(":detekt-rules:assemble") + autoCorrect = true + } + named("detektJvmMain") { + dependsOn(":detekt-rules:assemble") + getByName("build").dependsOn(this) + } + named("detekt") { + dependsOn(":detekt-rules:assemble") + getByName("build").dependsOn(this) + } + withType().configureEach { + maxParallelForks = Runtime.getRuntime().availableProcessors() + useJUnitPlatform() + testLogging { + setExceptionFormat("full") + setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError")) } - 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") - } + } + 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 = 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()) -} +tasks.withType { dependsOn(tasks.withType()) } diff --git a/detekt-rules/build.gradle.kts b/detekt-rules/build.gradle.kts index c8d0dadc3..3ac7db23d 100644 --- a/detekt-rules/build.gradle.kts +++ b/detekt-rules/build.gradle.kts @@ -5,7 +5,7 @@ repositories { mavenCentral() } plugins { `kotlin-dsl` base - alias(libs.plugins.spotless) + alias(libs.plugins.spotless) } spotless { @@ -15,24 +15,23 @@ spotless { } } - dependencies { - api(libs.detekt.api) - testImplementation(libs.detekt.test) + api(libs.detekt.api) + testImplementation(libs.detekt.test) testImplementation(libs.kotest.assertions) testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") implementation(libs.klogging) } -tasks.withType() { +tasks.withType { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } tasks.withType().configureEach { maxParallelForks = Runtime.getRuntime().availableProcessors() useJUnitPlatform() - systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class") - systemProperty("compile-snippet-tests", project.hasProperty("compile-test-snippets")) + systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class") + systemProperty("compile-snippet-tests", project.hasProperty("compile-test-snippets")) testLogging { setExceptionFormat("full") setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError")) diff --git a/detekt-rules/src/test/kotlin/org/example/detekt/JvmInlineAnnotationTest.kt b/detekt-rules/src/test/kotlin/org/example/detekt/JvmInlineAnnotationTest.kt index 509013791..ee3d225d0 100644 --- a/detekt-rules/src/test/kotlin/org/example/detekt/JvmInlineAnnotationTest.kt +++ b/detekt-rules/src/test/kotlin/org/example/detekt/JvmInlineAnnotationTest.kt @@ -13,8 +13,8 @@ internal class JvmInlineAnnotationTest(private val env: KotlinCoreEnvironment) { @Test fun `reports missing jvminline annotations`() { val code = """ - value class User(val id: String) - """ + value class User(val id: String) + """ val findings = JvmInlineAnnotation(Config.empty).compileAndLintWithContext(env, code) findings shouldHaveSize 1 } @@ -23,9 +23,9 @@ internal class JvmInlineAnnotationTest(private val env: KotlinCoreEnvironment) { fun `doesn't report annotated value classes`() { val code = """ - import kotlin.jvm.JvmInline - @JvmInline value class User(val id: String) - """ + import kotlin.jvm.JvmInline + @JvmInline value class User(val id: String) + """ val findings = JvmInlineAnnotation(Config.empty).compileAndLintWithContext(env, code) findings shouldHaveSize 0 } diff --git a/detekt-rules/src/test/kotlin/org/example/detekt/PublicDataClassConstructorWithValueParametersTest.kt b/detekt-rules/src/test/kotlin/org/example/detekt/PublicDataClassConstructorWithValueParametersTest.kt index fdc7a85c5..6d71d0b29 100644 --- a/detekt-rules/src/test/kotlin/org/example/detekt/PublicDataClassConstructorWithValueParametersTest.kt +++ b/detekt-rules/src/test/kotlin/org/example/detekt/PublicDataClassConstructorWithValueParametersTest.kt @@ -15,10 +15,10 @@ internal class PublicDataClassConstructorWithValueParametersTest( fun `reports data classes that have value class parameters without companion static constructor`() { val code = """ - import kotlin.jvm.JvmInline - @JvmInline value class Bar(val value: String) - data class Foo(val bar: Bar) - """ + import kotlin.jvm.JvmInline + @JvmInline value class Bar(val value: String) + data class Foo(val bar: Bar) + """ val findings = PublicDataClassConstructorWithValueParameters(Config.empty) .compileAndLintWithContext(env, code) @@ -30,18 +30,17 @@ internal class PublicDataClassConstructorWithValueParametersTest( fun `doesn't report data classes with value parameters with companion static constructor`() { val code = """ - import kotlin.jvm.JvmInline - import kotlin.jvm.JvmStatic - @JvmInline value class Bar(val value: String) - data class Foo(val bar: Bar) { - companion object { - @JvmStatic - fun apply(bar: Bar): Foo { - return Foo(bar) - } - } + import kotlin.jvm.JvmInline + import kotlin.jvm.JvmStatic + @JvmInline value class Bar(val value: String) + data class Foo(val bar: Bar) { + companion object { + @JvmStatic + fun apply(bar: Bar): Foo { + return Foo(bar) + } } - """ + }""" val findings = JvmInlineAnnotation(Config.empty).compileAndLintWithContext(env, code) findings shouldHaveSize 0 } @@ -49,15 +48,15 @@ internal class PublicDataClassConstructorWithValueParametersTest( @Test fun `reports data classes with value parameters with companion objects without static factory methods`() { val code = - """import kotlin.jvm.JvmInline - @JvmInline value class Bar(val value: String) - data class Foo(val bar: Bar){ - companion object { - val test: String = "test" - fun notAFactoryMethod(): String = test - } + """ + import kotlin.jvm.JvmInline + @JvmInline value class Bar(val value: String) + data class Foo(val bar: Bar){ + companion object { + val test: String = "test" + fun notAFactoryMethod(): String = test } - """ + }""" val findings = PublicDataClassConstructorWithValueParameters(Config.empty) .compileAndLintWithContext(env, code) @@ -68,16 +67,16 @@ internal class PublicDataClassConstructorWithValueParametersTest( @Test fun `reports data classes with value parameters with companion objects with static factory methods that are missing arguments of the targeted class, even if marked JvmStatic`() { val code = - """import kotlin.jvm.JvmInline - import kotlin.jvm.JvmStatic - @JvmInline value class Bar(val value: String) - data class Foo(val bar: Bar){ - companion object { - val test: Bar = Bar("test") - @JvmStatic fun applyMissingArguments():Foo = Foo(test) - } + """ + import kotlin.jvm.JvmInline + import kotlin.jvm.JvmStatic + @JvmInline value class Bar(val value: String) + data class Foo(val bar: Bar){ + companion object { + val test: Bar = Bar("test") + @JvmStatic fun applyMissingArguments():Foo = Foo(test) } - """ + }""" val findings = PublicDataClassConstructorWithValueParameters(Config.empty) .compileAndLintWithContext(env, code) @@ -88,16 +87,16 @@ internal class PublicDataClassConstructorWithValueParametersTest( @Test fun `does not report data classes with value parameters that have static factory methods in their companion objects that have all the arguments of the targeted class that are marked JvmStatic`() { val code = - """import kotlin.jvm.JvmStatic - import kotlin.jvm.JvmInline - @JvmInline value class Bar(val value: String) - data class Foo(val bar: Bar){ - companion object { - val test: String = "test" - @JvmStatic fun apply(bar: Bar):Foo = Foo(bar) - } + """ + import kotlin.jvm.JvmStatic + import kotlin.jvm.JvmInline + @JvmInline value class Bar(val value: String) + data class Foo(val bar: Bar) { + companion object { + val test: String = "test" + @JvmStatic fun apply(bar: Bar):Foo = Foo(bar) } - """ + }""" val findings = PublicDataClassConstructorWithValueParameters(Config.empty) .compileAndLintWithContext(env, code) diff --git a/docs/intro/scala.md b/docs/intro/scala.md index ca47d3c19..3ecb49a23 100644 --- a/docs/intro/scala.md +++ b/docs/intro/scala.md @@ -1,51 +1,19 @@ # Quick introduction to xef.ai (Scala version) -After adding the library to your project -(see the [main README](https://github.com/xebia-functional/xef/blob/main/README.md) for instructions), -you get access to the `ai` function, which is your gate to the modern AI world. +After adding the library to your project (see the +[main README](https://github.com/xebia-functional/xef/blob/main/README.md) for instructions), +you get access to the `conversation` function, which is your port of entry to the modern AI world. Inside of it, you can _prompt_ for information, which means posing the question to an LLM -(Large Language Model). The easiest way is to just get the information back as a string. +(Large Language Model). The easiest way is to just get the information back as a string or list of strings. -```scala -import com.xebia.functional.xef.scala.auto.* +```scala 3 +import com.xebia.functional.xef.scala.conversation.* -@main def runBook: Unit = ai { - val topic: String = "functional programming" - promptMessage(s"Give me a selection of books about $topic") -}.getOrElse(ex => println(ex.getMessage)) -``` - -In the example above we _execute_ the `ai` block with `getOrElse`, so in case an exception -is thrown (for example, if your API key is not correct), we are handing the error by printing -the reason of the error. - -In the next examples we'll write functions that rely on `ai`'s DSL functionality, -but without actually extracting the values yet using `getOrThrow` or `getOrElse`. -We'll eventually call this functions from an `ai` block as we've shown above, and -this allows us to build larger pipelines, and only extract the final result at the end. - -This can be done by either using a context parameters or function _using_ `AIScope`. -Let's compare the two: - -```scala -def book(topic: String)(using scope: AIScope): List[String] = - promptMessage(s"Give me a selection of books about $topic") - -def book(topic: String): AIScope ?=> List[String] = - promptMessage(s"Give me a selection of books about $topic") -``` - -Using the type alias `AI`, defined in `com.xebia.functional.xef.scala.auto` as: - -```scala -type AI[A] = AIScope ?=> A -``` - -book function can be written in this way: - -```scala -def book(topic: String): AI[List[String]] = - promptMessage(s"Give me a selection of books about $topic") +def books(topic: String): Unit = conversation: + val topBook: String = promptMessage(s"Give me the top-selling book about $topic") + println(topBook) + val selectedBooks: List[String] = promptMessages(s"Give me a selection of books about $topic") + println(selectedBooks.mkString("\n")) ``` ## Additional setup @@ -54,7 +22,7 @@ If the code above fails, you may need to perform some additional setup. ### OpenAI -By default, the `ai` block connects to [OpenAI](https://platform.openai.com/). +By default, the `conversation` block connects to [OpenAI](https://platform.openai.com/). To use their services you should provide the corresponding API key in the `OPENAI_TOKEN` environment variable, and have enough credits. @@ -76,8 +44,8 @@ Set the environment variable `OPENAI_TOKEN=xxx` ### Project Loom The Scala module depends on project [Loom](https://openjdk.org/projects/loom/), -so you will need at least Java 20 to use the library. Furthermore, you need to pass -the `--enable-preview` flag. +so you will need at least Java 20 to use the library. Furthermore, if using Java 20 specifically, +you need to pass the `--enable-preview` flag.
SBT @@ -91,7 +59,7 @@ env OPENAI_TOKEN= sbt -J--enable-preview IntelliJ - Set the Java version to at least 20 -- Set VM options to `--enable-preview` +- If using Java 20 specifically, set VM options to `--enable-preview`
@@ -102,28 +70,29 @@ strings we obtain. Fortunately, you can also ask xef.ai to give you back the inf using a _custom type_. The library takes care of instructing the LLM on building such a structure, and deserialize the result back for you. -```scala -import com.xebia.functional.xef.scala.auto.* +This can be done by declaring a case class that `derives SerialDescriptor, Decoder`: + +```scala 3 +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* import io.circe.Decoder -import io.circe.parser.decode -private final case class Book(name: String, author: String, summary: String) derives SerialDescriptor, Decoder +case class Book(name: String, author: String, pages: Int) derives SerialDescriptor, Decoder +``` -def summarizeBook(title: String, author: String)(using scope: AIScope): Book = - prompt(s"$title by $author summary.") +The `conversation` block can then be written in this way: -@main def runBook: Unit = - ai { - val toKillAMockingBird = summarizeBook("To Kill a Mockingbird", "Harper Lee") - println(s"${toKillAMockingBird.name} by ${toKillAMockingBird.author} summary:\n ${toKillAMockingBird.summary}") - }.getOrElse(ex => println(ex.getMessage)) +```scala 3 +def bookExample(topic: String): Unit = conversation: + val Book(title, author, pages) = prompt[Book](s"Give me the best-selling book about $topic") + println(s"The book $title is by $author and has $pages pages.") ``` -xef.ai for Scala uses xef.ai core, which it's based on Kotlin. Hence, the core +xef.ai for Scala uses xef.ai core, which is based on the Kotlin implementation. Hence, the core reuses [Kotlin's common serialization](https://kotlinlang.org/docs/serialization.html), and Scala uses [circe](https://github.com/circe/circe) to derive the required serializable instance. -The LLM is usually able to detect which kind of information should -go on each field based on its name (like `title` and `author` above). +The LLM is usually able to detect which kind of information should go in each field based on its name +(like `title` and `author` above). ## Context @@ -133,37 +102,45 @@ often want to supplement the LLM with more data: - Transient information referring to the current moment, like the current weather, or the trends in the stock market in the past 10 days. - Non-public information, for example for summarizing a piece of text you're creating - within you organization. + within your organization. These additional pieces of information are called the _context_ in xef.ai, and are attached to every question to the LLM. Although you can add arbitrary strings to the context at any point, the most common mode of usage is using an _agent_ to consult an external service, -and make its response part of the context. One such agent is `search`, which uses a web -search service to enrich that context. +and make its response part of the context. One such agent is `search`, which uses the +[Google Search API (SerpApi)](https://serpapi.com/) to enrich that context. + +(Note that a SerpApi token may be required to run this example.) + +```scala 3 +import com.xebia.functional.xef.conversation.llm.openai.* +import com.xebia.functional.xef.reasoning.serpapi.* +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder -```scala -import com.xebia.functional.xef.scala.agents.DefaultSearch -import com.xebia.functional.xef.scala.auto.* +val openAI: OpenAI = OpenAI.FromEnvironment -private def getQuestionAnswer(question: String)(using scope: AIScope): List[String] = - contextScope(DefaultSearch.search("Weather in Cádiz, Spain")) { - promptMessage(question) - } +def setContext(query: String)(using conversation: ScalaConversation): Unit = + addContext(Search(openAI.DEFAULT_CHAT, conversation, 3).search(query).get) -@main def runWeather: Unit = ai { +@main def runWeather(): Unit = conversation: + setContext("Weather in Cádiz, Spain") val question = "Knowing this forecast, what clothes do you recommend I should wear if I live in Cádiz?" - println(getQuestionAnswer(question).mkString("\n")) -}.getOrElse(ex => println(ex.getMessage)) + val answer = promptMessage(question) + println(answer) ``` > **Note** > The underlying mechanism of the context is a _vector store_, a data structure which > saves a set of strings, and is able to find those similar to another given one. -> By default xef.ai uses an _in-memory_ vector store, since it provides maximum +> By default xef.ai uses an _in-memory_ vector store, since this provides maximum > compatibility across platforms. However, if you foresee your context growing above > the hundreds of elements, you may consider switching to another alternative, like > Lucene or PostgreSQL. ## Examples -Check out the [examples folder](https://github.com/xebia-functional/xef/blob/main/examples/scala/auto) for a complete list of different use cases. +Check out the +[examples folder](https://github.com/xebia-functional/xef/blob/main/examples/scala/src/main/scala/com/xebia/functional/xef/examples) +for a complete list of different use cases. diff --git a/examples/java/build.gradle.kts b/examples/java/build.gradle.kts index 6acde3d30..aded6cd45 100644 --- a/examples/java/build.gradle.kts +++ b/examples/java/build.gradle.kts @@ -11,12 +11,6 @@ dependencies { implementation(projects.xefGpt4all) } -val ENABLE_PREVIEW = "--enable-preview" - -tasks.withType { - options.compilerArgs.add(ENABLE_PREVIEW) -} tasks.test { useJUnitPlatform() - jvmArgs(ENABLE_PREVIEW) } diff --git a/examples/kotlin/build.gradle.kts b/examples/kotlin/build.gradle.kts index 333c4cbf2..1addc2401 100644 --- a/examples/kotlin/build.gradle.kts +++ b/examples/kotlin/build.gradle.kts @@ -1,55 +1,47 @@ plugins { - id(libs.plugins.kotlin.jvm.get().pluginId) - id(libs.plugins.kotlinx.serialization.get().pluginId) - alias(libs.plugins.spotless) + id(libs.plugins.kotlin.jvm.get().pluginId) + id(libs.plugins.kotlinx.serialization.get().pluginId) + alias(libs.plugins.spotless) } -repositories { - mavenCentral() -} +repositories { mavenCentral() } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - toolchain { - languageVersion = JavaLanguageVersion.of(11) - } + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + toolchain { languageVersion = JavaLanguageVersion.of(11) } } dependencies { - implementation(projects.xefKotlin) - implementation(projects.xefFilesystem) - implementation(projects.xefPdf) - implementation(projects.xefSql) - implementation(projects.xefTokenizer) - implementation(projects.xefGpt4all) - implementation(projects.xefGcp) - implementation(projects.xefOpenai) - implementation(projects.xefReasoning) - implementation(projects.xefOpentelemetry) - implementation(projects.xefMlflow) - implementation(libs.kotlinx.serialization.json) - implementation(libs.logback) - implementation(libs.klogging) - implementation(libs.bundles.arrow) - implementation(libs.okio) - implementation(libs.jdbc.mysql.connector) - api(libs.ktor.client) + implementation(projects.xefKotlin) + implementation(projects.xefFilesystem) + implementation(projects.xefPdf) + implementation(projects.xefSql) + implementation(projects.xefTokenizer) + implementation(projects.xefGpt4all) + implementation(projects.xefGcp) + implementation(projects.xefOpenai) + implementation(projects.xefReasoning) + implementation(projects.xefOpentelemetry) + implementation(projects.xefMlflow) + implementation(libs.kotlinx.serialization.json) + implementation(libs.logback) + implementation(libs.klogging) + implementation(libs.bundles.arrow) + implementation(libs.okio) + implementation(libs.jdbc.mysql.connector) + api(libs.ktor.client) } spotless { - kotlin { - target("**/*.kt") - ktfmt().googleStyle().configure { - it.setRemoveUnusedImport(true) + kotlin { + target("**/*.kt") + ktfmt().googleStyle().configure { it.setRemoveUnusedImport(true) } } - } } tasks.getByName("processResources") { - dependsOn(projects.xefGpt4all.dependencyProject.tasks.getByName("jvmProcessResources")) - from("${projects.xefGpt4all.dependencyProject.buildDir}/processedResources/jvm/main") - into("$buildDir/resources/main") + dependsOn(projects.xefGpt4all.dependencyProject.tasks.getByName("jvmProcessResources")) + from("${projects.xefGpt4all.dependencyProject.buildDir}/processedResources/jvm/main") + into("$buildDir/resources/main") } - - diff --git a/examples/scala/.scalafmt.conf b/examples/scala/.scalafmt.conf index f6540bcff..038d0ede0 100644 --- a/examples/scala/.scalafmt.conf +++ b/examples/scala/.scalafmt.conf @@ -1,6 +1,6 @@ # tune this file as appropriate to your style! see: https://olafurpg.github.io/scalafmt/#Configuration -version = "3.7.3" +version = "3.7.15" runner.dialect = "scala3" @@ -17,7 +17,6 @@ align { ifWhileOpenParen = false openParenCallSite = false openParenDefnSite = false - tokens = ["%", "%%"] } @@ -34,4 +33,4 @@ optIn { project.excludeFilters = [ "metals.sbt" -] \ No newline at end of file +] diff --git a/examples/scala/build.gradle.kts b/examples/scala/build.gradle.kts index 3ce0f1895..615adf2a5 100644 --- a/examples/scala/build.gradle.kts +++ b/examples/scala/build.gradle.kts @@ -1,38 +1,30 @@ @file:Suppress("DSL_SCOPE_VIOLATION") plugins { - scala - alias(libs.plugins.spotless) + scala + alias(libs.plugins.spotless) } java { - sourceCompatibility = JavaVersion.VERSION_20 - targetCompatibility = JavaVersion.VERSION_20 - toolchain { - languageVersion = JavaLanguageVersion.of(20) - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + toolchain { languageVersion = JavaLanguageVersion.of(21) } } dependencies { - implementation(projects.xefCore) - implementation(projects.xefScala) - implementation(projects.xefReasoning) - implementation(projects.xefOpenai) - implementation(libs.circe.parser) - implementation(libs.scala.lang) - implementation(libs.logback) + implementation(projects.xefCore) + implementation(projects.xefScala) + implementation(projects.xefReasoning) + implementation(projects.xefOpenai) + implementation(libs.circe.parser) + implementation(libs.scala.lang) + implementation(libs.logback) } -tasks.withType().configureEach { - useJUnit() -} +tasks.withType().configureEach { useJUnit() } tasks.withType { - scalaCompileOptions.additionalParameters = listOf("-Wunused:all", "-Wvalue-discard") + scalaCompileOptions.additionalParameters = listOf("-Wunused:all", "-Wvalue-discard") } -spotless { - scala { - scalafmt("3.7.3").configFile(".scalafmt.conf").scalaMajorVersion("2.13") - } -} +spotless { scala { scalafmt("3.7.15").configFile(".scalafmt.conf") } } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/Simple.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/Simple.scala new file mode 100644 index 000000000..c9f4b17d1 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/Simple.scala @@ -0,0 +1,10 @@ +package com.xebia.functional.xef.examples.scala + +import com.xebia.functional.xef.scala.conversation.* + +@main def runBooks(): Unit = conversation: + val topic = "functional programming" + val topBook: String = promptMessage(s"Give me the top-selling book about $topic") + println(topBook) + val selectedBooks: List[String] = promptMessages(s"Give me a selection of books about $topic") + println(selectedBooks.mkString("\n")) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/Simple.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/Simple.scala new file mode 100644 index 000000000..fbe40da7e --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/Simple.scala @@ -0,0 +1,45 @@ +package com.xebia.functional.xef.examples.scala.context.serpapi + +import com.xebia.functional.xef.conversation.llm.openai.* +import com.xebia.functional.xef.reasoning.serpapi.* +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder + +import java.text.SimpleDateFormat +import java.util.Date + +val openAI: OpenAI = OpenAI.FromEnvironment + +val sdf = SimpleDateFormat("dd/M/yyyy") +def currentDate: String = sdf.format(new Date) + +def setContext(query: String)(using conversation: ScalaConversation): Unit = + addContext(Search(openAI.DEFAULT_CHAT, conversation, 3).search(query).get) + +case class BreakingNews(summary: String) derives SerialDescriptor, Decoder + +case class MarketNews(news: String, risingStockSymbols: List[String], fallingStockSymbols: List[String]) derives SerialDescriptor, Decoder + +case class Estimate(number: Long) derives SerialDescriptor, Decoder + +@main def runWeather(): Unit = conversation: + setContext("Weather in Cádiz, Spain") + val question = "Knowing this forecast, what clothes do you recommend I should wear if I live in Cádiz?" + val answer = promptMessage(question) + println(answer) + +@main def runBreakingNews(): Unit = conversation: + setContext(s"$currentDate COVID News") + val BreakingNews(summary) = prompt[BreakingNews](s"Write a summary of about 300 words given the provided context.") + println(summary) + +@main def runMarketNews(): Unit = conversation: + setContext(s"$currentDate Stock market results, rising stocks, falling stocks") + val news = prompt[MarketNews]("Write a short summary of the stock market results given the provided context.") + println(news) + +@main def runFermiEstimate(): Unit = conversation: + setContext("Estimate the number of medical needles in the world") + val Estimate(needlesInWorld) = prompt[Estimate]("Answer the question with an integer number given the provided context.") + println(s"Needles in world: $needlesInWorld") diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/UserQueries.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/UserQueries.scala new file mode 100644 index 000000000..711db19da --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/UserQueries.scala @@ -0,0 +1,20 @@ +package com.xebia.functional.xef.examples.scala.context.serpapi + +import com.xebia.functional.xef.reasoning.pdf.PDF +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder + +import scala.io.StdIn.readLine + +case class AIResponse(answer: String) derives SerialDescriptor, Decoder + +val PdfUrl = "https://people.cs.ksu.edu/~schmidt/705a/Scala/Programming-in-Scala.pdf" + +@main def runUserQueries(): Unit = conversation: + val pdf = PDF(openAI.DEFAULT_CHAT, openAI.DEFAULT_SERIALIZATION, summon[ScalaConversation]) + addContext(Array(pdf.readPDFFromUrl.readPDFFromUrl(PdfUrl).get)) + while (true) + println("Enter your question: ") + val AIResponse(answer) = prompt[AIResponse](readLine()) + println(s"$answer\n---\n") diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/images/HybridCity.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/images/HybridCity.scala new file mode 100644 index 000000000..1c9a3b4a9 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/images/HybridCity.scala @@ -0,0 +1,10 @@ +package com.xebia.functional.xef.examples.scala.images + +import com.xebia.functional.xef.prompt.Prompt +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder + +@main def runHybridCity(): Unit = conversation: + val imageUrls = images(Prompt("A hybrid city of Cádiz, Spain and Seattle, US.")) + println(imageUrls) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/AnimalStory.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/AnimalStory.scala new file mode 100644 index 000000000..19e6d433e --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/AnimalStory.scala @@ -0,0 +1,20 @@ +package com.xebia.functional.xef.examples.scala.iteration + +import com.xebia.functional.xef.prompt.JvmPromptBuilder +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder + +case class Animal(name: String, habitat: String, diet: String) derives SerialDescriptor, Decoder +case class Invention(name: String, inventor: String, year: Int, purpose: String) derives SerialDescriptor, Decoder + +@main def runAnimalStory(): Unit = conversation: + val animal = prompt[Animal]("A unique animal species") + val invention = prompt[Invention]("A groundbreaking invention from the 20th century.") + println(s"Animal: $animal") + println(s"Invention: $invention") + val builder = new JvmPromptBuilder() + .addSystemMessage("You are a writer for a science fiction magazine.") + .addUserMessage("Write a short story of 200 words that involves the animal and the invention.") + val story = promptMessage(builder.build) + println(story) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/ChessGame.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/ChessGame.scala new file mode 100644 index 000000000..42472e1a0 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/ChessGame.scala @@ -0,0 +1,52 @@ +package com.xebia.functional.xef.examples.scala.iteration + +import com.xebia.functional.xef.prompt.Prompt +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder + +import scala.annotation.tailrec + +case class ChessMove(player: String, move: String) derives SerialDescriptor, Decoder +case class ChessBoard(board: String) derives SerialDescriptor, Decoder +case class GameState(ended: Boolean = false, winner: Option[String] = None) derives SerialDescriptor, Decoder + +@tailrec +private def chessGame(moves: List[ChessMove] = Nil, gameState: GameState = new GameState)(using ScalaConversation): (String, ChessMove) = + if !gameState.ended then + val currentPlayer = if moves.size % 2 == 0 then "Player 1 (White)" else "Player 2 (Black)" + val previousMoves = moves.map(m => m.player + ":" + m.move).mkString(", ") + val movePrompt = moves match { + case Nil => s""" + |$currentPlayer, you are playing chess and it's your turn. + |Make your first move: + """.stripMargin + case l => s""" + |$currentPlayer, you are playing chess and it's your turn. + |Here are the previous moves: $previousMoves + |Make your next move: + """.stripMargin + } + println(movePrompt) + val move = prompt[ChessMove](movePrompt) + println(s"Move is: $move") + val boardPrompt = + s""" + |Given the following chess moves: $previousMoves, + |generate a chess board on a table with appropriate emoji representations for each move and piece. + |Add a brief description of the move and its implications. + """.stripMargin + val chessBoard = prompt[ChessBoard](boardPrompt) + println(s"Current board:\n${chessBoard.board}") + val gameStatePrompt = + s""" + |Given the following chess moves: ${moves.mkString(", ")}, + |has the game ended (win, draw, or stalemate)? + """.stripMargin + val gameState = prompt[GameState](gameStatePrompt) + chessGame(moves :+ move, gameState) + else (gameState.winner.getOrElse("Something went wrong"), moves.last) + +@main def runChessGame(): Unit = conversation: + val (winner, fMove) = chessGame() + println(s"Game over. Final move: $fMove, Winner: $winner") diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Annotated.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Annotated.scala new file mode 100644 index 000000000..168599987 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Annotated.scala @@ -0,0 +1,18 @@ +package com.xebia.functional.xef.examples.scala.serialization + +import com.xebia.functional.xef.prompt.Prompt +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder + +@Description("A book") +case class AnnotatedBook( + @Description("The name of the book") name: String, + @Description("The author of the book") author: String, + @Description("A 50 word paragraph with a summary of this book") summary: String +) derives SerialDescriptor, + Decoder + +@main def runAnnotatedBook(): Unit = conversation: + val book = prompt[AnnotatedBook]("To Kill a Mockingbird") + println(book) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Simple.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Simple.scala new file mode 100644 index 000000000..2652fd8a3 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Simple.scala @@ -0,0 +1,37 @@ +package com.xebia.functional.xef.examples.scala.serialization + +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder + +case class AsciiArt(art: String) derives SerialDescriptor, Decoder + +case class Book(title: String, author: String, pages: Int) derives SerialDescriptor, Decoder + +case class City(population: Int, description: String) derives SerialDescriptor, Decoder + +case class Movie(title: String, genre: String, director: String) derives SerialDescriptor, Decoder + +case class Recipe(name: String, ingredients: List[String]) derives SerialDescriptor, Decoder + +@main def runAsciiArt(): Unit = conversation: + val AsciiArt(art) = prompt[AsciiArt]("ASCII art of a cat dancing") + println(art) + +@main def runBook(): Unit = conversation: + val topic = "functional programming" + val Book(title, author, pages) = prompt[Book](s"Give me the best-selling book about $topic") + println(s"The book $title is by $author and has $pages pages.") + +@main def runMovie(): Unit = conversation: + val Movie(title, genre, director) = prompt[Movie]("Inception movie genre and director.") + println(s"The movie $title is a $genre film directed by $director.") + +@main def runCities(): Unit = conversation: + val cadiz = prompt[City]("Cádiz, Spain") + val seattle = prompt[City]("Seattle, WA") + println(s"The population of Cádiz is ${cadiz.population} and the population of Seattle is ${seattle.population}.") + +@main def runRecipe(): Unit = conversation: + val Recipe(name, ingredients) = prompt[Recipe]("Recipe for chocolate chip cookies.") + println(s"The recipe for $name is $ingredients.") diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/BreakingNews.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/BreakingNews.scala deleted file mode 100644 index 10316ae6d..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/BreakingNews.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.contexts - -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.reasoning.serpapi.Search -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -import java.text.SimpleDateFormat -import java.util.Date - -private final case class BreakingNewsAboutCovid(summary: String) derives SerialDescriptor, Decoder - -@main def runBreakingNews: Unit = - conversation { - val sdf = SimpleDateFormat("dd/M/yyyy") - val currentDate = sdf.format(Date()) - val search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, summon[ScalaConversation], 3) - addContext(search.search(s"$currentDate Covid News").get()) - val news = prompt[BreakingNewsAboutCovid](Prompt(s"Write a paragraph of about 300 words about: $currentDate Covid News")) - println(news.summary) - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/DivergentTasks.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/DivergentTasks.scala deleted file mode 100644 index bd842f932..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/DivergentTasks.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.contexts - -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.reasoning.serpapi.Search -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class NumberOfMedicalNeedlesInWorld(numberOfNeedles: Long) derives SerialDescriptor, Decoder - -@main def runDivergentTasks: Unit = - conversation { - val search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, summon[ScalaConversation], 3) - addContext(search.search("Estimate amount of medical needles in the world").get()) - val needlesInWorld = prompt[NumberOfMedicalNeedlesInWorld](Prompt("Provide the number of medical needles in the world as an integer number")) - println(s"Needles in world: ${needlesInWorld.numberOfNeedles}") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Markets.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Markets.scala deleted file mode 100644 index 3eb8a0814..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Markets.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.contexts - -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.reasoning.serpapi.Search -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -import java.text.SimpleDateFormat -import java.util.Date - -private final case class MarketNews(news: String, raisingStockSymbols: List[String], decreasingStockSymbols: List[String]) - derives SerialDescriptor, - Decoder - -@main def runMarkets: Unit = - conversation { - val sdf = SimpleDateFormat("dd/M/yyyy") - val currentDate = sdf.format(Date()) - val search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, summon[ScalaConversation], 3) - addContext(search.search(s"$currentDate Stock market results, raising stocks, decreasing stocks").get()) - val news = prompt[MarketNews](Prompt("Write a short summary of the stock market results given the provided context.")) - println(news) - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/PDFDocument.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/PDFDocument.scala deleted file mode 100644 index 2aa1d5bf8..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/PDFDocument.scala +++ /dev/null @@ -1,25 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.contexts - -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.reasoning.pdf.PDF -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -import scala.io.StdIn.readLine - -private final case class AIResponse(answer: String) derives SerialDescriptor, Decoder - -val pdfUrl = "https://people.cs.ksu.edu/~schmidt/705a/Scala/Programming-in-Scala.pdf" - -@main def runPDFDocument: Unit = - conversation { - val pdf = PDF(OpenAI.FromEnvironment.DEFAULT_CHAT, OpenAI.FromEnvironment.DEFAULT_SERIALIZATION, summon[ScalaConversation]) - addContext(Array(pdf.readPDFFromUrl.readPDFFromUrl(pdfUrl).get())) - while (true) { - println("Enter your question: ") - val line = scala.io.StdIn.readLine() - val response = prompt[AIResponse](Prompt(line)) - println(s"${response.answer}\n---\n") - } - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Weather.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Weather.scala deleted file mode 100644 index 453700db7..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Weather.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.xebia.functional.xef.scala.conversation - -import com.xebia.functional.xef.scala.conversation.* -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.reasoning.serpapi.Search -import com.xebia.functional.xef.prompt.Prompt - -private def getQuestionAnswer(question: Prompt)(using conversation: ScalaConversation): String = - val search: Search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, conversation, 3) - addContext(search.search("Weather in Cádiz, Spain").get()) - promptMessage(question) - -@main def runWeather: Unit = - val question = Prompt("Knowing this forecast, what clothes do you recommend I should wear if I live in Cádiz?") - println(conversation(getQuestionAnswer(question)).mkString("\n")) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/conversations/Animal.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/conversations/Animal.scala deleted file mode 100644 index 4f3405bb2..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/conversations/Animal.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.conversations - -import com.xebia.functional.xef.prompt.{JvmPromptBuilder, Prompt} -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class Animal(name: String, habitat: String, diet: String) derives SerialDescriptor, Decoder - -private final case class Invention(name: String, inventor: String, year: Int, purpose: String) derives SerialDescriptor, Decoder - -@main def runAnimal: Unit = - conversation { - val animal: Animal = prompt(Prompt("A unique animal species")) - val invention: Invention = prompt(Prompt("A groundbreaking invention from the 20th century.")) - - println(s"Animal: $animal") - println(s"Invention: $invention") - - val builder = new JvmPromptBuilder() - .addSystemMessage("You are a writer for a science fiction magazine.") - .addUserMessage("Write a short story of 200 words that involves the animal and the invention") - - val story = promptMessage(builder.build()) - - println(story) - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/fields/Book.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/fields/Book.scala deleted file mode 100644 index 15f278c5f..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/fields/Book.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.fields - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -@Description("A book") -case class Book( - @Description("the name of the book") name: String, - @Description("the author of the book") author: String, - @Description("A 50 word paragraph with a summary of this book") summary: String -) derives SerialDescriptor, - Decoder - -def summarizeBook(title: String, author: String)(using conversation: ScalaConversation): Book = - prompt(Prompt(s"$title by $author summary.")) - -@main def runBook: Unit = - conversation { - val toKillAMockingBird = summarizeBook("To Kill a Mockingbird", "Harper Lee") - println(s"${toKillAMockingBird.name} by ${toKillAMockingBird.author} summary:\n ${toKillAMockingBird.summary}") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/image/Population.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/image/Population.scala deleted file mode 100644 index 62dc2b1c4..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/image/Population.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.image - -import com.xebia.functional.xef.llm.models.images.ImagesGenerationResponse -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class Population(size: Int, description: String) derives SerialDescriptor, Decoder - -private final case class Image(description: String, url: String) derives SerialDescriptor, Decoder - -@main def runPopulation: Unit = - conversation { - val cadiz: Population = prompt(Prompt("Population of Cádiz, Spain.")) - val seattle: Population = prompt(Prompt("Population of Seattle, WA.")) - val imgs: ImagesGenerationResponse = images(Prompt("A hybrid city of Cádiz, Spain and Seattle, US.")) - println(imgs) - println(s"The population of Cádiz is ${cadiz.size} and the population of Seattle is ${seattle.size}") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ASCIIArt.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ASCIIArt.scala deleted file mode 100644 index ea963b93c..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ASCIIArt.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.serialization - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class ASCIIArt(art: String) derives SerialDescriptor, Decoder - -@main def runASCIIArt: Unit = - lazy val asciiArt = conversation { - prompt[ASCIIArt](Prompt("ASCII art of a cat dancing")) - } - println(asciiArt.art) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ChessAI.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ChessAI.scala deleted file mode 100644 index d4dde7858..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ChessAI.scala +++ /dev/null @@ -1,63 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.serialization - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -import scala.annotation.tailrec - -private final case class ChessMove(player: String, move: String) derives SerialDescriptor, Decoder - -private final case class ChessBoard(board: String) derives SerialDescriptor, Decoder - -private final case class GameState(ended: Boolean, winner: Option[String]) derives SerialDescriptor, Decoder - -@tailrec -private def chessGame(moves: List[ChessMove], gameState: GameState)(using conversation: ScalaConversation): (String, ChessMove) = - if !gameState.ended then - val currentPlayer = if moves.size % 2 == 0 then "Player 1 (White)" else "Player 2 (Black)" - - val previousMoves = moves.map(m => m.player + ":" + m.move).mkString(", ") - - val movePrompt = moves match { - case Nil => s""" - |$currentPlayer, you are playing chess and it's your turn. - |These are no previous chess board moves. - |Make your first move: - """.stripMargin - case l => s""" - |$currentPlayer, you are playing chess and it's your turn. - |These are the previous chess board moves: $previousMoves - |Make your next move: - """.stripMargin - } - println(movePrompt) - val move: ChessMove = prompt(Prompt(movePrompt)) - println(s"Move is: $move") - - val boardPrompt = - s""" - |Given the following chess moves: $previousMoves, - |generate a chess board on a table with appropriate emoji representations for each move and piece. - |Add a brief description of the move and it's implications - """.stripMargin - - val chessBoard: ChessBoard = prompt(Prompt(boardPrompt)) - println(s"Current board:\n${chessBoard.board}") - - val gameStatePrompt = - Prompt(s""" - |Given the following chess moves: ${moves.mkString(", ")}, - |has the game ended (win, draw, or stalemate)? - """.stripMargin) - - val gameState: GameState = prompt(gameStatePrompt) - - chessGame(moves :+ move, gameState) - else (gameState.winner.getOrElse("Something went wrong"), moves.last) - -@main def runChess: Unit = - conversation { - val (winner, fMove) = chessGame(Nil, GameState(false, None)) - println(s"Game over. Final move: $fMove, Winner: $winner") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Movie.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Movie.scala deleted file mode 100644 index 6eb070589..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Movie.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.serialization - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class Movie(title: String, genre: String, director: String) derives SerialDescriptor, Decoder - -@main def runMovie: Unit = - conversation { - val movie = prompt[Movie](Prompt("Inception movie genre and director.")) - println(s"The movie ${movie.title} is a ${movie.genre} film directed by ${movie.director}.") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Recipe.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Recipe.scala deleted file mode 100644 index f548c49f2..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Recipe.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.serialization - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class Recipe(name: String, ingredients: List[String]) derives SerialDescriptor, Decoder - -@main def runRecipe: Unit = - conversation { - val recipe: Recipe = prompt(Prompt("Recipe for chocolate chip cookies.")) - println(s"The recipe for ${recipe.name} is ${recipe.ingredients}") - } diff --git a/filesystem/build.gradle.kts b/filesystem/build.gradle.kts index fdd5d6061..c2ced941d 100644 --- a/filesystem/build.gradle.kts +++ b/filesystem/build.gradle.kts @@ -21,11 +21,9 @@ repositories { mavenCentral() } kotlin { jvm() js(IR) { nodejs() } - linuxX64() macosX64() mingwX64() - sourceSets { val commonMain by getting { dependencies { @@ -34,9 +32,7 @@ kotlin { implementation(libs.klogging) } } - val jsMain by getting { dependencies { implementation(libs.okio.nodefilesystem) } } - commonTest { dependencies { implementation(libs.okio.fakefilesystem) @@ -45,13 +41,10 @@ kotlin { implementation(libs.kotest.assertions) } } - val jvmTest by getting { dependencies { implementation(libs.kotest.junit5) } } - val linuxX64Main by getting val macosX64Main by getting val mingwX64Main by getting - create("nativeMain") { dependsOn(commonMain) linuxX64Main.dependsOn(this) diff --git a/gpt4all-kotlin/README.MD b/gpt4all-kotlin/README.MD index 2d535b51b..8f937af29 100644 --- a/gpt4all-kotlin/README.MD +++ b/gpt4all-kotlin/README.MD @@ -16,9 +16,9 @@ It is also possible to use a GPT4All local installation by setting the `jna.libr In order to use the library, apart from the previous configuration, you must have downloaded some GPT4All model in your local, and pass both the model path and the model type: -``` +```kotlin GPT4All(Path.of("models/gpt4all/ggml-gpt4all-j-v1.3-groovy.bin"), GPT4AllModel.Type.GPTJ).use { gpt4All -> - val promptMessage = Message(Message.Role.USER, "Some prompt goes here") - gpt4All.chatCompletion(listOf(promptMessage)) + val promptMessage = Message(Message.Role.USER, "Some prompt goes here") + gpt4All.chatCompletion(listOf(promptMessage)) } ``` diff --git a/gpt4all-kotlin/build.gradle.kts b/gpt4all-kotlin/build.gradle.kts index 185db9d32..6a969980a 100644 --- a/gpt4all-kotlin/build.gradle.kts +++ b/gpt4all-kotlin/build.gradle.kts @@ -28,31 +28,23 @@ detekt { kotlin { jvm { compilations { - val integrationTest by - compilations.creating { - // Create a test task to run the tests produced by this compilation: - tasks.register("integrationTest") { - description = "Run the integration tests" - group = "verification" - classpath = - compileDependencyFiles + - runtimeDependencyFiles + - output.allOutputs - testClassesDirs = output.classesDirs - - testLogging { events("passed") } - } - } + val integrationTest by compilations.creating { + // Create a test task to run the tests produced by this compilation: + tasks.register("integrationTest") { + description = "Run the integration tests" + group = "verification" + classpath = compileDependencyFiles + runtimeDependencyFiles + output.allOutputs + testClassesDirs = output.classesDirs + testLogging { events("passed") } + } + } val test by compilations.getting integrationTest.associateWith(test) } } - js(IR) { browser() } - sourceSets { val commonMain by getting { dependencies { implementation(projects.xefCore) } } - commonTest { dependencies { implementation(kotlin("test")) @@ -61,16 +53,13 @@ kotlin { implementation(libs.kotest.assertions) } } - val jvmMain by getting { dependencies { implementation(libs.gpt4all.java.bindings) implementation(libs.ai.djl.huggingface.tokenizers) } } - val jsMain by getting {} - val jvmTest by getting { dependencies { implementation(libs.kotest.junit5) } } } } @@ -96,6 +85,5 @@ tasks { setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError")) } } - withType { dependsOn(withType()) } } diff --git a/gradle.properties b/gradle.properties index 9400b3a33..a0979c287 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,4 +22,4 @@ systemProp.org.gradle.unsafe.kotlin.assignment=true # Workaround to disable Dokka setup from Arrow Gradle Config dokkaEnabled=false -scalaVersion=3.3.0 +scalaVersion=3.3.1 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba771..7f93135c4 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c30b486a8..3fa8f862f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d421..1aa94a426 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/integrations/gcp/README.MD b/integrations/gcp/README.MD index 72342413e..402044d0e 100644 --- a/integrations/gcp/README.MD +++ b/integrations/gcp/README.MD @@ -11,14 +11,14 @@ The most simple solution would be to copy an existing http module, and modify th But let's cover the different important pieces. First we need to set up the Gradle plugins to configure both _Kotlin Multiplatform_, and _KotlinX Serialization_ for content negotiation. -```kotlin -id("org.jetbrains.kotlin.multiplatform") version "1.9.0" +```groovy +id("org.jetbrains.kotlin.multiplatform") version "1.9.10" ``` In the Xef project we've already defined these dependencies in the [Version Catalog](), so you get a typed DSL inside Gradle to set this up with automatic versioning. -```kotlin +```groovy id(libs.plugins.kotlin.multiplatform.get().pluginId) ``` @@ -27,7 +27,7 @@ and set up the targets for the desired platforms. For Xef we set up following targets: -```kotlin +```groovy kotlin { jvm() js(IR) { @@ -56,14 +56,14 @@ but remember you only have access to _common_ Kotlin code. So now JDK specific p So now we've configured Kotlin, we should set up KotlinX Serialization, and it's compiler plugin. This is needed so we can send `JSON`, and different formats over the network. -```kotlin -id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0" +```groovy +id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" ``` In the Xef project we've already defined these dependencies in the [Version Catalog](), so you get a typed DSL inside Gradle to set this up with automatic versioning. -```kotlin +```groovy id(libs.plugins.kotlinx.serialization.get().pluginId) ``` @@ -78,7 +78,7 @@ We'll start with setting up some _common_ dependencies, we do so again within `k First let's add a dependency on `xef-core`, such that we can implement the `Chat`, `ChatWithFunction`, etc. interfaces based on our integration. -```kotlin +```groovy kotlin { sourceSets { val commonMain by getting { @@ -92,7 +92,7 @@ kotlin { Finally, we also need to set up Ktor. For most HTTP integration we need 3 _common_ dependencies. -```kotlin +```groovy implementation("io.ktor:ktor-client-core:2.3.2") implementation("io.ktor:ktor-client-content-negotiation:2.3.2") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.2") @@ -100,7 +100,7 @@ implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.2") There are bundles in the Xef project, so we can more easily depend on them in a typed way. -```kotlin +```groovy kotlin { sourceSets { val commonMain by getting { @@ -123,42 +123,36 @@ Almost all targets are relying on the `CIO` engine, which is the _Coroutines_ en There are plenty of other options we can choose, more info [here](https://ktor.io/docs/http-client-engines.html#minimal-version). -```kotlin +```groovy kotlin { sourceSets { ... - val jvmMain by getting { dependencies { implementation(libs.logback) api(libs.ktor.client.cio) } } - val jsMain by getting { dependencies { api(libs.ktor.client.js) } } - val linuxX64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val macosX64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val macosArm64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val mingwX64Main by getting { dependencies { api(libs.ktor.client.winhttp) @@ -365,4 +359,4 @@ HttpClient { ## Implementing Core's Chat interface -TODO \ No newline at end of file +TODO diff --git a/integrations/gcp/build.gradle.kts b/integrations/gcp/build.gradle.kts index e3b4d7fde..3c199d1a9 100644 --- a/integrations/gcp/build.gradle.kts +++ b/integrations/gcp/build.gradle.kts @@ -1,16 +1,13 @@ plugins { - id(libs.plugins.kotlin.multiplatform.get().pluginId) - id(libs.plugins.kotlinx.serialization.get().pluginId) - alias(libs.plugins.spotless) - alias(libs.plugins.arrow.gradle.publish) - alias(libs.plugins.semver.gradle) - alias(libs.plugins.detekt) + id(libs.plugins.kotlin.multiplatform.get().pluginId) + id(libs.plugins.kotlinx.serialization.get().pluginId) + alias(libs.plugins.spotless) + alias(libs.plugins.arrow.gradle.publish) + alias(libs.plugins.semver.gradle) + alias(libs.plugins.detekt) } - -dependencies { - detektPlugins(project(":detekt-rules")) -} +dependencies { detektPlugins(project(":detekt-rules")) } detekt { toolVersion = "1.23.1" @@ -19,105 +16,86 @@ detekt { autoCorrect = true } - -repositories { - mavenCentral() -} +repositories { mavenCentral() } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - toolchain { - languageVersion = JavaLanguageVersion.of(11) - } + 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) - implementation(libs.bundles.ktor.client) - implementation(libs.uuid) - implementation(libs.kotlinx.datetime) - } + jvm() + js(IR) { + browser() + nodejs() } - - val jvmMain by getting { - dependencies { - implementation(libs.logback) - api(libs.ktor.client.cio) - } - } - - val jsMain by getting { - dependencies { - api(libs.ktor.client.js) - } + linuxX64() + macosX64() + macosArm64() + mingwX64() + sourceSets { + val commonMain by getting { + dependencies { + api(projects.xefCore) + implementation(libs.bundles.ktor.client) + implementation(libs.uuid) + implementation(libs.kotlinx.datetime) + } + } + val jvmMain by getting { + dependencies { + implementation(libs.logback) + api(libs.ktor.client.cio) + } + } + val jsMain by getting { + dependencies { + api(libs.ktor.client.js) + } + } + val linuxX64Main by getting { + dependencies { + api(libs.ktor.client.cio) + } + } + val macosX64Main by getting { + dependencies { + api(libs.ktor.client.cio) + } + } + val macosArm64Main by getting { + dependencies { + api(libs.ktor.client.cio) + } + } + val mingwX64Main by getting { + dependencies { + api(libs.ktor.client.winhttp) + } + } } - - val linuxX64Main by getting { - dependencies { - api(libs.ktor.client.cio) - } - } - - val macosX64Main by getting { - dependencies { - api(libs.ktor.client.cio) - } - } - - val macosArm64Main by getting { - dependencies { - api(libs.ktor.client.cio) - } - } - - val mingwX64Main by getting { - dependencies { - api(libs.ktor.client.winhttp) - } - } - } } spotless { - kotlin { - target("**/*.kt") - ktfmt().googleStyle().configure { - it.setRemoveUnusedImport(true) + kotlin { + target("**/*.kt") + ktfmt().googleStyle().configure { it.setRemoveUnusedImport(true) } } - } } -tasks{ - withType().configureEach { - dependsOn(":detekt-rules:assemble") - autoCorrect = true - } - named("detektJvmMain") { - dependsOn(":detekt-rules:assemble") - getByName("build").dependsOn(this) - } - named("detekt") { - dependsOn(":detekt-rules:assemble") - getByName("build").dependsOn(this) - } - withType { - dependsOn(withType()) - } - +tasks { + withType().configureEach { + dependsOn(":detekt-rules:assemble") + autoCorrect = true + } + named("detektJvmMain") { + dependsOn(":detekt-rules:assemble") + getByName("build").dependsOn(this) + } + named("detekt") { + dependsOn(":detekt-rules:assemble") + getByName("build").dependsOn(this) + } + withType { dependsOn(withType()) } } - diff --git a/integrations/lucene/build.gradle.kts b/integrations/lucene/build.gradle.kts index a6557f3c8..1a6f9b961 100644 --- a/integrations/lucene/build.gradle.kts +++ b/integrations/lucene/build.gradle.kts @@ -38,6 +38,5 @@ tasks { dependsOn(":detekt-rules:assemble") getByName("build").dependsOn(this) } - withType { dependsOn(withType()) } } diff --git a/integrations/mlflow/build.gradle.kts b/integrations/mlflow/build.gradle.kts index da8ed47ff..7aa655f29 100644 --- a/integrations/mlflow/build.gradle.kts +++ b/integrations/mlflow/build.gradle.kts @@ -7,10 +7,7 @@ plugins { alias(libs.plugins.detekt) } - -dependencies { - detektPlugins(project(":detekt-rules")) -} +dependencies { detektPlugins(project(":detekt-rules")) } detekt { toolVersion = "1.23.1" @@ -19,17 +16,12 @@ detekt { autoCorrect = true } - -repositories { - mavenCentral() -} +repositories { mavenCentral() } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 - toolchain { - languageVersion = JavaLanguageVersion.of(11) - } + toolchain { languageVersion = JavaLanguageVersion.of(11) } } kotlin { @@ -38,12 +30,10 @@ kotlin { browser() nodejs() } - linuxX64() macosX64() macosArm64() mingwX64() - sourceSets { val commonMain by getting { dependencies { @@ -53,38 +43,32 @@ kotlin { implementation(libs.kotlinx.datetime) } } - val jvmMain by getting { dependencies { implementation(libs.logback) api(libs.ktor.client.cio) } } - val jsMain by getting { dependencies { api(libs.ktor.client.js) } } - val linuxX64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val macosX64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val macosArm64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val mingwX64Main by getting { dependencies { api(libs.ktor.client.winhttp) @@ -96,9 +80,7 @@ kotlin { spotless { kotlin { target("**/*.kt") - ktfmt().googleStyle().configure { - it.setRemoveUnusedImport(true) - } + ktfmt().googleStyle().configure { it.setRemoveUnusedImport(true) } } } @@ -118,6 +100,4 @@ tasks{ withType { dependsOn(withType()) } - } - diff --git a/integrations/opentelemetry/build.gradle.kts b/integrations/opentelemetry/build.gradle.kts index 6437e05f0..aae9caa41 100644 --- a/integrations/opentelemetry/build.gradle.kts +++ b/integrations/opentelemetry/build.gradle.kts @@ -32,7 +32,6 @@ dependencies { implementation(libs.opentelemetry.semconv) implementation(libs.opentelemetry.extension.kotlin) implementation(libs.opentelemetry.exporter.otlp) - testImplementation(libs.junit.jupiter.api) testImplementation(libs.kotest.property) testImplementation(libs.kotest.framework) @@ -43,9 +42,7 @@ dependencies { spotless { kotlin { target("**/*.kt") - ktfmt().googleStyle().configure { - it.setRemoveUnusedImport(true) - } + ktfmt().googleStyle().configure { it.setRemoveUnusedImport(true) } } } @@ -58,6 +55,5 @@ tasks { dependsOn(":detekt-rules:assemble") getByName("build").dependsOn(this) } - withType { dependsOn(withType()) } } diff --git a/integrations/pdf/build.gradle.kts b/integrations/pdf/build.gradle.kts index 2e814686d..fd49a4a85 100644 --- a/integrations/pdf/build.gradle.kts +++ b/integrations/pdf/build.gradle.kts @@ -38,6 +38,5 @@ tasks { dependsOn(":detekt-rules:assemble") getByName("build").dependsOn(this) } - withType { dependsOn(withType()) } } diff --git a/integrations/postgresql/build.gradle.kts b/integrations/postgresql/build.gradle.kts index e30f8dce9..91759b10f 100644 --- a/integrations/postgresql/build.gradle.kts +++ b/integrations/postgresql/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { implementation(libs.uuid) implementation(libs.hikari) implementation(libs.postgresql) - testImplementation(libs.junit.jupiter.api) testImplementation(libs.kotest.property) testImplementation(libs.kotest.framework) @@ -48,10 +47,7 @@ tasks { dependsOn(":detekt-rules:assemble") getByName("build").dependsOn(this) } - withType { dependsOn(withType()) } } -tasks.test{ - useJUnitPlatform() -} +tasks.test{ useJUnitPlatform() } diff --git a/integrations/sql/build.gradle.kts b/integrations/sql/build.gradle.kts index f66a674cc..128751c42 100644 --- a/integrations/sql/build.gradle.kts +++ b/integrations/sql/build.gradle.kts @@ -1,24 +1,18 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { id(libs.plugins.kotlin.jvm.get().pluginId) alias(libs.plugins.arrow.gradle.publish) alias(libs.plugins.semver.gradle) - alias(libs.plugins.detekt) + alias(libs.plugins.detekt) } dependencies { detektPlugins(project(":detekt-rules")) } -repositories { - mavenCentral() -} +repositories { mavenCentral() } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 - toolchain { - languageVersion = JavaLanguageVersion.of(11) - } + toolchain { languageVersion = JavaLanguageVersion.of(11) } } detekt { @@ -43,6 +37,5 @@ tasks { dependsOn(":detekt-rules:assemble") getByName("build").dependsOn(this) } - withType { dependsOn(withType()) } } diff --git a/java/README.md b/java/README.md index 8c8f7c4ee..380f6937b 100644 --- a/java/README.md +++ b/java/README.md @@ -2,7 +2,7 @@ Build the project locally, from the project root: -```bash +```shell ./gradlew build ``` @@ -11,7 +11,7 @@ Build the project locally, from the project root: The Java module uses the [spotless](https://github.com/diffplug/spotless/tree/main/plugin-gradle#java) plugin. Therefore, the previous command (`./gradlew build`) will fail in case there is any formatting issue. To apply format, you can run the following command: -```bash +```shell ./gradlew spotlessApply ``` @@ -23,4 +23,4 @@ Check out some use case at the [Java examples](../examples/java) folder. How to run the examples (from IntelliJ IDEA): -* Set Env variable: "OPENAI_TOKEN=xxx" +* Set Env variable: `OPENAI_TOKEN=xxx` diff --git a/java/build.gradle.kts b/java/build.gradle.kts index 4d9a61920..99ba21ab2 100644 --- a/java/build.gradle.kts +++ b/java/build.gradle.kts @@ -27,10 +27,6 @@ java { withSourcesJar() } -tasks.withType().configureEach { - useJUnit() -} +tasks.withType().configureEach { useJUnit() } -tasks.withType { - dependsOn(tasks.withType()) -} +tasks.withType { dependsOn(tasks.withType()) } diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts index 2c49aba70..0b0d2e196 100644 --- a/kotlin/build.gradle.kts +++ b/kotlin/build.gradle.kts @@ -35,12 +35,10 @@ kotlin { browser() nodejs() } - linuxX64() macosX64() macosArm64() mingwX64() - sourceSets { val commonMain by getting { dependencies { @@ -79,7 +77,6 @@ tasks { setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError")) } } - withType().configureEach { kotlin.sourceSets.forEach { kotlinSourceSet -> dokkaSourceSets.named(kotlinSourceSet.name) { @@ -89,15 +86,11 @@ tasks { } skipDeprecated.set(true) reportUndocumented.set(false) - val baseUrl: String = checkNotNull(project.properties["pom.smc.url"]?.toString()) - + val baseUrl = 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() - ) + remoteUrl.set(uri("$baseUrl/blob/main/${srcDir.relativeTo(rootProject.rootDir)}").toURL()) remoteLineSuffix.set("#L") } } diff --git a/models/gpt4all/get-models.sh b/models/gpt4all/get-models.sh index 459dec821..32afa866d 100755 --- a/models/gpt4all/get-models.sh +++ b/models/gpt4all/get-models.sh @@ -1,4 +1,3 @@ #!/usr/bin/env bash -#curl --url https://gpt4all.io/models/ggml-gpt4all-l13b-snoozy.bin --output ./ggml-gpt4all-l13b-snoozy.bin curl --url https://gpt4all.io/models/ggml-gpt4all-j-v1.3-groovy.bin --output ./ggml-gpt4all-j-v1.3-groovy.bin diff --git a/openai/build.gradle.kts b/openai/build.gradle.kts index 20bc75bbe..7e846e525 100644 --- a/openai/build.gradle.kts +++ b/openai/build.gradle.kts @@ -14,7 +14,6 @@ plugins { alias(libs.plugins.arrow.gradle.publish) alias(libs.plugins.semver.gradle) alias(libs.plugins.detekt) - // id("com.xebia.asfuture").version("0.0.1") } dependencies { detektPlugins(project(":detekt-rules")) } @@ -35,21 +34,16 @@ detekt { kotlin { jvm { compilations { - val integrationTest by - compilations.creating { - // Create a test task to run the tests produced by this compilation: - tasks.register("integrationTest") { - description = "Run the integration tests" - group = "verification" - classpath = - compileDependencyFiles + - runtimeDependencyFiles + - output.allOutputs - testClassesDirs = output.classesDirs - - testLogging { events("passed") } - } - } + val integrationTest by compilations.creating { + // Create a test task to run the tests produced by this compilation: + tasks.register("integrationTest") { + description = "Run the integration tests" + group = "verification" + classpath = compileDependencyFiles + runtimeDependencyFiles + output.allOutputs + testClassesDirs = output.classesDirs + testLogging { events("passed") } + } + } val test by compilations.getting integrationTest.associateWith(test) } @@ -58,15 +52,12 @@ kotlin { browser() nodejs() } - linuxX64() macosX64() macosArm64() mingwX64() - sourceSets { all { languageSettings.optIn("kotlin.ExperimentalStdlibApi") } - val commonMain by getting { dependencies { implementation(projects.xefCore) @@ -74,7 +65,6 @@ kotlin { implementation(libs.klogging) } } - val commonTest by getting { dependencies { implementation(libs.kotest.property) @@ -82,31 +72,22 @@ kotlin { implementation(libs.kotest.assertions) } } - val jvmMain by getting { dependencies { implementation(libs.logback) api(libs.ktor.client.cio) } } - val jvmTest by getting { dependencies { implementation(libs.kotest.junit5) } } - val jsMain by getting { dependencies { api(libs.ktor.client.js) } } - val linuxX64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val macosX64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val macosArm64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val mingwX64Main by getting { dependencies { api(libs.ktor.client.winhttp) } } - val linuxX64Test by getting val macosX64Test by getting val macosArm64Test by getting val mingwX64Test by getting - create("nativeMain") { dependsOn(commonMain) linuxX64Main.dependsOn(this) @@ -114,7 +95,6 @@ kotlin { macosArm64Main.dependsOn(this) mingwX64Main.dependsOn(this) } - create("nativeTest") { dependsOn(commonTest) linuxX64Test.dependsOn(this) @@ -153,7 +133,6 @@ tasks { setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError")) } } - withType().configureEach { kotlin.sourceSets.forEach { kotlinSourceSet -> dokkaSourceSets.named(kotlinSourceSet.name) { @@ -163,15 +142,11 @@ tasks { } skipDeprecated.set(true) reportUndocumented.set(false) - val baseUrl: String = checkNotNull(project.properties["pom.smc.url"]?.toString()) - + val baseUrl = 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() - ) + remoteUrl.set(uri("$baseUrl/blob/main/${srcDir.relativeTo(rootProject.rootDir)}").toURL()) remoteLineSuffix.set("#L") } } diff --git a/reasoning/build.gradle.kts b/reasoning/build.gradle.kts index ce103b59e..84a10a426 100644 --- a/reasoning/build.gradle.kts +++ b/reasoning/build.gradle.kts @@ -14,7 +14,6 @@ plugins { alias(libs.plugins.arrow.gradle.publish) alias(libs.plugins.semver.gradle) alias(libs.plugins.detekt) - // id("com.xebia.asfuture").version("0.0.1") } java { @@ -35,21 +34,16 @@ detekt { kotlin { jvm { compilations { - val integrationTest by - compilations.creating { - // Create a test task to run the tests produced by this compilation: - tasks.register("integrationTest") { - description = "Run the integration tests" - group = "verification" - classpath = - compileDependencyFiles + - runtimeDependencyFiles + - output.allOutputs - testClassesDirs = output.classesDirs - - testLogging { events("passed") } - } - } + val integrationTest by compilations.creating { + // Create a test task to run the tests produced by this compilation: + tasks.register("integrationTest") { + description = "Run the integration tests" + group = "verification" + classpath = compileDependencyFiles + runtimeDependencyFiles + output.allOutputs + testClassesDirs = output.classesDirs + testLogging { events("passed") } + } + } val test by compilations.getting integrationTest.associateWith(test) } @@ -58,15 +52,12 @@ kotlin { browser() nodejs() } - linuxX64() macosX64() macosArm64() mingwX64() - sourceSets { all { languageSettings.optIn("kotlin.ExperimentalStdlibApi") } - val commonMain by getting { dependencies { implementation(projects.xefCore) @@ -76,7 +67,6 @@ kotlin { implementation(libs.bundles.ktor.client) } } - val commonTest by getting { dependencies { implementation(libs.kotest.property) @@ -84,7 +74,6 @@ kotlin { implementation(libs.kotest.assertions) } } - val jvmMain by getting { dependencies { implementation(libs.logback) @@ -93,19 +82,12 @@ kotlin { api(libs.ktor.client.cio) } } - val jsMain by getting { dependencies { api(libs.ktor.client.js) } } - val jvmTest by getting { dependencies { implementation(libs.kotest.junit5) } } - val linuxX64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val macosX64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val macosArm64Main by getting { dependencies { api(libs.ktor.client.cio) } } - val mingwX64Main by getting { dependencies { api(libs.ktor.client.winhttp) } } - create("nativeMain") { dependsOn(commonMain) linuxX64Main.dependsOn(this) @@ -135,7 +117,6 @@ tasks { dependsOn(":detekt-rules:assemble") getByName("build").dependsOn(this) } - withType().configureEach { maxParallelForks = Runtime.getRuntime().availableProcessors() useJUnitPlatform() @@ -144,7 +125,6 @@ tasks { setEvents(listOf("passed", "skipped", "failed", "standardOut", "standardError")) } } - withType().configureEach { kotlin.sourceSets.forEach { kotlinSourceSet -> dokkaSourceSets.named(kotlinSourceSet.name) { @@ -154,15 +134,11 @@ tasks { } skipDeprecated.set(true) reportUndocumented.set(false) - val baseUrl: String = checkNotNull(project.properties["pom.smc.url"]?.toString()) - + val baseUrl = 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() - ) + remoteUrl.set(uri("$baseUrl/blob/main/${srcDir.relativeTo(rootProject.rootDir)}").toURL()) remoteLineSuffix.set("#L") } } diff --git a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ReActAgent.kt b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ReActAgent.kt index 25088ebda..1f8577308 100644 --- a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ReActAgent.kt +++ b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ReActAgent.kt @@ -19,7 +19,19 @@ class ReActAgent( private val scope: Conversation, private val tools: List, private val maxIterations: Int = 10, - private val configuration: PromptConfiguration = PromptConfiguration(temperature = 0.0) + private val configuration: PromptConfiguration = PromptConfiguration(temperature = 0.0), + private val critique: suspend ReActAgent.(String, Finish) -> Critique = + { input: String, finish: Finish -> + critiqueCall(input, finish) + }, + private val decide: suspend ReActAgent.(String, Int, ThoughtObservation) -> Decide = + { input: String, iterations: Int, thought: ThoughtObservation -> + decideCall(input, iterations, thought) + }, + private val finish: suspend ReActAgent.(String) -> Finish = { input: String -> + finishCall(input) + }, + private val runTool: suspend ReActAgent.() -> RunTool = { runToolCall() }, ) : Conversation by scope { sealed class Result { @@ -32,7 +44,7 @@ class ReActAgent( data class Finish(val result: String) : Result() } - private data class ThoughtObservation(val thought: String, val observation: String) + data class ThoughtObservation(val thought: String, val observation: String) @Serializable enum class NextStep { @@ -161,11 +173,11 @@ class ReActAgent( if (currentIteration > maxIterations) { emit(Result.MaxIterationsReached("🤷‍ Max iterations reached")) } else { - val decide = decideCall(prompt = prompt, thought = thought, iterations = currentIteration) + val decide = decide(prompt, currentIteration, thought) emit(Result.Log("🤖 I decided : ${decide.thought}")) when (decide.nextStep) { NextStep.RunTool -> { - val runTool = runToolCall() + val runTool = runTool() val tool = tools.find { it.name.equals(runTool.tool, ignoreCase = true) } if (tool == null) { emit(Result.Log("🤖 I don't know how to use the tool ${runTool.tool}")) @@ -187,8 +199,8 @@ class ReActAgent( } } NextStep.Finish -> { - val result = finishCall(prompt = prompt) - val critique = critiqueCall(prompt = prompt, finish = result) + val result = finish(prompt) + val critique = critique(prompt, result) emit(Result.Log("🤖 After critiquing the answer I decided : ${critique.thought}")) when (critique.outcome) { CompleteAnswerForUserRequest -> emit(Result.Finish(result.result)) diff --git a/scala/.scalafmt.conf b/scala/.scalafmt.conf index f6540bcff..038d0ede0 100644 --- a/scala/.scalafmt.conf +++ b/scala/.scalafmt.conf @@ -1,6 +1,6 @@ # tune this file as appropriate to your style! see: https://olafurpg.github.io/scalafmt/#Configuration -version = "3.7.3" +version = "3.7.15" runner.dialect = "scala3" @@ -17,7 +17,6 @@ align { ifWhileOpenParen = false openParenCallSite = false openParenDefnSite = false - tokens = ["%", "%%"] } @@ -34,4 +33,4 @@ optIn { project.excludeFilters = [ "metals.sbt" -] \ No newline at end of file +] diff --git a/scala/README.md b/scala/README.md index 611295003..e74c18873 100644 --- a/scala/README.md +++ b/scala/README.md @@ -2,16 +2,17 @@ Build the project locally, from the project root: -```bash +```shell ./gradlew build ``` ## Scalafmt The Scala module uses the [spotless](https://github.com/diffplug/spotless/tree/main/plugin-gradle#scala) plugin. -Therefore, the previous command (`./gradlew build`) will fail in case there is any formatting issue. To apply format, you can run the following command: +Therefore, the previous command (`./gradlew build`) will fail if there are any formatting issues. +To apply formatting, you can run the following command: -```bash +```shell ./gradlew spotlessApply ``` @@ -23,6 +24,6 @@ Check out some use case at the [Scala examples](../examples/scala) folder. How to run the examples (from IntelliJ IDEA): -* Set Java version 19 -* Set VM options: "--enable-preview" -* Set Env variable: "OPENAI_TOKEN=xxx" +* Set Java version 20 or above +* Set VM options: `--enable-preview` (if using Java 20 specifically) +* Set Env variable: `OPENAI_TOKEN=xxx` diff --git a/scala/build.gradle.kts b/scala/build.gradle.kts index 16d80a64b..e17e0c4c4 100644 --- a/scala/build.gradle.kts +++ b/scala/build.gradle.kts @@ -12,11 +12,9 @@ plugins { dependencies { implementation(projects.xefCore) implementation(projects.xefOpenai) - implementation(libs.kotlinx.coroutines.reactive) - + implementation(libs.kotlinx.coroutines.reactive) // TODO split to separate Scala library implementation(projects.xefPdf) - implementation(libs.circe.parser) implementation(libs.circe) implementation(libs.scala.lang) @@ -25,24 +23,16 @@ dependencies { } java { - sourceCompatibility = JavaVersion.VERSION_20 - targetCompatibility = JavaVersion.VERSION_20 - toolchain { - languageVersion = JavaLanguageVersion.of(20) - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + toolchain { languageVersion = JavaLanguageVersion.of(21) } withSourcesJar() } -tasks.withType().configureEach { - useJUnit() -} +tasks.withType().configureEach { useJUnit() } tasks.withType { scalaCompileOptions.additionalParameters = listOf("-Wunused:all", "-Wvalue-discard") } -spotless { - scala { - scalafmt("3.7.3").configFile(".scalafmt.conf").scalaMajorVersion("2.13") - } -} +spotless { scala { scalafmt("3.7.15").configFile(".scalafmt.conf") } } diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/KotlinXSerializers.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/KotlinXSerializers.scala deleted file mode 100644 index cd57424c2..000000000 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/KotlinXSerializers.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.xebia.functional.xef.scala.conversation - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.BuiltinSerializersKt -import kotlinx.serialization.builtins.BuiltinSerializersKt.serializer - -import java.lang - -object KotlinXSerializers: - val int: KSerializer[Integer] = - serializer(kotlin.jvm.internal.IntCompanionObject.INSTANCE) - - val string: KSerializer[String] = - serializer(kotlin.jvm.internal.StringCompanionObject.INSTANCE) - - val boolean: KSerializer[lang.Boolean] = - serializer(kotlin.jvm.internal.BooleanCompanionObject.INSTANCE) - - val double: KSerializer[lang.Double] = - serializer(kotlin.jvm.internal.DoubleCompanionObject.INSTANCE) - - val float: KSerializer[lang.Float] = - serializer(kotlin.jvm.internal.FloatCompanionObject.INSTANCE) - - val long: KSerializer[lang.Long] = - serializer(kotlin.jvm.internal.LongCompanionObject.INSTANCE) - - val short: KSerializer[lang.Short] = - serializer(kotlin.jvm.internal.ShortCompanionObject.INSTANCE) - - val byte: KSerializer[lang.Byte] = - serializer(kotlin.jvm.internal.ByteCompanionObject.INSTANCE) - - val char: KSerializer[Character] = - serializer(kotlin.jvm.internal.CharCompanionObject.INSTANCE) - - val unit: KSerializer[kotlin.Unit] = - serializer(kotlin.Unit.INSTANCE) diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/package.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/package.scala index 7f4d107c6..d65b83efa 100644 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/package.scala +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/package.scala @@ -1,84 +1,65 @@ package com.xebia.functional.xef.scala.conversation +import com.xebia.functional.xef.conversation.* import com.xebia.functional.xef.conversation.llm.openai.* -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.conversation.{FromJson, JVMConversation} +import com.xebia.functional.xef.conversation.llm.openai.OpenAI.FromEnvironment.* import com.xebia.functional.xef.llm.* import com.xebia.functional.xef.llm.models.images.* -import com.xebia.functional.xef.store.{ConversationId, LocalVectorStore, VectorStore} -import com.xebia.functional.xef.metrics.{LogsMetric, Metric} +import com.xebia.functional.xef.metrics.* +import com.xebia.functional.xef.prompt.Prompt +import com.xebia.functional.xef.scala.serialization.* +import com.xebia.functional.xef.store.* import io.circe.Decoder import io.circe.parser.parse -import org.reactivestreams.{Subscriber, Subscription} +import org.reactivestreams.* -import java.util -import java.util.UUID +import java.util.UUID.* import java.util.concurrent.LinkedBlockingQueue import scala.jdk.CollectionConverters.* -class ScalaConversation(store: VectorStore, metric: Metric, conversationId: Option[ConversationId]) - extends JVMConversation(store, metric, conversationId.orNull) +class ScalaConversation(store: VectorStore, metric: Metric, conversationId: ConversationId) extends JVMConversation(store, metric, conversationId) def addContext(context: Array[String])(using conversation: ScalaConversation): Unit = conversation.addContextFromArray(context).join() -def prompt[A: Decoder: SerialDescriptor]( - prompt: Prompt, - chat: ChatWithFunctions = OpenAI.FromEnvironment.DEFAULT_SERIALIZATION -)(using - conversation: ScalaConversation -): A = - val fromJson = new FromJson[A] { - def fromJson(json: String): A = - parse(json).flatMap(Decoder[A].decodeJson(_)).fold(throw _, identity) - } +def prompt[A: Decoder: SerialDescriptor](prompt: Prompt, chat: ChatWithFunctions = DEFAULT_SERIALIZATION)(using conversation: ScalaConversation): A = conversation.prompt(chat, prompt, chat.chatFunction(SerialDescriptor[A].serialDescriptor), fromJson).join() -def promptMessage( - prompt: Prompt, - chat: Chat = OpenAI.FromEnvironment.DEFAULT_CHAT -)(using conversation: ScalaConversation): String = +def promptMessage(prompt: Prompt, chat: Chat = DEFAULT_CHAT)(using conversation: ScalaConversation): String = conversation.promptMessage(chat, prompt).join() -def promptMessages( - prompt: Prompt, - chat: Chat = OpenAI.FromEnvironment.DEFAULT_CHAT -)(using - conversation: ScalaConversation -): List[String] = +def promptMessages(prompt: Prompt, chat: Chat = DEFAULT_CHAT)(using conversation: ScalaConversation): List[String] = conversation.promptMessages(chat, prompt).join().asScala.toList -def promptStreaming( - prompt: Prompt, - chat: Chat = OpenAI.FromEnvironment.DEFAULT_CHAT -)(using - conversation: ScalaConversation -): LazyList[String] = +def promptStreaming(prompt: Prompt, chat: Chat = DEFAULT_CHAT)(using conversation: ScalaConversation): LazyList[String] = val publisher = conversation.promptStreamingToPublisher(chat, prompt) val queue = new LinkedBlockingQueue[String]() - publisher.subscribe(new Subscriber[String] { - // TODO change to fs2 or similar + publisher.subscribe(new Subscriber[String]: // TODO change to fs2 or similar def onSubscribe(s: Subscription): Unit = s.request(Long.MaxValue) - def onNext(t: String): Unit = queue.add(t); () - def onError(t: Throwable): Unit = throw t - def onComplete(): Unit = () - }) + ) LazyList.continually(queue.take) -def images( - prompt: Prompt, - images: Images = OpenAI.FromEnvironment.DEFAULT_IMAGES, - numberImages: Int = 1, - size: String = "1024x1024" -)(using +def prompt[A: Decoder: SerialDescriptor](message: String)(using ScalaConversation): A = + prompt(Prompt(message)) + +def promptMessage(message: String)(using ScalaConversation): String = + promptMessage(Prompt(message)) + +def promptMessages(message: String)(using ScalaConversation): List[String] = + promptMessages(Prompt(message)) + +def promptStreaming(message: String)(using ScalaConversation): LazyList[String] = + promptStreaming(Prompt(message)) + +def images(prompt: Prompt, images: Images = DEFAULT_IMAGES, numberImages: Int = 1, size: String = "1024x1024")(using conversation: ScalaConversation ): ImagesGenerationResponse = conversation.images(images, prompt, numberImages, size).join() -def conversation[A]( - block: ScalaConversation ?=> A, - conversationId: Option[ConversationId] = Some(ConversationId(UUID.randomUUID().toString)) -): A = block(using ScalaConversation(LocalVectorStore(OpenAI.FromEnvironment.DEFAULT_EMBEDDING), LogsMetric(), conversationId)) +def conversation[A](block: ScalaConversation ?=> A, id: ConversationId = ConversationId(randomUUID.toString)): A = + block(using ScalaConversation(LocalVectorStore(DEFAULT_EMBEDDING), LogsMetric(), id)) + +private def fromJson[A: Decoder]: FromJson[A] = json => parse(json).flatMap(Decoder[A].decodeJson(_)).fold(throw _, identity) diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/KotlinXSerializers.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/KotlinXSerializers.scala new file mode 100644 index 000000000..30aa05e4a --- /dev/null +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/KotlinXSerializers.scala @@ -0,0 +1,18 @@ +package com.xebia.functional.xef.scala.serialization + +import kotlin.jvm.internal.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.BuiltinSerializersKt +import kotlinx.serialization.builtins.BuiltinSerializersKt.serializer + +object KotlinXSerializers: + val int: KSerializer[java.lang.Integer] = serializer(IntCompanionObject.INSTANCE) + val string: KSerializer[String] = serializer(StringCompanionObject.INSTANCE) + val boolean: KSerializer[java.lang.Boolean] = serializer(BooleanCompanionObject.INSTANCE) + val double: KSerializer[java.lang.Double] = serializer(DoubleCompanionObject.INSTANCE) + val float: KSerializer[java.lang.Float] = serializer(FloatCompanionObject.INSTANCE) + val long: KSerializer[java.lang.Long] = serializer(LongCompanionObject.INSTANCE) + val short: KSerializer[java.lang.Short] = serializer(ShortCompanionObject.INSTANCE) + val byte: KSerializer[java.lang.Byte] = serializer(ByteCompanionObject.INSTANCE) + val char: KSerializer[Character] = serializer(CharCompanionObject.INSTANCE) + val unit: KSerializer[kotlin.Unit] = serializer(kotlin.Unit.INSTANCE) diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptor.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptor.scala similarity index 94% rename from scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptor.scala rename to scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptor.scala index a7dc121df..9b574065a 100644 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptor.scala +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptor.scala @@ -1,20 +1,19 @@ -package com.xebia.functional.xef.scala.conversation +package com.xebia.functional.xef.scala.serialization import com.xebia.functional.xef.conversation.jvm.Description as JvmDescription -import kotlinx.serialization.descriptors.{SerialDescriptor as KtSerialDescriptor, SerialKind, StructureKind} import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.{SerialDescriptor as KtSerialDescriptor, SerialKind, StructureKind} import kotlinx.serialization.encoding.{Decoder as KtDecoder, Encoder as KtEncoder} -import scala.quoted.* import java.lang.annotation.Annotation import java.util -import scala.compiletime.{constValue, erasedValue, summonInline} +import scala.annotation.meta.* +import scala.compiletime.* import scala.deriving.* import scala.jdk.CollectionConverters.* +import scala.quoted.* import scala.reflect.ClassTag -import scala.annotation.meta._ - trait SerialDescriptor[A]: def serialDescriptor: KtSerialDescriptor def kserializer: KSerializer[A] = new KSerializer[A]: @@ -81,40 +80,33 @@ object SerialDescriptor extends SerialDescriptorInstances: // Does the element at the given index have a default value, or is it wrapped in `Option`, or is a union with `Null`? override def isElementOptional(index: Int): Boolean = false - def serialDescriptor = serialDescriptorImpl + def serialDescriptor: KtSerialDescriptor = serialDescriptorImpl inline def getStaticAnnotations[A]: List[Any] = ${ getAnnotationsImpl[A] } -def getAnnotationsImpl[A: Type](using Quotes): Expr[List[Any]] = { +def getAnnotationsImpl[A: Type](using Quotes): Expr[List[Any]] = import quotes.reflect.* val annotations = TypeRepr.of[A].typeSymbol.annotations.map(_.asExpr) Expr.ofList(annotations) -} inline def getFieldAnnotationsMap[A]: Map[String, List[Any]] = ${ getFieldAnnotationsMapImpl[A] } -def getFieldAnnotationsMapImpl[A: Type](using Quotes): Expr[Map[String, List[Any]]] = { - import quotes.reflect._ - +def getFieldAnnotationsMapImpl[A: Type](using Quotes): Expr[Map[String, List[Any]]] = + import quotes.reflect.* // Extract the fields from the type val fields = TypeRepr.of[A].typeSymbol.primaryConstructor.paramSymss.flatten - // For each field, extract its name and annotations - val fieldAnnotationsMap: List[Expr[(String, List[Any])]] = fields.map { field => + val fieldAnnotationsMap: List[Expr[(String, List[Any])]] = fields.map: field => val fieldName = Expr(field.name) val annotations = Expr.ofList(field.annotations.map(_.asExpr)) '{ ($fieldName, $annotations) } - } - // Convert list of (String, List[Any]) tuples to a map - fieldAnnotationsMap match { + fieldAnnotationsMap match case Nil => '{ Map.empty[String, List[Any]] } case _ => val mapExpr = Expr.ofList(fieldAnnotationsMap) '{ $mapExpr.toMap } - } -} /** * A scala annotation that maps to the jvm description annotation diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorInstances.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorInstances.scala similarity index 98% rename from scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorInstances.scala rename to scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorInstances.scala index c213c28e9..ee761f27c 100644 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorInstances.scala +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorInstances.scala @@ -1,4 +1,4 @@ -package com.xebia.functional.xef.scala.conversation +package com.xebia.functional.xef.scala.serialization import kotlin.jvm.internal.Reflection import kotlin.reflect.KClass diff --git a/scala/src/test/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorSpec.scala b/scala/src/test/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorSpec.scala similarity index 98% rename from scala/src/test/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorSpec.scala rename to scala/src/test/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorSpec.scala index 6141cc617..4c103b6e1 100644 --- a/scala/src/test/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorSpec.scala +++ b/scala/src/test/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorSpec.scala @@ -1,4 +1,4 @@ -package com.xebia.functional.xef.scala.conversation +package com.xebia.functional.xef.scala.serialization import cats.syntax.either.* import kotlinx.serialization.builtins.BuiltinSerializersKt diff --git a/server/README.md b/server/README.md index a5328da75..0c311f895 100644 --- a/server/README.md +++ b/server/README.md @@ -8,16 +8,16 @@ In order to run the server, you need to run the following services: ### Docker -```bash - docker-compose -f docker/postgresql/docker-compose.yaml up +```shell +docker-compose -f docker/postgresql/docker-compose.yaml up ``` ### Server -```bash - ./gradlew server +```shell +./gradlew server ``` ### Web -Please, refer to the [web README](web/README.md) for more information. +Please refer to the [web README](web/README.md) for more information. diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 6b7025df3..d7d360436 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -7,21 +7,15 @@ plugins { alias(libs.plugins.spotless) } -repositories { - mavenCentral() -} +repositories { mavenCentral() } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 - toolchain { - languageVersion = JavaLanguageVersion.of(11) - } + toolchain { languageVersion = JavaLanguageVersion.of(11) } } -node { - nodeProjectDir.set(file("${project.projectDir}/web")) -} +node { nodeProjectDir.set(file("${project.projectDir}/web")) } dependencies { implementation(libs.exposed.core) @@ -58,7 +52,6 @@ dependencies { implementation(projects.xefCore) implementation(projects.xefLucene) implementation(projects.xefPostgresql) - testImplementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.engine) testImplementation(libs.kotest.property) @@ -72,9 +65,7 @@ dependencies { spotless { kotlin { target("**/*.kt") - ktfmt().googleStyle().configure { - it.setRemoveUnusedImport(true) - } + ktfmt().googleStyle().configure { it.setRemoveUnusedImport(true) } } } @@ -90,7 +81,7 @@ task("web-app") { description = "xef-server web application" classpath = sourceSets.main.get().runtimeClasspath mainClass.set("com.xebia.functional.xef.server.WebApp") - } +} task("server") { dependsOn("compileKotlin") @@ -100,10 +91,6 @@ task("server") { mainClass.set("com.xebia.functional.xef.server.Server") } -tasks.named("test") { - useJUnitPlatform() -} +tasks.named("test") { useJUnitPlatform() } -tasks.withType { - dependsOn(tasks.withType()) -} +tasks.withType { dependsOn(tasks.withType()) } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/ModelUriAdapter.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/ModelUriAdapter.kt new file mode 100644 index 000000000..628560be9 --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/ModelUriAdapter.kt @@ -0,0 +1,79 @@ +package com.xebia.functional.xef.server.http.client + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.client.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.util.* +import io.ktor.util.pipeline.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive + +class ModelUriAdapter +internal constructor(private val urlMap: Map>) { + + val logger = KotlinLogging.logger {} + + fun isDefined(path: OpenAIPathType): Boolean = urlMap.containsKey(path) + + fun findPath(path: OpenAIPathType, model: String): String? = urlMap[path]?.get(model) + + companion object : HttpClientPlugin { + + override val key: AttributeKey = AttributeKey("ModelAuthAdapter") + + override fun prepare(block: ModelUriAdapterBuilder.() -> Unit): ModelUriAdapter = + ModelUriAdapterBuilder().apply(block).build() + + override fun install(plugin: ModelUriAdapter, scope: HttpClient) { + installModelAuthAdapter(plugin, scope) + } + + private fun readModelFromRequest(originalRequest: OutgoingContent.ByteArrayContent?): String? { + val requestBody = originalRequest?.bytes()?.toString(Charsets.UTF_8) + val json = requestBody?.let { Json.decodeFromString(it) } + return json?.get("model")?.jsonPrimitive?.content + } + + private fun installModelAuthAdapter(plugin: ModelUriAdapter, scope: HttpClient) { + val adaptAuthRequestPhase = PipelinePhase("ModelAuthAdaptRequest") + scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, adaptAuthRequestPhase) + scope.sendPipeline.intercept(adaptAuthRequestPhase) { content -> + val originalPath = OpenAIPathType.from(context.url.encodedPath) ?: return@intercept + if (plugin.isDefined(originalPath)) { + val originalRequest = content as? OutgoingContent.ByteArrayContent + if (originalRequest == null) { + plugin.logger.warn { + """ + |Can't adapt the model auth. + |The body type is: ${content::class}, with Content-Type: ${context.contentType()}. + | + |If you expect serialized body, please check that you have installed the corresponding + |plugin(like `ContentNegotiation`) and set `Content-Type` header.""" + .trimMargin() + } + return@intercept + } + val model = readModelFromRequest(originalRequest) + val newURL = model?.let { plugin.findPath(originalPath, it) } + if (newURL == null) { + plugin.logger.info { + "Model auth didn't found a new url for path $originalPath and model $model" + } + } else { + val baseBuilder = URLBuilder(newURL).build() + context.url.set( + scheme = baseBuilder.protocol.name, + host = baseBuilder.host, + port = baseBuilder.port, + path = baseBuilder.encodedPath + ) + } + } + } + } + } +} diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/ModelUriAdapterBuilder.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/ModelUriAdapterBuilder.kt new file mode 100644 index 000000000..a2bda8039 --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/ModelUriAdapterBuilder.kt @@ -0,0 +1,17 @@ +package com.xebia.functional.xef.server.http.client + +class ModelUriAdapterBuilder { + + private var pathMap: Map> = LinkedHashMap() + + fun setPathMap(pathMap: Map>) { + this.pathMap = pathMap + } + + fun addToPath(path: OpenAIPathType, vararg modelUriPaths: Pair) { + val newPathTypeMap = mapOf(*modelUriPaths.map { Pair(it.first, it.second) }.toTypedArray()) + this.pathMap += mapOf(path to newPathTypeMap) + } + + internal fun build(): ModelUriAdapter = ModelUriAdapter(pathMap) +} diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/OpenAIPathType.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/OpenAIPathType.kt new file mode 100644 index 000000000..786fc2b80 --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/client/OpenAIPathType.kt @@ -0,0 +1,15 @@ +package com.xebia.functional.xef.server.http.client + +enum class OpenAIPathType(val value: String) { + CHAT("/v1/chat/completions"), + EMBEDDINGS("/v1/embeddings"), + FINE_TUNING("/v1/fine_tuning/jobs"), + FILES("/v1/files"), + IMAGES("/v1/images/generations"), + MODELS("/v1/models"), + MODERATION("/v1/moderations"); + + companion object { + fun from(v: String): OpenAIPathType? = entries.find { it.value == v } + } +} diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AIRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AIRoutes.kt index 99bb38f43..407c12430 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AIRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AIRoutes.kt @@ -75,7 +75,7 @@ private suspend fun HttpClient.makeRequest( method = HttpMethod.Post setBody(body) } - call.response.headers.copyFrom(response.headers) + call.response.headers.copyFrom(response.headers, "Content-Length") call.respond(response.status, response.readBytes()) } @@ -91,17 +91,18 @@ private suspend fun HttpClient.makeStreaming( setBody(body) } .execute { httpResponse -> - call.response.headers.copyFrom(httpResponse.headers) + call.response.headers.copyFrom(httpResponse.headers, "Content-Length") call.respondOutputStream { httpResponse.bodyAsChannel().copyTo(this@respondOutputStream) } } } -private fun ResponseHeaders.copyFrom(headers: Headers) = +private fun ResponseHeaders.copyFrom(headers: Headers, vararg filterOut: String) = headers .entries() .filter { (key, _) -> !HttpHeaders.isUnsafe(key) } // setting unsafe headers results in exception + .filterNot { (key, _) -> filterOut.any { it.equals(key, true) } } .forEach { (key, values) -> values.forEach { value -> this.appendIfAbsent(key, value) } } internal fun HeadersBuilder.copyFrom(headers: Headers) = diff --git a/server/web/README.md b/server/web/README.md index 55ef9366a..371e940cd 100644 --- a/server/web/README.md +++ b/server/web/README.md @@ -19,7 +19,7 @@ You need to have Node.js versions 14.18+ or 16+. But the current LTS is [Node.js As other general JS and Node.js based projects, dependencies are managed through `npm`, so the first thing needed to be done to run the project is to install its dependencies with: -```bash +```shell npm install ``` @@ -29,17 +29,17 @@ npm install Through vite, you can currently run the following commands in the project: - Start a dev server to work on the project: - ```bash + ```shell npm run dev ``` - Build the project for production: - ```bash + ```shell npm run build ``` - Locally preview a production build: - ```bash + ```shell npm run preview ``` @@ -100,4 +100,3 @@ Thorugh [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/), you - Code concerning state and other contexts related functions are in `state`. - The MUI theme definition and configuration is set in the `styles` folder. You might not need to touch any of these settings. - Other utility and helper functions should be placed under `src/utils`. - diff --git a/settings.gradle.kts b/settings.gradle.kts index 6c8417956..6816a46bc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -63,7 +63,6 @@ project(":xef-kotlin").projectDir = file("kotlin") include("xef-kotlin-examples") project(":xef-kotlin-examples").projectDir = file("examples/kotlin") - // // @@ -72,7 +71,6 @@ project(":xef-scala-examples").projectDir = file("examples/scala") include("xef-scala") project(":xef-scala").projectDir = file("scala") - // // @@ -100,4 +98,3 @@ project(":xef-server").projectDir = file("server") include("detekt-rules") project(":detekt-rules").projectDir = file("detekt-rules") // - diff --git a/tokenizer/build.gradle.kts b/tokenizer/build.gradle.kts index d0e72fe80..a5e71ff72 100644 --- a/tokenizer/build.gradle.kts +++ b/tokenizer/build.gradle.kts @@ -35,10 +35,8 @@ kotlin { macosX64() macosArm64() mingwX64() - sourceSets { val commonMain by getting - commonTest { dependencies { implementation(kotlin("test")) @@ -48,19 +46,15 @@ kotlin { implementation("com.goncalossilva:resources:0.3.2") } } - val jvmTest by getting { dependencies { implementation(libs.kotest.junit5) } } - js { nodejs { testTask { useMocha { timeout = "10000" } } } browser { testTask { useMocha { timeout = "10000" } } } } - val linuxX64Main by getting val macosX64Main by getting val macosArm64Main by getting val mingwX64Main by getting - create("nativeMain") { dependsOn(commonMain) linuxX64Main.dependsOn(this)