diff --git a/.editorconfig b/.editorconfig index 6fc7e1c..138c31d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,7 +19,7 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false -[pom.xml] +[*.xml] indent_size = 4 [*.kt] @@ -27,3 +27,5 @@ indent_size = 4 max_line_length = 100 ij_kotlin_name_count_to_use_star_import = 999 ij_kotlin_name_count_to_use_star_import_for_members = 999 +# noinspection EditorConfigKeyCorrectness +ktlint_function_naming_ignore_when_annotated_with = "Test" diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml new file mode 100644 index 0000000..315bdfc --- /dev/null +++ b/.idea/externalDependencies.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Makefile b/Makefile index df04911..50873f9 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,8 @@ build: mvn clean verify site + +lint: + # brew install ktlint + ktlint --format + # https://docs.openrewrite.org/recipes/maven/bestpractices + mvn rewrite:run -P lint diff --git a/README.md b/README.md index cf93add..80c9bb9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Add the following dependencies to your `pom.xml`: me.kpavlov.langchain4j.kotlin - langchain4j-core-kotlin + langchain4j-kotlin [LATEST_VERSION] @@ -47,7 +47,7 @@ Add the following to your `build.gradle.kts`: ```kotlin dependencies { - implementation("me.kpavlov.langchain4j.kotlin:langchain4j-core-kotlin:$LATEST_VERSION") + implementation("me.kpavlov.langchain4j.kotlin:langchain4j-kotlin:$LATEST_VERSION") implementation("dev.langchain4j:langchain4j-core") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm") } @@ -113,11 +113,15 @@ Using Make: make build ``` - ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. +Run before submitting your changes +```shell +make lint +``` + ## Acknowledgements - [LangChain4j](https://github.com/langchain4j/langchain4j) - The core library this project enhances diff --git a/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/ChatLanguageModelIT.kt b/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/ChatLanguageModelIT.kt deleted file mode 100644 index b6ea8ab..0000000 --- a/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/ChatLanguageModelIT.kt +++ /dev/null @@ -1,96 +0,0 @@ -package me.kpavlov.langchain4j.kotlin - -import assertk.assertThat -import assertk.assertions.contains -import assertk.assertions.isNotNull -import assertk.assertions.isSuccess -import dev.langchain4j.data.document.Document -import dev.langchain4j.data.document.DocumentLoader -import dev.langchain4j.data.document.parser.TextDocumentParser -import dev.langchain4j.data.document.source.FileSystemSource -import dev.langchain4j.data.message.SystemMessage -import dev.langchain4j.data.message.UserMessage -import dev.langchain4j.model.chat.ChatLanguageModel -import dev.langchain4j.model.chat.chatAsync -import dev.langchain4j.model.chat.generateAsync -import dev.langchain4j.model.chat.request.ChatRequest -import dev.langchain4j.model.chat.request.ResponseFormat -import dev.langchain4j.model.openai.OpenAiChatModel -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.slf4j.LoggerFactory -import java.nio.file.Paths - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class ChatLanguageModelIT { - - private val logger = LoggerFactory.getLogger(javaClass) - - private val model: ChatLanguageModel = OpenAiChatModel - .builder() - .apiKey("demo") - .modelName("gpt-4o-mini") - .temperature(0.0) - .maxTokens(1024) - .build() - - private lateinit var document: Document - - @BeforeAll - fun beforeAll() = runTest { - document = loadDocument("notes/blumblefang.txt", logger) - } - - @Test - fun `ChatLanguageModel should generateAsync`() = runTest { - - val response = model.generateAsync( - listOf( - SystemMessage.from( - """ - You are helpful advisor answering questions only related to the given text""" - .trimIndent() - ), - UserMessage.from( - """ - What does Blumblefang love? Text: ```${document.text()}``` - """.trimIndent() - ), - ) - ) - - logger.info("Response: {}", response); - assertThat(response).isNotNull() - val content = response.content() - assertThat(content.text()).contains("Blumblefang loves to help") - } - - @Test - fun `ChatLanguageModel should chatAsync`() = runTest { - - val document = loadDocument("notes/blumblefang.txt", logger) - - val response = model.chatAsync(ChatRequest.builder() - .messages(listOf( - SystemMessage.from( - """ - You are helpful advisor answering questions only related to the given text""" - .trimIndent() - ), - UserMessage.from( - """ - What does Blumblefang love? Text: ```${document.text()}``` - """.trimIndent() - ), - )) - .responseFormat(ResponseFormat.TEXT) - ) - - logger.info("Response: {}", response); - assertThat(response).isNotNull() - val content = response.aiMessage() - assertThat(content.text()).contains("Blumblefang loves to help") - } -} diff --git a/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/StreamingChatLanguageModelIT.kt b/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/StreamingChatLanguageModelIT.kt deleted file mode 100644 index 3aaa7fc..0000000 --- a/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/StreamingChatLanguageModelIT.kt +++ /dev/null @@ -1,80 +0,0 @@ -package me.kpavlov.langchain4j.kotlin - -import assertk.assertThat -import assertk.assertions.contains -import assertk.assertions.isEqualTo -import assertk.assertions.isNotNull -import dev.langchain4j.data.message.AiMessage -import dev.langchain4j.data.message.ChatMessage -import dev.langchain4j.data.message.SystemMessage -import dev.langchain4j.data.message.UserMessage -import dev.langchain4j.model.chat.StreamingChatLanguageModel -import dev.langchain4j.model.chat.StreamingChatLanguageModelReply.Completion -import dev.langchain4j.model.chat.StreamingChatLanguageModelReply.Token -import dev.langchain4j.model.chat.generateFlow -import dev.langchain4j.model.openai.OpenAiStreamingChatModel -import dev.langchain4j.model.output.Response -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.fail -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.slf4j.LoggerFactory -import java.util.concurrent.atomic.AtomicReference - -@Disabled("Run it manually") -class StreamingChatLanguageModelIT { - - private val logger = LoggerFactory.getLogger(javaClass) - - private val model: StreamingChatLanguageModel = OpenAiStreamingChatModel - .builder() - .apiKey(TestEnvironment.env("OPENAI_API_KEY")) - .modelName("gpt-4o-mini") - .temperature(0.0) - .maxTokens(100) - .build() - - @Test - fun `StreamingChatLanguageModel should generateFlow`() = runTest { - val document = loadDocument("notes/blumblefang.txt", logger) - - val messages = listOf( - SystemMessage.from( - """ - You are helpful advisor answering questions only related to the given text""" - .trimIndent() - ), - UserMessage.from( - """ - What does Blumblefang love? Text: ```${document.text()}``` - """.trimIndent() - ), - ) - - val responseRef = AtomicReference?>() - - val collectedTokens = mutableListOf() - - model.generateFlow(messages) - .collect { - logger.info("Received event: $it") - when (it) { - is Token -> { - logger.info("Token: '${it.token}'") - collectedTokens.add(it.token) - } - - is Completion -> responseRef.set(it.response) - else -> fail("Unsupported event: $it") - } - } - - val response = responseRef.get()!! - assertThat(response.metadata()).isNotNull() - val content = response.content() - assertThat(content).isNotNull() - assertThat(collectedTokens.joinToString("")) - .isEqualTo(content.text()) - assertThat(content.text()).contains("Blumblefang loves to help") - } -} diff --git a/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/TestEnvironment.kt b/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/TestEnvironment.kt deleted file mode 100644 index 0c07fed..0000000 --- a/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/TestEnvironment.kt +++ /dev/null @@ -1,18 +0,0 @@ -package me.kpavlov.langchain4j.kotlin - -import io.github.cdimascio.dotenv.dotenv -import org.slf4j.LoggerFactory -import java.nio.file.Paths - -private val logger = LoggerFactory.getLogger(TestEnvironment.javaClass) - -object TestEnvironment { - private val dotenv = - dotenv { - directory = Paths.get("${System.getProperty("user.dir")}/..").normalize().toString() - logger.info("Loading .env file from $directory") - ignoreIfMissing = true - } - - fun env(name: String) = dotenv.get(name) -} diff --git a/langchain4j-kotlin/notebooks/lc4kNotebook.ipynb b/langchain4j-kotlin/notebooks/lc4kNotebook.ipynb new file mode 100644 index 0000000..d1187ec --- /dev/null +++ b/langchain4j-kotlin/notebooks/lc4kNotebook.ipynb @@ -0,0 +1,94 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "# Welcome to LangChain4j-Kotlin Notebook!" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-13T08:41:13.973072Z", + "start_time": "2024-11-13T08:41:11.937475Z" + } + }, + "cell_type": "code", + "source": [ + "import dev.langchain4j.model.chat.generateAsync\n", + "import dev.langchain4j.model.openai.OpenAiChatModel\n", + "import dev.langchain4j.data.message.SystemMessage\n", + "import dev.langchain4j.data.message.UserMessage\n", + "import kotlinx.coroutines.runBlocking\n", + "\n", + "val model = OpenAiChatModel\n", + " .builder()\n", + " .apiKey(\"demo\")\n", + " .modelName(\"gpt-4o-mini\")\n", + " .temperature(0.0)\n", + " .maxTokens(1024)\n", + " .build()\n", + "\n", + "runBlocking {\n", + " val result = model.generateAsync(\n", + " listOf(\n", + " SystemMessage.from(\"You are helpful assistant\"),\n", + " UserMessage.from(\"Make a joke about Kotlin, Langchani4j and LLM\"),\n", + " )\n", + " )\n", + " \n", + " println(result.content().text())\n", + "}\n", + "\n", + " \n", + " " + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Why did the Kotlin developer break up with Langchain4j?\n", + "\n", + "Because they found a new love in LLM that could handle their \"null\" feelings without throwing any exceptions!\n" + ] + } + ], + "execution_count": 36 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-13T08:39:55.837900Z", + "start_time": "2024-11-13T08:39:55.836524Z" + } + }, + "cell_type": "code", + "source": "", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "1.9.23", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "projectDependencies": [ + "langchain4j-kotlin" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/langchain4j-core-kotlin/pom.xml b/langchain4j-kotlin/pom.xml similarity index 65% rename from langchain4j-core-kotlin/pom.xml rename to langchain4j-kotlin/pom.xml index 4e54ed9..245a69b 100644 --- a/langchain4j-core-kotlin/pom.xml +++ b/langchain4j-kotlin/pom.xml @@ -1,13 +1,15 @@ - + 4.0.0 me.kpavlov.langchain4j.kotlin - langchain4j-kotlin-aggregator + root 0.1.1-SNAPSHOT + ../pom.xml - langchain4j-core-kotlin + langchain4j-kotlin LangChain4j-Kotlin :: Core @@ -22,9 +24,13 @@ org.slf4j slf4j-api - ${slf4j.version} + + org.junit.jupiter + junit-jupiter-api + test + dev.langchain4j langchain4j @@ -35,6 +41,11 @@ langchain4j-open-ai test + + me.kpavlov.finchly + finchly + test + diff --git a/langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/internal/Logging.kt b/langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/internal/Logging.kt similarity index 63% rename from langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/internal/Logging.kt rename to langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/internal/Logging.kt index c404544..19d9354 100644 --- a/langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/internal/Logging.kt +++ b/langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/internal/Logging.kt @@ -1,4 +1,4 @@ -package dev.langchain4j.internal +package me.kpavlov.langchain4j.kotlin.internal import org.slf4j.MarkerFactory diff --git a/langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/model/chat/ChatLanguageModelExtensions.kt b/langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/ChatLanguageModelExtensions.kt similarity index 94% rename from langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/model/chat/ChatLanguageModelExtensions.kt rename to langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/ChatLanguageModelExtensions.kt index 9b0747d..b2b7ec4 100644 --- a/langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/model/chat/ChatLanguageModelExtensions.kt +++ b/langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/ChatLanguageModelExtensions.kt @@ -1,7 +1,8 @@ -package dev.langchain4j.model.chat +package me.kpavlov.langchain4j.kotlin.model.chat import dev.langchain4j.data.message.AiMessage import dev.langchain4j.data.message.ChatMessage +import dev.langchain4j.model.chat.ChatLanguageModel import dev.langchain4j.model.chat.request.ChatRequest import dev.langchain4j.model.chat.response.ChatResponse import dev.langchain4j.model.output.Response @@ -55,9 +56,8 @@ suspend fun ChatLanguageModel.chatAsync(request: ChatRequest): ChatResponse { * @see ChatRequest.Builder * @see chatAsync */ -suspend fun ChatLanguageModel.chatAsync(requestBuilder: ChatRequest.Builder): ChatResponse { - return chatAsync(requestBuilder.build()) -} +suspend fun ChatLanguageModel.chatAsync(requestBuilder: ChatRequest.Builder): ChatResponse = + chatAsync(requestBuilder.build()) /** * Processes a chat request using a [ChatRequest.Builder] for convenient request @@ -82,9 +82,8 @@ suspend fun ChatLanguageModel.chatAsync(requestBuilder: ChatRequest.Builder): Ch * @see ChatResponse * @see ChatRequest.Builder */ -fun ChatLanguageModel.chat(requestBuilder: ChatRequest.Builder): ChatResponse { - return this.chat(requestBuilder.build()) -} +fun ChatLanguageModel.chat(requestBuilder: ChatRequest.Builder): ChatResponse = + this.chat(requestBuilder.build()) /** * Asynchronously generates a response for a list of chat messages using @@ -113,5 +112,3 @@ suspend fun ChatLanguageModel.generateAsync(messages: List): Respon val model = this return coroutineScope { model.generate(messages) } } - - diff --git a/langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/model/chat/StreamingChatLanguageModelExtensions.kt b/langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/StreamingChatLanguageModelExtensions.kt similarity index 61% rename from langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/model/chat/StreamingChatLanguageModelExtensions.kt rename to langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/StreamingChatLanguageModelExtensions.kt index e8c6bc9..93e60c9 100644 --- a/langchain4j-core-kotlin/src/main/kotlin/dev/langchain4j/model/chat/StreamingChatLanguageModelExtensions.kt +++ b/langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/StreamingChatLanguageModelExtensions.kt @@ -1,13 +1,14 @@ -package dev.langchain4j.model.chat +package me.kpavlov.langchain4j.kotlin.model.chat import dev.langchain4j.data.message.AiMessage import dev.langchain4j.data.message.ChatMessage -import dev.langchain4j.internal.PII import dev.langchain4j.model.StreamingResponseHandler +import dev.langchain4j.model.chat.StreamingChatLanguageModel import dev.langchain4j.model.output.Response import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import me.kpavlov.langchain4j.kotlin.internal.PII import org.slf4j.LoggerFactory private val logger = LoggerFactory.getLogger(StreamingChatLanguageModel::class.java) @@ -22,14 +23,19 @@ sealed interface StreamingChatLanguageModelReply { * * @property token The individual token string generated by the model. */ - data class Token(val token: String) : StreamingChatLanguageModelReply + data class Token( + val token: String, + ) : StreamingChatLanguageModelReply + /** * Represents the complete response received at the end of the streaming process. * This includes the final message along with any additional metadata. * * @property response The complete response containing the AI message and associated metadata. */ - data class Completion(val response: Response) : StreamingChatLanguageModelReply + data class Completion( + val response: Response, + ) : StreamingChatLanguageModelReply } /** @@ -64,37 +70,42 @@ sealed interface StreamingChatLanguageModelReply { */ fun StreamingChatLanguageModel.generateFlow( messages: List, -): Flow = callbackFlow { - - val model = this@generateFlow +): Flow = + callbackFlow { + val model = this@generateFlow - val handler = object : StreamingResponseHandler { - override fun onNext(token: String) { - logger.trace(PII, "Received token: {}", token) - trySend(StreamingChatLanguageModelReply.Token(token)) - } + val handler = + object : StreamingResponseHandler { + override fun onNext(token: String) { + logger.trace( + me.kpavlov.langchain4j.kotlin.internal.PII, + "Received token: {}", + token, + ) + trySend(StreamingChatLanguageModelReply.Token(token)) + } - override fun onComplete(response: Response) { - logger.trace(PII, "Received response: {}", response) - trySend(StreamingChatLanguageModelReply.Completion(response)) - close() - } + override fun onComplete(response: Response) { + logger.trace( + me.kpavlov.langchain4j.kotlin.internal.PII, + "Received response: {}", + response, + ) + trySend(StreamingChatLanguageModelReply.Completion(response)) + close() + } - override fun onError(error: Throwable) { - close(error) - } - } + override fun onError(error: Throwable) { + close(error) + } + } - logger.info("Starting flow...") - model.generate(messages, handler) + logger.info("Starting flow...") + model.generate(messages, handler) - // This will be called when the flow collection is closed or cancelled. - awaitClose { - // cleanup - logger.info("Flow is canceled") + // This will be called when the flow collection is closed or cancelled. + awaitClose { + // cleanup + logger.info("Flow is canceled") + } } -} - - - - diff --git a/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/ChatLanguageModelIT.kt b/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/ChatLanguageModelIT.kt new file mode 100644 index 0000000..1a55ef8 --- /dev/null +++ b/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/ChatLanguageModelIT.kt @@ -0,0 +1,99 @@ +package me.kpavlov.langchain4j.kotlin + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isNotNull +import dev.langchain4j.data.document.Document +import dev.langchain4j.data.message.SystemMessage +import dev.langchain4j.data.message.UserMessage +import dev.langchain4j.model.chat.ChatLanguageModel +import dev.langchain4j.model.chat.request.ChatRequest +import dev.langchain4j.model.chat.request.ResponseFormat +import dev.langchain4j.model.openai.OpenAiChatModel +import kotlinx.coroutines.test.runTest +import me.kpavlov.langchain4j.kotlin.model.chat.chatAsync +import me.kpavlov.langchain4j.kotlin.model.chat.generateAsync +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.slf4j.LoggerFactory + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class ChatLanguageModelIT { + private val logger = LoggerFactory.getLogger(javaClass) + + private val model: ChatLanguageModel = + OpenAiChatModel + .builder() + .apiKey(TestEnvironment.openaiApiKey) + .modelName("gpt-4o-mini") + .temperature(0.0) + .maxTokens(1024) + .build() + + private lateinit var document: Document + + @BeforeAll + fun beforeAll() = + runTest { + document = loadDocument("notes/blumblefang.txt", logger) + } + + @Test + fun `ChatLanguageModel should generateAsync`() = + runTest { + val response = + model.generateAsync( + listOf( + SystemMessage.from( + """ + You are helpful advisor answering questions only related to the given text + + """.trimIndent(), + ), + UserMessage.from( + """ + What does Blumblefang love? Text: ```${document.text()}``` + """.trimIndent(), + ), + ), + ) + + logger.info("Response: {}", response) + assertThat(response).isNotNull() + val content = response.content() + assertThat(content.text()).contains("Blumblefang loves to help") + } + + @Test + fun `ChatLanguageModel should chatAsync`() = + runTest { + val document = loadDocument("notes/blumblefang.txt", logger) + + val response = + model.chatAsync( + ChatRequest + .builder() + .messages( + listOf( + SystemMessage.from( + """ + You are helpful advisor answering questions only related to the given text + + """.trimIndent(), + ), + UserMessage.from( + """ + What does Blumblefang love? Text: ```${document.text()}``` + """.trimIndent(), + ), + ), + ).responseFormat(ResponseFormat.TEXT), + ) + + logger.info("Response: {}", response) + assertThat(response).isNotNull() + val content = response.aiMessage() + assertThat(content.text()).contains("Blumblefang loves to help") + } +} diff --git a/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/Documents.kt b/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/Documents.kt similarity index 87% rename from langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/Documents.kt rename to langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/Documents.kt index 9853f1b..c2818ae 100644 --- a/langchain4j-core-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/Documents.kt +++ b/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/Documents.kt @@ -7,7 +7,10 @@ import dev.langchain4j.data.document.source.FileSystemSource import org.slf4j.Logger import java.nio.file.Paths -suspend fun loadDocument(documentName: String,logger: Logger) : Document { +suspend fun loadDocument( + documentName: String, + logger: Logger, +): Document { val source = FileSystemSource(Paths.get("./src/test/resources/data/$documentName")) val document = DocumentLoader.load(source, TextDocumentParser()) diff --git a/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/StreamingChatLanguageModelIT.kt b/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/StreamingChatLanguageModelIT.kt new file mode 100644 index 0000000..cfa6461 --- /dev/null +++ b/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/StreamingChatLanguageModelIT.kt @@ -0,0 +1,87 @@ +package me.kpavlov.langchain4j.kotlin + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import dev.langchain4j.data.message.AiMessage +import dev.langchain4j.data.message.ChatMessage +import dev.langchain4j.data.message.SystemMessage +import dev.langchain4j.data.message.UserMessage +import dev.langchain4j.model.chat.StreamingChatLanguageModel +import dev.langchain4j.model.openai.OpenAiStreamingChatModel +import dev.langchain4j.model.output.Response +import kotlinx.coroutines.test.runTest +import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatLanguageModelReply.Completion +import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatLanguageModelReply.Token +import me.kpavlov.langchain4j.kotlin.model.chat.generateFlow +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicReference + +@EnabledIfEnvironmentVariable( + named = "OPENAI_API_KEY", + matches = ".+", +) +class StreamingChatLanguageModelIT { + private val logger = LoggerFactory.getLogger(javaClass) + + private val model: StreamingChatLanguageModel = + OpenAiStreamingChatModel + .builder() + .apiKey(TestEnvironment.openaiApiKey) + .modelName("gpt-4o-mini") + .temperature(0.0) + .maxTokens(100) + .build() + + @Test + fun `StreamingChatLanguageModel should generateFlow`() = + runTest { + val document = loadDocument("notes/blumblefang.txt", logger) + + val messages = + listOf( + SystemMessage.from( + """ + You are helpful advisor answering questions only related to the given text + + """.trimIndent(), + ), + UserMessage.from( + """ + What does Blumblefang love? Text: ```${document.text()}``` + """.trimIndent(), + ), + ) + + val responseRef = AtomicReference?>() + + val collectedTokens = mutableListOf() + + model + .generateFlow(messages) + .collect { + logger.info("Received event: $it") + when (it) { + is Token -> { + logger.info("Token: '${it.token}'") + collectedTokens.add(it.token) + } + + is Completion -> responseRef.set(it.response) + else -> fail("Unsupported event: $it") + } + } + + val response = responseRef.get()!! + assertThat(response.metadata()).isNotNull() + val content = response.content() + assertThat(content).isNotNull() + assertThat(collectedTokens.joinToString("")) + .isEqualTo(content.text()) + assertThat(content.text()).contains("Blumblefang loves to help") + } +} diff --git a/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/TestEnvironment.kt b/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/TestEnvironment.kt new file mode 100644 index 0000000..8e65690 --- /dev/null +++ b/langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/TestEnvironment.kt @@ -0,0 +1,7 @@ +package me.kpavlov.langchain4j.kotlin + +object TestEnvironment : me.kpavlov.finchly.BaseTestEnvironment( + dotEnvFileDir = "../", +) { + val openaiApiKey = TestEnvironment.get("OPENAI_API_KEY", "demo") +} diff --git a/langchain4j-core-kotlin/src/test/resources/data/books/captain-blood.txt b/langchain4j-kotlin/src/test/resources/data/books/captain-blood.txt similarity index 100% rename from langchain4j-core-kotlin/src/test/resources/data/books/captain-blood.txt rename to langchain4j-kotlin/src/test/resources/data/books/captain-blood.txt diff --git a/langchain4j-core-kotlin/src/test/resources/data/notes/blumblefang.txt b/langchain4j-kotlin/src/test/resources/data/notes/blumblefang.txt similarity index 100% rename from langchain4j-core-kotlin/src/test/resources/data/notes/blumblefang.txt rename to langchain4j-kotlin/src/test/resources/data/notes/blumblefang.txt diff --git a/langchain4j-core-kotlin/src/test/resources/data/notes/quantum-computing.txt b/langchain4j-kotlin/src/test/resources/data/notes/quantum-computing.txt similarity index 100% rename from langchain4j-core-kotlin/src/test/resources/data/notes/quantum-computing.txt rename to langchain4j-kotlin/src/test/resources/data/notes/quantum-computing.txt diff --git a/pom.xml b/pom.xml index f7eeeae..c77520e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,20 +1,46 @@ - + 4.0.0 me.kpavlov.langchain4j.kotlin - langchain4j-kotlin-aggregator + root 0.1.1-SNAPSHOT pom LangChain4j-Kotlin :: Aggregator Kotlin enhancements for LangChain4j https://github.com/kpavlov/langchain4j-kotlin + + + MIT License + https://opensource.org/license/mit + + + + + + Konstantin Pavlov + https://kpavlov.me + + author + owner + + + + - langchain4j-core-kotlin + langchain4j-kotlin reports + + scm:git:git://github.com/kpavlov/langchain4j-kotlin.git + scm:git:ssh://github.com/kpavlov/langchain4j-kotlin.git + https://github.com/kpavlov/langchain4j-kotlin/tree/main + HEAD + + UTF-8 official @@ -24,7 +50,7 @@ ${java.version} 4.2.2 - 6.4.2 + 0.1.1 5.11.3 1.9.0 0.35.0 @@ -65,6 +91,13 @@ pom import + + org.junit + junit-bom + ${junit.version} + pom + import + org.slf4j slf4j-simple @@ -77,6 +110,12 @@ 0.28.1 test + + me.kpavlov.finchly + finchly + ${finchly.version} + test + @@ -93,24 +132,6 @@ ${awaitility.version} test - - io.github.cdimascio - dotenv-kotlin - ${dotenv-kotlin.version} - test - - - org.jetbrains.kotlin - kotlin-test-junit5 - test - - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - org.jetbrains.kotlinx kotlinx-coroutines-test @@ -134,10 +155,12 @@ + org.apache.maven.plugins maven-surefire-plugin 3.5.2 + org.apache.maven.plugins maven-failsafe-plugin 3.5.2 @@ -182,6 +205,11 @@ maven-javadoc-plugin 3.11.1 + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.6.2 + @@ -212,6 +240,7 @@ + org.apache.maven.plugins maven-failsafe-plugin @@ -284,31 +313,6 @@ - - scm:git:git://github.com/kpavlov/langchain4j-kotlin.git - scm:git:ssh://github.com/kpavlov/langchain4j-kotlin.git - https://github.com/kpavlov/langchain4j-kotlin/tree/main - HEAD - - - - - MIT License - https://opensource.org/license/mit - - - - - - Konstantin Pavlov - https://kpavlov.me - - author - owner - - - - release @@ -355,6 +359,25 @@ + + lint + + + + org.openrewrite.maven + rewrite-maven-plugin + 5.43.0 + + true + + org.openrewrite.maven.BestPractices + + true + + + + + diff --git a/reports/pom.xml b/reports/pom.xml index bf93b6d..a511211 100644 --- a/reports/pom.xml +++ b/reports/pom.xml @@ -3,8 +3,9 @@ 4.0.0 me.kpavlov.langchain4j.kotlin - langchain4j-kotlin-aggregator + root 0.1.1-SNAPSHOT + ../pom.xml reports @@ -14,7 +15,7 @@ me.kpavlov.langchain4j.kotlin - langchain4j-core-kotlin + langchain4j-kotlin ${project.parent.version}